@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/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
- dirname,
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
- const filename = this.resolveInternalHref(rootfile, href);
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
- const path = this.resolveInternalHref(rootfile, manifestItem.href);
323
- return this.adapter.archiveLength(path);
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.getFileData(
329
- join(this.adapter.rootPath, "META-INF", "container.xml"),
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"]) === "application/oebps-package+xml";
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 = resolve(this.adapter.rootPath, fullPath);
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.getFileData(rootfile, "utf-8");
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 = lookup(href);
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
- * Returns a Zip Entry path for an HREF
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
- resolveInternalHref(from, href) {
1638
- const startPath = dirname(from);
1639
- return resolve(
1640
- this.adapter.rootPath,
1641
- hrefToPlatformPath(startPath),
1642
- hrefToPlatformPath(href)
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.resolveInternalHref(rootfile, relativeTo) : rootfile;
1656
- const path = this.resolveInternalHref(from, href);
1657
- return path.replace(toRoot ? this.adapter.rootPath : dirname(rootfile), "").slice(1);
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.resolveInternalHref(rootfile, relativeTo) : rootfile;
1662
- const path = this.resolveInternalHref(from, href);
1663
- const itemEntry = encoding ? await this.getFileData(path, encoding) : await this.getFileData(path);
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 path = this.resolveInternalHref(rootfile, manifestItem.href);
1673
- const itemEntry = encoding ? await this.getFileData(path, encoding) : await this.getFileData(path);
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 href = this.resolveInternalHref(rootfile, manifestItem.href);
2216
+ const entry = await this.resolveEntry(rootfile, manifestItem.href);
1732
2217
  if (encoding === "utf-8") {
1733
- await this.writeEntryContents(href, contents, encoding);
2218
+ await this.writeEntryContents(entry, contents, encoding);
1734
2219
  } else {
1735
- await this.writeEntryContents(href, contents);
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.resolveInternalHref(rootfile, item.href);
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
- await Epub.assertEpub3(epub);
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="application/oebps-package+xml" full-path="OEBPS/content.opf"/>
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
- const version = await epub.getVersion();
2271
- if (version.startsWith("3.")) {
2272
- return epub;
2273
- }
2274
- const tocEntries = await epub.getNcxTableOfContents();
2275
- let landmarks = [];
2276
- await epub.withPackage((pkg) => {
2277
- landmarks = Upgrade.extractGuideLandmarks(pkg);
2278
- Upgrade.upgradePackageMetadata(pkg);
2279
- Upgrade.fixFontMimeTypes(pkg);
2280
- Upgrade.removeGuide(pkg);
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.removeSpineTocRef(pkg);
2769
+ await Upgrade.removeNcx(epub);
2283
2770
  }
2284
- Upgrade.setPackageVersion(pkg, "3.0");
2285
- });
2286
- await Upgrade.collectManifestProperties(epub);
2287
- if (removeNcx) {
2288
- await Upgrade.removeNcx(epub);
2289
- }
2290
- const navHref = await Upgrade.chooseNavHref(epub);
2291
- const navContent = await Upgrade.buildNavDocument(
2292
- epub,
2293
- tocEntries,
2294
- landmarks
2295
- );
2296
- await epub.addManifestItem(
2297
- {
2298
- id: "nav",
2299
- href: navHref,
2300
- mediaType: "application/xhtml+xml",
2301
- properties: ["nav"]
2302
- },
2303
- navContent,
2304
- "utf-8"
2305
- );
2306
- const manifest = await epub.getManifest();
2307
- for (const item of Object.values(manifest)) {
2308
- if (((_a = item.mediaType) == null ? void 0 : _a.toLowerCase()) !== "application/xhtml+xml") continue;
2309
- const contents = await epub.readXhtmlItemContents(item.id);
2310
- await epub.writeXhtmlItemContents(item.id, contents);
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 {