@storyteller-platform/epub 0.6.3 → 0.7.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 +529 -1075
- package/dist/adapters/interface.d.cts +25 -19
- package/dist/adapters/interface.d.ts +25 -19
- package/dist/adapters/memory.cjs +19 -29
- package/dist/adapters/memory.d.cts +4 -5
- package/dist/adapters/memory.d.ts +4 -5
- package/dist/adapters/memory.js +19 -32
- package/dist/adapters/tmpfs.cjs +50 -45
- package/dist/adapters/tmpfs.d.cts +9 -7
- package/dist/adapters/tmpfs.d.ts +9 -7
- package/dist/adapters/tmpfs.js +50 -45
- package/dist/epubcheck.cjs +2 -2
- package/dist/epubcheck.js +2 -2
- package/dist/index.cjs +585 -94
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +586 -100
- package/dist/upgrade.cjs +12 -44
- package/dist/upgrade.d.cts +175 -11
- package/dist/upgrade.d.ts +175 -11
- package/dist/upgrade.js +12 -44
- package/dist/zip.cjs +142 -0
- package/dist/zip.d.cts +5 -0
- package/dist/zip.d.ts +5 -0
- package/dist/zip.js +124 -0
- package/package.json +4 -5
package/dist/index.js
CHANGED
|
@@ -3,14 +3,9 @@ import { cp, mkdir } from "node:fs/promises";
|
|
|
3
3
|
import { Mutex } from "async-mutex";
|
|
4
4
|
import { XMLBuilder, XMLParser } from "fast-xml-parser";
|
|
5
5
|
import memoize from "mem";
|
|
6
|
-
import { lookup } from "mime-types";
|
|
7
6
|
import { nanoid } from "nanoid";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
hrefToPlatformPath,
|
|
11
|
-
join,
|
|
12
|
-
resolve
|
|
13
|
-
} from "@storyteller-platform/path";
|
|
7
|
+
import { MediaType } from "@storyteller-platform/media-types";
|
|
8
|
+
import { dirname } from "@storyteller-platform/path";
|
|
14
9
|
import { TmpFsAdapter } from "./adapters/tmpfs.js";
|
|
15
10
|
import * as Upgrade from "./upgrade.js";
|
|
16
11
|
import { MemoryAdapter } from "./adapters/memory.js";
|
|
@@ -19,6 +14,61 @@ class EpubVersionError extends Error {
|
|
|
19
14
|
}
|
|
20
15
|
class EpubReadOnlyError extends Error {
|
|
21
16
|
}
|
|
17
|
+
const OPF_NAMESPACE = "http://www.idpf.org/2007/opf";
|
|
18
|
+
const ENTRY_ORIGIN = "epub:///";
|
|
19
|
+
function entryToUrlPath(entry) {
|
|
20
|
+
return entry.split("/").map(encodeURIComponent).join("/");
|
|
21
|
+
}
|
|
22
|
+
function resolveEntryUrl(from, href) {
|
|
23
|
+
const base = new URL(entryToUrlPath(from), ENTRY_ORIGIN);
|
|
24
|
+
if (!URL.canParse(href, base.toString())) return null;
|
|
25
|
+
const url = new URL(href, base);
|
|
26
|
+
return url.href.startsWith(ENTRY_ORIGIN) ? url : null;
|
|
27
|
+
}
|
|
28
|
+
function renameOpfAttributes(attrs, prefix) {
|
|
29
|
+
const attrPrefix = `@_${prefix}:`;
|
|
30
|
+
const renamed = {};
|
|
31
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
32
|
+
if (key === `@_xmlns:${prefix}`) continue;
|
|
33
|
+
if (key.startsWith(attrPrefix)) {
|
|
34
|
+
renamed[`@_opf:${key.slice(attrPrefix.length)}`] = value;
|
|
35
|
+
} else {
|
|
36
|
+
renamed[key] = value;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return renamed;
|
|
40
|
+
}
|
|
41
|
+
function stripOpfPrefix(node, prefix) {
|
|
42
|
+
if (Epub.isXmlTextNode(node)) return node;
|
|
43
|
+
const name = Epub.getXmlElementName(node);
|
|
44
|
+
const bareName = name.startsWith(`${prefix}:`) ? name.slice(prefix.length + 1) : name;
|
|
45
|
+
const element = {
|
|
46
|
+
[bareName]: Epub.getXmlChildren(node).map(
|
|
47
|
+
(child) => stripOpfPrefix(child, prefix)
|
|
48
|
+
)
|
|
49
|
+
};
|
|
50
|
+
if (node[":@"]) {
|
|
51
|
+
element[":@"] = renameOpfAttributes(node[":@"], prefix);
|
|
52
|
+
}
|
|
53
|
+
return element;
|
|
54
|
+
}
|
|
55
|
+
function normalizeOpfNamespacePrefixes(doc) {
|
|
56
|
+
var _a;
|
|
57
|
+
const root = doc.find(
|
|
58
|
+
(node) => !Epub.isXmlTextNode(node) && Epub.getXmlElementName(node).endsWith(":package")
|
|
59
|
+
);
|
|
60
|
+
if (!root) return doc;
|
|
61
|
+
const prefix = Epub.getXmlElementName(root).slice(0, -":package".length);
|
|
62
|
+
if (((_a = root[":@"]) == null ? void 0 : _a[`@_xmlns:${prefix}`]) !== OPF_NAMESPACE) return doc;
|
|
63
|
+
const stripped = stripOpfPrefix(root, prefix);
|
|
64
|
+
stripped[":@"] = {
|
|
65
|
+
...stripped[":@"],
|
|
66
|
+
"@_xmlns": OPF_NAMESPACE,
|
|
67
|
+
"@_xmlns:opf": OPF_NAMESPACE
|
|
68
|
+
};
|
|
69
|
+
doc[doc.indexOf(root)] = stripped;
|
|
70
|
+
return doc;
|
|
71
|
+
}
|
|
22
72
|
class Epub {
|
|
23
73
|
/**
|
|
24
74
|
* Prefer the static factories ({@link Epub.using}, {@link Epub.from},
|
|
@@ -299,14 +349,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
299
349
|
);
|
|
300
350
|
}
|
|
301
351
|
const rootfile = await this.getRootfile();
|
|
302
|
-
|
|
303
|
-
await this.adapter.remove(filename);
|
|
304
|
-
}
|
|
305
|
-
async getFileData(path, encoding) {
|
|
306
|
-
if (encoding) {
|
|
307
|
-
return this.adapter.read(path, encoding);
|
|
308
|
-
}
|
|
309
|
-
return this.adapter.read(path);
|
|
352
|
+
await this.adapter.remove(await this.resolveEntry(rootfile, href));
|
|
310
353
|
}
|
|
311
354
|
/**
|
|
312
355
|
* Length of the underlying archive entry for a manifest item, in bytes
|
|
@@ -319,14 +362,15 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
319
362
|
const manifestItem = manifest[id];
|
|
320
363
|
if (!manifestItem)
|
|
321
364
|
throw new Error(`Could not find item with id "${id}" in manifest`);
|
|
322
|
-
|
|
323
|
-
|
|
365
|
+
return this.adapter.archiveLength(
|
|
366
|
+
await this.resolveEntry(rootfile, manifestItem.href)
|
|
367
|
+
);
|
|
324
368
|
}
|
|
325
369
|
async getRootfile() {
|
|
326
370
|
var _a;
|
|
327
371
|
if (this.rootfile !== null) return this.rootfile;
|
|
328
|
-
const containerString = await this.
|
|
329
|
-
|
|
372
|
+
const containerString = await this.adapter.read(
|
|
373
|
+
"META-INF/container.xml",
|
|
330
374
|
"utf-8"
|
|
331
375
|
);
|
|
332
376
|
if (!containerString)
|
|
@@ -350,7 +394,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
350
394
|
Epub.getXmlChildren(rootfiles),
|
|
351
395
|
(node) => {
|
|
352
396
|
var _a2;
|
|
353
|
-
return !Epub.isXmlTextNode(node) && ((_a2 = node[":@"]) == null ? void 0 : _a2["@_media-type"]) ===
|
|
397
|
+
return !Epub.isXmlTextNode(node) && ((_a2 = node[":@"]) == null ? void 0 : _a2["@_media-type"]) === MediaType.OPF.mime;
|
|
354
398
|
}
|
|
355
399
|
);
|
|
356
400
|
const fullPath = (_a = rootfile == null ? void 0 : rootfile[":@"]) == null ? void 0 : _a["@_full-path"];
|
|
@@ -358,12 +402,12 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
358
402
|
throw new Error(
|
|
359
403
|
"Failed to parse EPUB container.xml: Found no rootfile element"
|
|
360
404
|
);
|
|
361
|
-
this.rootfile =
|
|
405
|
+
this.rootfile = await this.resolveEntry("", fullPath);
|
|
362
406
|
return this.rootfile;
|
|
363
407
|
}
|
|
364
408
|
async getPackageDocument() {
|
|
365
409
|
const rootfile = await this.getRootfile();
|
|
366
|
-
const packageDocumentString = await this.
|
|
410
|
+
const packageDocumentString = await this.adapter.read(rootfile, "utf-8");
|
|
367
411
|
if (!packageDocumentString)
|
|
368
412
|
throw new Error(
|
|
369
413
|
`Failed to parse EPUB: could not find package document at ${rootfile}`
|
|
@@ -371,7 +415,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
371
415
|
const packageDocument = Epub.xmlParser.parse(
|
|
372
416
|
packageDocumentString
|
|
373
417
|
);
|
|
374
|
-
return packageDocument;
|
|
418
|
+
return normalizeOpfNamespacePrefixes(packageDocument);
|
|
375
419
|
}
|
|
376
420
|
async getPackageElement() {
|
|
377
421
|
const packageDocument = await this.getPackageDocument();
|
|
@@ -550,12 +594,15 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
550
594
|
return metadata;
|
|
551
595
|
}
|
|
552
596
|
/**
|
|
553
|
-
* Retrieve the identifier from the dc:identifier element
|
|
597
|
+
* Retrieve the first identifier from the dc:identifier element
|
|
554
598
|
* in the EPUB metadata.
|
|
555
599
|
*
|
|
556
600
|
* If there is no dc:identifier element, returns null.
|
|
557
601
|
*
|
|
558
602
|
* @link https://www.w3.org/TR/epub-33/#sec-opf-dcidentifier
|
|
603
|
+
*
|
|
604
|
+
* @deprecated Use {@link getUniqueIdentifier} instead to get the unique identifier,
|
|
605
|
+
* or {@link getIdentifiers} to get all identifiers.
|
|
559
606
|
*/
|
|
560
607
|
async getIdentifier() {
|
|
561
608
|
const metadata = await this.getMetadata();
|
|
@@ -569,6 +616,8 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
569
616
|
* Otherwise creates a new element
|
|
570
617
|
*
|
|
571
618
|
* @link https://www.w3.org/TR/epub-33/#sec-opf-dcidentifier
|
|
619
|
+
*
|
|
620
|
+
* @deprecated Use {@link setUniqueIdentifier} instead.
|
|
572
621
|
*/
|
|
573
622
|
async setIdentifier(identifier) {
|
|
574
623
|
await this.replaceMetadata(({ type }) => type === "dc:identifier", {
|
|
@@ -577,6 +626,395 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
577
626
|
value: identifier
|
|
578
627
|
});
|
|
579
628
|
}
|
|
629
|
+
/**
|
|
630
|
+
* Retrieve the identifier with the unique identifier id
|
|
631
|
+
* in the EPUB metadata.
|
|
632
|
+
*
|
|
633
|
+
* If there is no unique identifier id, returns the first dc:identifier element.
|
|
634
|
+
*
|
|
635
|
+
* @link https://www.w3.org/TR/epub-33/#sec-opf-dcidentifier
|
|
636
|
+
*/
|
|
637
|
+
async getUniqueIdentifier() {
|
|
638
|
+
const metadata = await this.getMetadata();
|
|
639
|
+
const uniqueId = await this.getUniqueIdentifierId();
|
|
640
|
+
if (!uniqueId) {
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
const entry = metadata.find(
|
|
644
|
+
({ type, id }) => type === "dc:identifier" && id === uniqueId
|
|
645
|
+
);
|
|
646
|
+
return (entry == null ? void 0 : entry.value) ?? null;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Set the unique identifier id for the EPUB.
|
|
650
|
+
*
|
|
651
|
+
* Updates the existing dc:identifier element referenced by the unique identifier id if one exists.
|
|
652
|
+
* Otherwise creates a new element with the provided identifier, and sets the unique identifier id to the new element's id.
|
|
653
|
+
*
|
|
654
|
+
* Note: you likely shouldn't change the unique identifier id unless you are producing a new EPUB.
|
|
655
|
+
*
|
|
656
|
+
* @link https://www.w3.org/TR/epub-33/#sec-opf-dcidentifier
|
|
657
|
+
*/
|
|
658
|
+
async setUniqueIdentifier(identifier) {
|
|
659
|
+
await this.withPackage(async (packageElement) => {
|
|
660
|
+
const metadata = Epub.findXmlChildByName(
|
|
661
|
+
"metadata",
|
|
662
|
+
Epub.getXmlChildren(packageElement)
|
|
663
|
+
);
|
|
664
|
+
if (!metadata) {
|
|
665
|
+
throw new Error(
|
|
666
|
+
"Failed to parse EPUB: found no metadata element in package document"
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
let uniqueId = await this.getUniqueIdentifierId();
|
|
670
|
+
if (!uniqueId) {
|
|
671
|
+
const newUniqueId = nanoid();
|
|
672
|
+
packageElement[":@"] = {
|
|
673
|
+
...packageElement[":@"],
|
|
674
|
+
"@_unique-identifier": newUniqueId
|
|
675
|
+
};
|
|
676
|
+
uniqueId = newUniqueId;
|
|
677
|
+
}
|
|
678
|
+
const children = Epub.getXmlChildren(metadata);
|
|
679
|
+
const entry = Epub.findXmlChildByName(
|
|
680
|
+
"dc:identifier",
|
|
681
|
+
children,
|
|
682
|
+
(node) => {
|
|
683
|
+
var _a;
|
|
684
|
+
return ((_a = node[":@"]) == null ? void 0 : _a["@_id"]) === uniqueId;
|
|
685
|
+
}
|
|
686
|
+
);
|
|
687
|
+
if (entry) {
|
|
688
|
+
children.splice(
|
|
689
|
+
children.indexOf(entry),
|
|
690
|
+
1,
|
|
691
|
+
Epub.createXmlElement("dc:identifier", { id: uniqueId }, [
|
|
692
|
+
Epub.createXmlTextNode(identifier)
|
|
693
|
+
])
|
|
694
|
+
);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
children.push(
|
|
698
|
+
Epub.createXmlElement("dc:identifier", { id: uniqueId }, [
|
|
699
|
+
Epub.createXmlTextNode(identifier)
|
|
700
|
+
])
|
|
701
|
+
);
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Retrieve the id of the publication's unique identifier, as declared by the
|
|
706
|
+
* package element's `unique-identifier` attribute.
|
|
707
|
+
*
|
|
708
|
+
* @link https://www.w3.org/TR/epub-33/#sec-opf-dcidentifier
|
|
709
|
+
*/
|
|
710
|
+
async getUniqueIdentifierId() {
|
|
711
|
+
var _a;
|
|
712
|
+
const packageElement = await this.getPackageElement();
|
|
713
|
+
return ((_a = packageElement[":@"]) == null ? void 0 : _a["@_unique-identifier"]) ?? null;
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Collect `dc:identifier` or `dc:source` entries, attaching the value and
|
|
717
|
+
* scheme of any refining `identifier-type` meta (spec D.3.8) and a legacy
|
|
718
|
+
* `opf:scheme` attribute. Values are not interpreted.
|
|
719
|
+
*
|
|
720
|
+
* `onRefinement` is invoked for every other meta refining a collected entry,
|
|
721
|
+
* so callers can surface element-specific refinements (e.g. `source-of` on a
|
|
722
|
+
* `dc:source`).
|
|
723
|
+
*/
|
|
724
|
+
static collectDcEntries(metadata, type, onRefinement) {
|
|
725
|
+
const entries = metadata.filter((entry) => entry.type === type && entry.value !== void 0).map((entry) => ({
|
|
726
|
+
// filtered above, so value is defined
|
|
727
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
728
|
+
value: entry.value,
|
|
729
|
+
...entry.id && { id: entry.id },
|
|
730
|
+
...entry.properties["opf:scheme"] && {
|
|
731
|
+
scheme: entry.properties["opf:scheme"]
|
|
732
|
+
}
|
|
733
|
+
}));
|
|
734
|
+
for (const meta of metadata) {
|
|
735
|
+
if (meta.type !== "meta" || meta.value === void 0) continue;
|
|
736
|
+
const property = meta.properties["property"];
|
|
737
|
+
const refines = meta.properties["refines"];
|
|
738
|
+
if (!property || !refines) continue;
|
|
739
|
+
const target = entries.find((t) => t.id === refines.slice(1));
|
|
740
|
+
if (!target) continue;
|
|
741
|
+
if (property === "identifier-type") {
|
|
742
|
+
target.identifierType = meta.value;
|
|
743
|
+
if (meta.properties["scheme"]) target.scheme = meta.properties["scheme"];
|
|
744
|
+
} else {
|
|
745
|
+
onRefinement == null ? void 0 : onRefinement(target, property, meta.value);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return entries;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Retrieve every `dc:identifier` entry, returned as found.
|
|
752
|
+
*
|
|
753
|
+
* Values are not interpreted. Any refining `identifier-type` meta (spec
|
|
754
|
+
* D.3.8) or legacy `opf:scheme` attribute is surfaced on the entry, but no
|
|
755
|
+
* parsing of the value itself is attempted. To read `dc:source` entries, use
|
|
756
|
+
* {@link getSources}.
|
|
757
|
+
*
|
|
758
|
+
* @link https://www.w3.org/TR/epub-33/#sec-opf-dcidentifier
|
|
759
|
+
*/
|
|
760
|
+
async getIdentifiers() {
|
|
761
|
+
const metadata = await this.getMetadata();
|
|
762
|
+
return Epub.collectDcEntries(metadata, "dc:identifier");
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Retrieve every `dc:source` entry, returned as found.
|
|
766
|
+
*
|
|
767
|
+
* Like {@link getIdentifiers}, values are not interpreted. In addition to a
|
|
768
|
+
* refining `identifier-type`, a refining `source-of` meta (spec D.3.11) is
|
|
769
|
+
* surfaced as `sourceOf`.
|
|
770
|
+
*
|
|
771
|
+
* @link https://www.w3.org/TR/epub-33/#sec-opf-dcsource
|
|
772
|
+
*/
|
|
773
|
+
async getSources() {
|
|
774
|
+
const metadata = await this.getMetadata();
|
|
775
|
+
return Epub.collectDcEntries(
|
|
776
|
+
metadata,
|
|
777
|
+
"dc:source",
|
|
778
|
+
(source, property) => {
|
|
779
|
+
if (property === "source-of") {
|
|
780
|
+
source.isPageBreakSource = true;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Retrieve the `pageBreakSource` property (EPUB 3.4, spec D.2.9), the
|
|
787
|
+
* publication-level source for the source of its page break markers.
|
|
788
|
+
*
|
|
789
|
+
* This property replaces the refining `source-of="pagination"` meta (spec
|
|
790
|
+
* D.3.11), see {@link EpubSource.sourceOf}. If no `pageBreakSource` property is found,
|
|
791
|
+
* we fall back to finding a `dc:source` with a `source-of="pagination"` refinement.
|
|
792
|
+
*
|
|
793
|
+
* @link https://www.w3.org/TR/epub/#pageBreakSource
|
|
794
|
+
*/
|
|
795
|
+
async getPageBreakSource() {
|
|
796
|
+
const entry = await this.findMetadataItem(
|
|
797
|
+
(item) => item.type === "meta" && item.properties["property"] === "pageBreakSource" && !!item.value
|
|
798
|
+
);
|
|
799
|
+
if (entry) {
|
|
800
|
+
return {
|
|
801
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
802
|
+
value: entry.value,
|
|
803
|
+
id: entry.id,
|
|
804
|
+
identifierType: void 0,
|
|
805
|
+
scheme: void 0,
|
|
806
|
+
isPageBreakSource: true
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
const sources = await this.getSources();
|
|
810
|
+
const source = sources.find((source2) => source2.isPageBreakSource);
|
|
811
|
+
if (source) {
|
|
812
|
+
return source;
|
|
813
|
+
}
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Set the `pageBreakSource` property (EPUB 3.4, spec D.2.9), or remove it when
|
|
818
|
+
* passed null. Replaces an existing `pageBreakSource` meta if present.
|
|
819
|
+
*
|
|
820
|
+
* Pass `none` to indicate the pagination is unique to this publication.
|
|
821
|
+
*
|
|
822
|
+
* @link https://www.w3.org/TR/epub/#pageBreakSource
|
|
823
|
+
*/
|
|
824
|
+
async setPageBreakSource(value) {
|
|
825
|
+
if (value === null) {
|
|
826
|
+
await this.removeMetadata(
|
|
827
|
+
(item) => item.properties["property"] === "pageBreakSource"
|
|
828
|
+
);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
await this.replaceMetadata(
|
|
832
|
+
(item) => item.properties["property"] === "pageBreakSource",
|
|
833
|
+
{ type: "meta", properties: { property: "pageBreakSource" }, value }
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Replace the publication's `dc:identifier` entries.
|
|
838
|
+
*
|
|
839
|
+
* This replaces ALL existing `dc:identifier` elements except the publication's
|
|
840
|
+
* unique identifier (the one referenced by the package element's
|
|
841
|
+
* `unique-identifier` attribute), which is always preserved and must not be
|
|
842
|
+
* included in the provided list. If included anyway, it is ignored. See
|
|
843
|
+
* {@link setUniqueIdentifier} to change it.
|
|
844
|
+
*
|
|
845
|
+
* Identifiers are placed in the order they are provided.
|
|
846
|
+
*
|
|
847
|
+
* `dc:source` entries are not touched, use {@link setSources} for those.
|
|
848
|
+
*
|
|
849
|
+
* When an entry has an `identifierType`, it is written in the refining form
|
|
850
|
+
* (a `meta` with `property="identifier-type"`, carrying the `scheme`
|
|
851
|
+
* attribute when provided). An entry with only a `scheme` is written using
|
|
852
|
+
* the legacy `opf:scheme` attribute.
|
|
853
|
+
*
|
|
854
|
+
* @link https://www.w3.org/TR/epub-33/#sec-opf-dcidentifier
|
|
855
|
+
*/
|
|
856
|
+
async setIdentifiers(identifiers) {
|
|
857
|
+
const uniqueId = await this.getUniqueIdentifierId();
|
|
858
|
+
await this.withPackage((packageElement) => {
|
|
859
|
+
var _a;
|
|
860
|
+
const metadata = Epub.findXmlChildByName(
|
|
861
|
+
"metadata",
|
|
862
|
+
Epub.getXmlChildren(packageElement)
|
|
863
|
+
);
|
|
864
|
+
if (!metadata)
|
|
865
|
+
throw new Error(
|
|
866
|
+
"Failed to parse EPUB: found no metadata element in package document"
|
|
867
|
+
);
|
|
868
|
+
const children = Epub.getXmlChildren(metadata);
|
|
869
|
+
const removedIds = /* @__PURE__ */ new Set();
|
|
870
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
871
|
+
const node = children[i];
|
|
872
|
+
if (Epub.isXmlTextNode(node) || Epub.getXmlElementName(node) !== "dc:identifier") {
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
const id = (_a = node[":@"]) == null ? void 0 : _a["@_id"];
|
|
876
|
+
if (id && id === uniqueId) {
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
if (id) {
|
|
880
|
+
removedIds.add(id);
|
|
881
|
+
}
|
|
882
|
+
children.splice(i, 1);
|
|
883
|
+
}
|
|
884
|
+
Epub.removeRefiningMetas(children, removedIds, ["identifier-type"]);
|
|
885
|
+
for (const identifier of identifiers) {
|
|
886
|
+
if (identifier.id && identifier.id === uniqueId) continue;
|
|
887
|
+
const id = identifier.id ?? (identifier.identifierType !== void 0 ? nanoid() : void 0);
|
|
888
|
+
children.push(
|
|
889
|
+
Epub.createXmlElement(
|
|
890
|
+
"dc:identifier",
|
|
891
|
+
{
|
|
892
|
+
...id && { id },
|
|
893
|
+
...identifier.scheme && identifier.identifierType === void 0 && {
|
|
894
|
+
"opf:scheme": identifier.scheme
|
|
895
|
+
}
|
|
896
|
+
},
|
|
897
|
+
[Epub.createXmlTextNode(identifier.value)]
|
|
898
|
+
)
|
|
899
|
+
);
|
|
900
|
+
if (identifier.identifierType !== void 0 && id) {
|
|
901
|
+
children.push(
|
|
902
|
+
Epub.createXmlElement(
|
|
903
|
+
"meta",
|
|
904
|
+
{
|
|
905
|
+
refines: `#${id}`,
|
|
906
|
+
property: "identifier-type",
|
|
907
|
+
...identifier.scheme && { scheme: identifier.scheme }
|
|
908
|
+
},
|
|
909
|
+
[Epub.createXmlTextNode(identifier.identifierType)]
|
|
910
|
+
)
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Remove `meta` refinements pointing at any of the given ids,
|
|
918
|
+
* restricted to the given `property` values as a cleanup step
|
|
919
|
+
* Mutates `children` in place.
|
|
920
|
+
*/
|
|
921
|
+
static removeRefiningMetas(children, ids, properties) {
|
|
922
|
+
var _a, _b, _c;
|
|
923
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
924
|
+
const node = children[i];
|
|
925
|
+
if (Epub.isXmlTextNode(node) || Epub.getXmlElementName(node) !== "meta") {
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
const property = (_a = node[":@"]) == null ? void 0 : _a["@_property"];
|
|
929
|
+
if (!property || !properties.includes(property)) continue;
|
|
930
|
+
const refines = (_c = (_b = node[":@"]) == null ? void 0 : _b["@_refines"]) == null ? void 0 : _c.slice(1);
|
|
931
|
+
if (refines && ids.has(refines)) {
|
|
932
|
+
children.splice(i, 1);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Replace the publication's `dc:source` entries.
|
|
938
|
+
*
|
|
939
|
+
* This replaces ALL existing `dc:source` elements (and their refining
|
|
940
|
+
* `identifier-type` / `source-of` metas). `dc:identifier` entries are not
|
|
941
|
+
* touched; use {@link setIdentifiers} for those. Pass an empty array to
|
|
942
|
+
* remove all sources.
|
|
943
|
+
*
|
|
944
|
+
* When an entry has an `identifierType`, it is written in the refining form.
|
|
945
|
+
* A `sourceOf` value is written as a refining `source-of` meta (spec D.3.11).
|
|
946
|
+
* An entry with only a `scheme` uses the legacy `opf:scheme` attribute.
|
|
947
|
+
*
|
|
948
|
+
* @link https://www.w3.org/TR/epub-33/#sec-opf-dcsource
|
|
949
|
+
*/
|
|
950
|
+
async setSources(sources) {
|
|
951
|
+
await this.withPackage((packageElement) => {
|
|
952
|
+
var _a;
|
|
953
|
+
const metadata = Epub.findXmlChildByName(
|
|
954
|
+
"metadata",
|
|
955
|
+
Epub.getXmlChildren(packageElement)
|
|
956
|
+
);
|
|
957
|
+
if (!metadata)
|
|
958
|
+
throw new Error(
|
|
959
|
+
"Failed to parse EPUB: found no metadata element in package document"
|
|
960
|
+
);
|
|
961
|
+
const children = Epub.getXmlChildren(metadata);
|
|
962
|
+
const removedIds = /* @__PURE__ */ new Set();
|
|
963
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
964
|
+
const node = children[i];
|
|
965
|
+
if (Epub.isXmlTextNode(node) || Epub.getXmlElementName(node) !== "dc:source") {
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
const id = (_a = node[":@"]) == null ? void 0 : _a["@_id"];
|
|
969
|
+
if (id) {
|
|
970
|
+
removedIds.add(id);
|
|
971
|
+
}
|
|
972
|
+
children.splice(i, 1);
|
|
973
|
+
}
|
|
974
|
+
Epub.removeRefiningMetas(children, removedIds, [
|
|
975
|
+
"identifier-type",
|
|
976
|
+
"source-of"
|
|
977
|
+
]);
|
|
978
|
+
for (const source of sources) {
|
|
979
|
+
const needsId = source.identifierType !== void 0 || source.isPageBreakSource;
|
|
980
|
+
const id = source.id ?? (needsId ? nanoid() : void 0);
|
|
981
|
+
children.push(
|
|
982
|
+
Epub.createXmlElement(
|
|
983
|
+
"dc:source",
|
|
984
|
+
{
|
|
985
|
+
...id && { id },
|
|
986
|
+
...source.scheme && source.identifierType === void 0 && {
|
|
987
|
+
"opf:scheme": source.scheme
|
|
988
|
+
}
|
|
989
|
+
},
|
|
990
|
+
[Epub.createXmlTextNode(source.value)]
|
|
991
|
+
)
|
|
992
|
+
);
|
|
993
|
+
if (source.identifierType !== void 0 && id) {
|
|
994
|
+
children.push(
|
|
995
|
+
Epub.createXmlElement(
|
|
996
|
+
"meta",
|
|
997
|
+
{
|
|
998
|
+
refines: `#${id}`,
|
|
999
|
+
property: "identifier-type",
|
|
1000
|
+
...source.scheme && { scheme: source.scheme }
|
|
1001
|
+
},
|
|
1002
|
+
[Epub.createXmlTextNode(source.identifierType)]
|
|
1003
|
+
)
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
if (source.isPageBreakSource && id) {
|
|
1007
|
+
children.push(
|
|
1008
|
+
Epub.createXmlElement(
|
|
1009
|
+
"meta",
|
|
1010
|
+
{ refines: `#${id}`, property: "source-of" },
|
|
1011
|
+
[Epub.createXmlTextNode("pagination")]
|
|
1012
|
+
)
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
580
1018
|
/**
|
|
581
1019
|
* Even "EPUB 3" publications sometimes still only use the
|
|
582
1020
|
* EPUB 2 specification for identifying the cover image.
|
|
@@ -649,11 +1087,12 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
649
1087
|
* the provided href within the publication.
|
|
650
1088
|
*/
|
|
651
1089
|
async setCoverImage(href, data) {
|
|
1090
|
+
var _a;
|
|
652
1091
|
const coverImageItem = await this.getCoverImageItem();
|
|
653
1092
|
if (coverImageItem) {
|
|
654
1093
|
await this.removeManifestItem(coverImageItem.id);
|
|
655
1094
|
}
|
|
656
|
-
const mediaType =
|
|
1095
|
+
const mediaType = (_a = MediaType.fromPath(href)) == null ? void 0 : _a.mime;
|
|
657
1096
|
if (!mediaType)
|
|
658
1097
|
throw new Error(`Invalid file extension for cover image: ${href}`);
|
|
659
1098
|
await this.addManifestItem(
|
|
@@ -1632,35 +2071,81 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1632
2071
|
return this.getNavigation("page-list", { resolveToRoot });
|
|
1633
2072
|
}
|
|
1634
2073
|
/**
|
|
1635
|
-
*
|
|
2074
|
+
* Name the entry a resolved URL addresses.
|
|
2075
|
+
*
|
|
2076
|
+
* A correctly authored publication percent-encodes its hrefs, so the
|
|
2077
|
+
* decoded reading is the one the spec calls for and the one we prefer.
|
|
2078
|
+
* Some publications instead write the entry name verbatim, so when the two
|
|
2079
|
+
* readings differ we ask the archive which one it holds. A verbatim name
|
|
2080
|
+
* carrying a bare `%`, as `100%.xhtml` does, is not a valid encoding at all
|
|
2081
|
+
* and can only be its own answer.
|
|
1636
2082
|
*/
|
|
1637
|
-
|
|
1638
|
-
const
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
2083
|
+
async entryForUrl(url) {
|
|
2084
|
+
const asWritten = url.pathname.slice(1);
|
|
2085
|
+
let decoded;
|
|
2086
|
+
try {
|
|
2087
|
+
decoded = decodeURIComponent(asWritten);
|
|
2088
|
+
} catch {
|
|
2089
|
+
return asWritten;
|
|
2090
|
+
}
|
|
2091
|
+
if (decoded === asWritten) {
|
|
2092
|
+
return decoded;
|
|
2093
|
+
}
|
|
2094
|
+
if (await this.adapter.exists(decoded)) {
|
|
2095
|
+
return decoded;
|
|
2096
|
+
}
|
|
2097
|
+
return await this.adapter.exists(asWritten) ? asWritten : decoded;
|
|
2098
|
+
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Resolve an href to the name of the archive entry it addresses.
|
|
2101
|
+
*
|
|
2102
|
+
* @param from The entry the href appears in; it resolves against that
|
|
2103
|
+
* entry's directory
|
|
2104
|
+
* @throws when the href addresses something outside the publication
|
|
2105
|
+
*/
|
|
2106
|
+
async resolveEntry(from, href) {
|
|
2107
|
+
const url = resolveEntryUrl(from, href);
|
|
2108
|
+
if (!url) {
|
|
2109
|
+
throw new Error(
|
|
2110
|
+
`href does not address an entry in this publication: ${href}`
|
|
2111
|
+
);
|
|
2112
|
+
}
|
|
2113
|
+
return this.entryForUrl(url);
|
|
1644
2114
|
}
|
|
1645
2115
|
/**
|
|
1646
2116
|
* Returns a path-relative-scheme-less URL, relative to the
|
|
1647
2117
|
* container root.
|
|
1648
2118
|
*
|
|
2119
|
+
* An href that points outside the publication, such as an external link in
|
|
2120
|
+
* a navigation document, is handed back unchanged.
|
|
2121
|
+
*
|
|
1649
2122
|
* @param href The href to resolve
|
|
1650
2123
|
* @param [relativeTo] Optional - The href to resolve this href relative to.
|
|
1651
2124
|
Use if resolving a relative href from a file other than the package document.
|
|
1652
2125
|
*/
|
|
1653
2126
|
async resolveHref(href, relativeTo, { toRoot } = {}) {
|
|
2127
|
+
if (href.startsWith("#")) return href;
|
|
1654
2128
|
const rootfile = await this.getRootfile();
|
|
1655
|
-
const from = relativeTo ? this.
|
|
1656
|
-
const
|
|
1657
|
-
|
|
2129
|
+
const from = relativeTo ? await this.resolveEntry(rootfile, relativeTo) : rootfile;
|
|
2130
|
+
const url = resolveEntryUrl(from, href);
|
|
2131
|
+
if (!url) return href;
|
|
2132
|
+
const entry = (await this.entryForUrl(url)).split("/");
|
|
2133
|
+
const base = toRoot ? [] : rootfile.split("/").slice(0, -1);
|
|
2134
|
+
let shared = 0;
|
|
2135
|
+
while (shared < base.length && base[shared] === entry[shared]) {
|
|
2136
|
+
shared++;
|
|
2137
|
+
}
|
|
2138
|
+
const relative = [
|
|
2139
|
+
...Array(base.length - shared).fill(".."),
|
|
2140
|
+
...entry.slice(shared).map(encodeURIComponent)
|
|
2141
|
+
];
|
|
2142
|
+
return relative.join("/") + url.hash;
|
|
1658
2143
|
}
|
|
1659
2144
|
async readFileContents(href, relativeTo, encoding) {
|
|
1660
2145
|
const rootfile = await this.getRootfile();
|
|
1661
|
-
const from = relativeTo ? this.
|
|
1662
|
-
const
|
|
1663
|
-
const itemEntry = encoding ? await this.
|
|
2146
|
+
const from = relativeTo ? await this.resolveEntry(rootfile, relativeTo) : rootfile;
|
|
2147
|
+
const entry = await this.resolveEntry(from, href);
|
|
2148
|
+
const itemEntry = encoding ? await this.adapter.read(entry, encoding) : await this.adapter.read(entry);
|
|
1664
2149
|
return itemEntry;
|
|
1665
2150
|
}
|
|
1666
2151
|
async readItemContents(id, encoding) {
|
|
@@ -1669,8 +2154,8 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1669
2154
|
const manifestItem = manifest[id];
|
|
1670
2155
|
if (!manifestItem)
|
|
1671
2156
|
throw new Error(`Could not find item with id "${id}" in manifest`);
|
|
1672
|
-
const
|
|
1673
|
-
const itemEntry = encoding ? await this.
|
|
2157
|
+
const entry = await this.resolveEntry(rootfile, manifestItem.href);
|
|
2158
|
+
const itemEntry = encoding ? await this.adapter.read(entry, encoding) : await this.adapter.read(entry);
|
|
1674
2159
|
return itemEntry;
|
|
1675
2160
|
}
|
|
1676
2161
|
/**
|
|
@@ -1728,11 +2213,11 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1728
2213
|
if (!manifestItem)
|
|
1729
2214
|
throw new Error(`Could not find item with id "${id}" in manifest`);
|
|
1730
2215
|
memoize.clear(this.readXhtmlItemContents);
|
|
1731
|
-
const
|
|
2216
|
+
const entry = await this.resolveEntry(rootfile, manifestItem.href);
|
|
1732
2217
|
if (encoding === "utf-8") {
|
|
1733
|
-
await this.writeEntryContents(
|
|
2218
|
+
await this.writeEntryContents(entry, contents, encoding);
|
|
1734
2219
|
} else {
|
|
1735
|
-
await this.writeEntryContents(
|
|
2220
|
+
await this.writeEntryContents(entry, contents);
|
|
1736
2221
|
}
|
|
1737
2222
|
}
|
|
1738
2223
|
/**
|
|
@@ -1803,7 +2288,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1803
2288
|
});
|
|
1804
2289
|
this.manifest = null;
|
|
1805
2290
|
const rootfile = await this.getRootfile();
|
|
1806
|
-
const filename = this.
|
|
2291
|
+
const filename = await this.resolveEntry(rootfile, item.href);
|
|
1807
2292
|
const data = encoding === "utf-8" || encoding === "xml" ? new TextEncoder().encode(
|
|
1808
2293
|
encoding === "utf-8" ? contents : await Epub.xmlBuilder.build(
|
|
1809
2294
|
contents
|
|
@@ -1987,10 +2472,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1987
2472
|
);
|
|
1988
2473
|
const spineTocId = (_a = spine == null ? void 0 : spine[":@"]) == null ? void 0 : _a["@_toc"];
|
|
1989
2474
|
const ncxItem = spineTocId ? manifest[spineTocId] : Object.values(manifest).find(
|
|
1990
|
-
(item) =>
|
|
1991
|
-
var _a2;
|
|
1992
|
-
return ((_a2 = item.mediaType) == null ? void 0 : _a2.toLowerCase()) === "application/x-dtbncx+xml";
|
|
1993
|
-
}
|
|
2475
|
+
(item) => MediaType.fromMime(item.mediaType ?? "") === MediaType.NCX
|
|
1994
2476
|
);
|
|
1995
2477
|
if (!ncxItem) return [];
|
|
1996
2478
|
const ncxContent = await this.readItemContents(ncxItem.id, "utf-8");
|
|
@@ -2131,7 +2613,12 @@ class EpubFactory {
|
|
|
2131
2613
|
"This is not a valid EPUB publication. Could not read the package document."
|
|
2132
2614
|
);
|
|
2133
2615
|
}
|
|
2134
|
-
|
|
2616
|
+
try {
|
|
2617
|
+
await Epub.assertEpub3(epub);
|
|
2618
|
+
} catch (error) {
|
|
2619
|
+
epub.discardAndClose();
|
|
2620
|
+
throw error;
|
|
2621
|
+
}
|
|
2135
2622
|
return epub;
|
|
2136
2623
|
}
|
|
2137
2624
|
/**
|
|
@@ -2171,14 +2658,11 @@ class EpubFactory {
|
|
|
2171
2658
|
const container = encoder.encode(`<?xml version="1.0"?>
|
|
2172
2659
|
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
|
2173
2660
|
<rootfiles>
|
|
2174
|
-
<rootfile media-type="
|
|
2661
|
+
<rootfile media-type="${MediaType.OPF.mime}" full-path="OEBPS/content.opf"/>
|
|
2175
2662
|
</rootfiles>
|
|
2176
2663
|
</container>
|
|
2177
2664
|
`);
|
|
2178
|
-
await adapter.write(
|
|
2179
|
-
join(adapter.rootPath, "META-INF", "container.xml"),
|
|
2180
|
-
container
|
|
2181
|
-
);
|
|
2665
|
+
await adapter.write("META-INF/container.xml", container);
|
|
2182
2666
|
const packageDocument = encoder.encode(`<?xml version="1.0"?>
|
|
2183
2667
|
<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/">
|
|
2184
2668
|
<metadata>
|
|
@@ -2189,10 +2673,7 @@ class EpubFactory {
|
|
|
2189
2673
|
</spine>
|
|
2190
2674
|
</package>
|
|
2191
2675
|
`);
|
|
2192
|
-
await adapter.write(
|
|
2193
|
-
join(adapter.rootPath, "OEBPS", "content.opf"),
|
|
2194
|
-
packageDocument
|
|
2195
|
-
);
|
|
2676
|
+
await adapter.write("OEBPS/content.opf", packageDocument);
|
|
2196
2677
|
const epub = new Epub(this.adapterClass, adapter, path);
|
|
2197
2678
|
const metadata = [
|
|
2198
2679
|
{
|
|
@@ -2241,7 +2722,6 @@ class EpubFactory {
|
|
|
2241
2722
|
* @throws when the adapter is read-only
|
|
2242
2723
|
*/
|
|
2243
2724
|
async upgrade(path, options = {}) {
|
|
2244
|
-
var _a;
|
|
2245
2725
|
if (!this.adapterClass.capabilities.writable) {
|
|
2246
2726
|
throw new EpubReadOnlyError(
|
|
2247
2727
|
`adapter ${this.adapterClass.kind} is read-only; cannot upgrade`
|
|
@@ -2267,49 +2747,55 @@ class EpubFactory {
|
|
|
2267
2747
|
"This is not a valid EPUB publication. Could not read the package document."
|
|
2268
2748
|
);
|
|
2269
2749
|
}
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2750
|
+
try {
|
|
2751
|
+
const version = await epub.getVersion();
|
|
2752
|
+
if (version.startsWith("3.")) {
|
|
2753
|
+
return epub;
|
|
2754
|
+
}
|
|
2755
|
+
const tocEntries = await epub.getNcxTableOfContents();
|
|
2756
|
+
let landmarks = [];
|
|
2757
|
+
await epub.withPackage((pkg) => {
|
|
2758
|
+
landmarks = Upgrade.extractGuideLandmarks(pkg);
|
|
2759
|
+
Upgrade.upgradePackageMetadata(pkg);
|
|
2760
|
+
Upgrade.fixFontMimeTypes(pkg);
|
|
2761
|
+
Upgrade.removeGuide(pkg);
|
|
2762
|
+
if (removeNcx) {
|
|
2763
|
+
Upgrade.removeSpineTocRef(pkg);
|
|
2764
|
+
}
|
|
2765
|
+
Upgrade.setPackageVersion(pkg, "3.0");
|
|
2766
|
+
});
|
|
2767
|
+
await Upgrade.collectManifestProperties(epub);
|
|
2281
2768
|
if (removeNcx) {
|
|
2282
|
-
Upgrade.
|
|
2769
|
+
await Upgrade.removeNcx(epub);
|
|
2283
2770
|
}
|
|
2284
|
-
Upgrade.
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2771
|
+
const navHref = await Upgrade.chooseNavHref(epub);
|
|
2772
|
+
const navContent = await Upgrade.buildNavDocument(
|
|
2773
|
+
epub,
|
|
2774
|
+
tocEntries,
|
|
2775
|
+
landmarks
|
|
2776
|
+
);
|
|
2777
|
+
await epub.addManifestItem(
|
|
2778
|
+
{
|
|
2779
|
+
id: "nav",
|
|
2780
|
+
href: navHref,
|
|
2781
|
+
mediaType: MediaType.XHTML.mime,
|
|
2782
|
+
properties: ["nav"]
|
|
2783
|
+
},
|
|
2784
|
+
navContent,
|
|
2785
|
+
"utf-8"
|
|
2786
|
+
);
|
|
2787
|
+
const manifest = await epub.getManifest();
|
|
2788
|
+
for (const item of Object.values(manifest)) {
|
|
2789
|
+
if (MediaType.fromMime(item.mediaType ?? "") !== MediaType.XHTML)
|
|
2790
|
+
continue;
|
|
2791
|
+
const contents = await epub.readXhtmlItemContents(item.id);
|
|
2792
|
+
await epub.writeXhtmlItemContents(item.id, contents);
|
|
2793
|
+
}
|
|
2794
|
+
return epub;
|
|
2795
|
+
} catch (error) {
|
|
2796
|
+
epub.discardAndClose();
|
|
2797
|
+
throw error;
|
|
2311
2798
|
}
|
|
2312
|
-
return epub;
|
|
2313
2799
|
}
|
|
2314
2800
|
}
|
|
2315
2801
|
export {
|