@storyteller-platform/epub 0.6.3 → 0.7.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.
@@ -97,7 +97,8 @@ const AUDIO_FILE_EXTENSIONS = [
97
97
  ...WEBM_FILE_EXTENSIONS
98
98
  ];
99
99
  function isAudioFile(filenameOrExt) {
100
- return AUDIO_FILE_EXTENSIONS.some((ext) => filenameOrExt.endsWith(ext));
100
+ const lowered = filenameOrExt.toLowerCase();
101
+ return AUDIO_FILE_EXTENSIONS.some((ext) => lowered.endsWith(ext));
101
102
  }
102
103
  function mintRootPath() {
103
104
  return (0, import_path.join)((0, import_node_os.tmpdir)(), `storyteller-platform-epub-${(0, import_node_crypto.randomUUID)()}`);
@@ -41,7 +41,8 @@ const AUDIO_FILE_EXTENSIONS = [
41
41
  ...WEBM_FILE_EXTENSIONS
42
42
  ];
43
43
  function isAudioFile(filenameOrExt) {
44
- return AUDIO_FILE_EXTENSIONS.some((ext) => filenameOrExt.endsWith(ext));
44
+ const lowered = filenameOrExt.toLowerCase();
45
+ return AUDIO_FILE_EXTENSIONS.some((ext) => lowered.endsWith(ext));
45
46
  }
46
47
  function mintRootPath() {
47
48
  return join(tmpdir(), `storyteller-platform-epub-${randomUUID()}`);
package/dist/index.cjs CHANGED
@@ -582,12 +582,15 @@ ${JSON.stringify(element, null, 2)}`
582
582
  return metadata;
583
583
  }
584
584
  /**
585
- * Retrieve the identifier from the dc:identifier element
585
+ * Retrieve the first identifier from the dc:identifier element
586
586
  * in the EPUB metadata.
587
587
  *
588
588
  * If there is no dc:identifier element, returns null.
589
589
  *
590
590
  * @link https://www.w3.org/TR/epub-33/#sec-opf-dcidentifier
591
+ *
592
+ * @deprecated Use {@link getUniqueIdentifier} instead to get the unique identifier,
593
+ * or {@link getIdentifiers} to get all identifiers.
591
594
  */
592
595
  async getIdentifier() {
593
596
  const metadata = await this.getMetadata();
@@ -601,6 +604,8 @@ ${JSON.stringify(element, null, 2)}`
601
604
  * Otherwise creates a new element
602
605
  *
603
606
  * @link https://www.w3.org/TR/epub-33/#sec-opf-dcidentifier
607
+ *
608
+ * @deprecated Use {@link setUniqueIdentifier} instead.
604
609
  */
605
610
  async setIdentifier(identifier) {
606
611
  await this.replaceMetadata(({ type }) => type === "dc:identifier", {
@@ -609,6 +614,395 @@ ${JSON.stringify(element, null, 2)}`
609
614
  value: identifier
610
615
  });
611
616
  }
617
+ /**
618
+ * Retrieve the identifier with the unique identifier id
619
+ * in the EPUB metadata.
620
+ *
621
+ * If there is no unique identifier id, returns the first dc:identifier element.
622
+ *
623
+ * @link https://www.w3.org/TR/epub-33/#sec-opf-dcidentifier
624
+ */
625
+ async getUniqueIdentifier() {
626
+ const metadata = await this.getMetadata();
627
+ const uniqueId = await this.getUniqueIdentifierId();
628
+ if (!uniqueId) {
629
+ return null;
630
+ }
631
+ const entry = metadata.find(
632
+ ({ type, id }) => type === "dc:identifier" && id === uniqueId
633
+ );
634
+ return (entry == null ? void 0 : entry.value) ?? null;
635
+ }
636
+ /**
637
+ * Set the unique identifier id for the EPUB.
638
+ *
639
+ * Updates the existing dc:identifier element referenced by the unique identifier id if one exists.
640
+ * Otherwise creates a new element with the provided identifier, and sets the unique identifier id to the new element's id.
641
+ *
642
+ * Note: you likely shouldn't change the unique identifier id unless you are producing a new EPUB.
643
+ *
644
+ * @link https://www.w3.org/TR/epub-33/#sec-opf-dcidentifier
645
+ */
646
+ async setUniqueIdentifier(identifier) {
647
+ await this.withPackage(async (packageElement) => {
648
+ const metadata = Epub.findXmlChildByName(
649
+ "metadata",
650
+ Epub.getXmlChildren(packageElement)
651
+ );
652
+ if (!metadata) {
653
+ throw new Error(
654
+ "Failed to parse EPUB: found no metadata element in package document"
655
+ );
656
+ }
657
+ let uniqueId = await this.getUniqueIdentifierId();
658
+ if (!uniqueId) {
659
+ const newUniqueId = (0, import_nanoid.nanoid)();
660
+ packageElement[":@"] = {
661
+ ...packageElement[":@"],
662
+ "@_unique-identifier": newUniqueId
663
+ };
664
+ uniqueId = newUniqueId;
665
+ }
666
+ const children = Epub.getXmlChildren(metadata);
667
+ const entry = Epub.findXmlChildByName(
668
+ "dc:identifier",
669
+ children,
670
+ (node) => {
671
+ var _a;
672
+ return ((_a = node[":@"]) == null ? void 0 : _a["@_id"]) === uniqueId;
673
+ }
674
+ );
675
+ if (entry) {
676
+ children.splice(
677
+ children.indexOf(entry),
678
+ 1,
679
+ Epub.createXmlElement("dc:identifier", { id: uniqueId }, [
680
+ Epub.createXmlTextNode(identifier)
681
+ ])
682
+ );
683
+ return;
684
+ }
685
+ children.push(
686
+ Epub.createXmlElement("dc:identifier", { id: uniqueId }, [
687
+ Epub.createXmlTextNode(identifier)
688
+ ])
689
+ );
690
+ });
691
+ }
692
+ /**
693
+ * Retrieve the id of the publication's unique identifier, as declared by the
694
+ * package element's `unique-identifier` attribute.
695
+ *
696
+ * @link https://www.w3.org/TR/epub-33/#sec-opf-dcidentifier
697
+ */
698
+ async getUniqueIdentifierId() {
699
+ var _a;
700
+ const packageElement = await this.getPackageElement();
701
+ return ((_a = packageElement[":@"]) == null ? void 0 : _a["@_unique-identifier"]) ?? null;
702
+ }
703
+ /**
704
+ * Collect `dc:identifier` or `dc:source` entries, attaching the value and
705
+ * scheme of any refining `identifier-type` meta (spec D.3.8) and a legacy
706
+ * `opf:scheme` attribute. Values are not interpreted.
707
+ *
708
+ * `onRefinement` is invoked for every other meta refining a collected entry,
709
+ * so callers can surface element-specific refinements (e.g. `source-of` on a
710
+ * `dc:source`).
711
+ */
712
+ static collectDcEntries(metadata, type, onRefinement) {
713
+ const entries = metadata.filter((entry) => entry.type === type && entry.value !== void 0).map((entry) => ({
714
+ // filtered above, so value is defined
715
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
716
+ value: entry.value,
717
+ ...entry.id && { id: entry.id },
718
+ ...entry.properties["opf:scheme"] && {
719
+ scheme: entry.properties["opf:scheme"]
720
+ }
721
+ }));
722
+ for (const meta of metadata) {
723
+ if (meta.type !== "meta" || meta.value === void 0) continue;
724
+ const property = meta.properties["property"];
725
+ const refines = meta.properties["refines"];
726
+ if (!property || !refines) continue;
727
+ const target = entries.find((t) => t.id === refines.slice(1));
728
+ if (!target) continue;
729
+ if (property === "identifier-type") {
730
+ target.identifierType = meta.value;
731
+ if (meta.properties["scheme"]) target.scheme = meta.properties["scheme"];
732
+ } else {
733
+ onRefinement == null ? void 0 : onRefinement(target, property, meta.value);
734
+ }
735
+ }
736
+ return entries;
737
+ }
738
+ /**
739
+ * Retrieve every `dc:identifier` entry, returned as found.
740
+ *
741
+ * Values are not interpreted. Any refining `identifier-type` meta (spec
742
+ * D.3.8) or legacy `opf:scheme` attribute is surfaced on the entry, but no
743
+ * parsing of the value itself is attempted. To read `dc:source` entries, use
744
+ * {@link getSources}.
745
+ *
746
+ * @link https://www.w3.org/TR/epub-33/#sec-opf-dcidentifier
747
+ */
748
+ async getIdentifiers() {
749
+ const metadata = await this.getMetadata();
750
+ return Epub.collectDcEntries(metadata, "dc:identifier");
751
+ }
752
+ /**
753
+ * Retrieve every `dc:source` entry, returned as found.
754
+ *
755
+ * Like {@link getIdentifiers}, values are not interpreted. In addition to a
756
+ * refining `identifier-type`, a refining `source-of` meta (spec D.3.11) is
757
+ * surfaced as `sourceOf`.
758
+ *
759
+ * @link https://www.w3.org/TR/epub-33/#sec-opf-dcsource
760
+ */
761
+ async getSources() {
762
+ const metadata = await this.getMetadata();
763
+ return Epub.collectDcEntries(
764
+ metadata,
765
+ "dc:source",
766
+ (source, property) => {
767
+ if (property === "source-of") {
768
+ source.isPageBreakSource = true;
769
+ }
770
+ }
771
+ );
772
+ }
773
+ /**
774
+ * Retrieve the `pageBreakSource` property (EPUB 3.4, spec D.2.9), the
775
+ * publication-level source for the source of its page break markers.
776
+ *
777
+ * This property replaces the refining `source-of="pagination"` meta (spec
778
+ * D.3.11), see {@link EpubSource.sourceOf}. If no `pageBreakSource` property is found,
779
+ * we fall back to finding a `dc:source` with a `source-of="pagination"` refinement.
780
+ *
781
+ * @link https://www.w3.org/TR/epub/#pageBreakSource
782
+ */
783
+ async getPageBreakSource() {
784
+ const entry = await this.findMetadataItem(
785
+ (item) => item.type === "meta" && item.properties["property"] === "pageBreakSource" && !!item.value
786
+ );
787
+ if (entry) {
788
+ return {
789
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
790
+ value: entry.value,
791
+ id: entry.id,
792
+ identifierType: void 0,
793
+ scheme: void 0,
794
+ isPageBreakSource: true
795
+ };
796
+ }
797
+ const sources = await this.getSources();
798
+ const source = sources.find((source2) => source2.isPageBreakSource);
799
+ if (source) {
800
+ return source;
801
+ }
802
+ return null;
803
+ }
804
+ /**
805
+ * Set the `pageBreakSource` property (EPUB 3.4, spec D.2.9), or remove it when
806
+ * passed null. Replaces an existing `pageBreakSource` meta if present.
807
+ *
808
+ * Pass `none` to indicate the pagination is unique to this publication.
809
+ *
810
+ * @link https://www.w3.org/TR/epub/#pageBreakSource
811
+ */
812
+ async setPageBreakSource(value) {
813
+ if (value === null) {
814
+ await this.removeMetadata(
815
+ (item) => item.properties["property"] === "pageBreakSource"
816
+ );
817
+ return;
818
+ }
819
+ await this.replaceMetadata(
820
+ (item) => item.properties["property"] === "pageBreakSource",
821
+ { type: "meta", properties: { property: "pageBreakSource" }, value }
822
+ );
823
+ }
824
+ /**
825
+ * Replace the publication's `dc:identifier` entries.
826
+ *
827
+ * This replaces ALL existing `dc:identifier` elements except the publication's
828
+ * unique identifier (the one referenced by the package element's
829
+ * `unique-identifier` attribute), which is always preserved and must not be
830
+ * included in the provided list. If included anyway, it is ignored. See
831
+ * {@link setUniqueIdentifier} to change it.
832
+ *
833
+ * Identifiers are placed in the order they are provided.
834
+ *
835
+ * `dc:source` entries are not touched, use {@link setSources} for those.
836
+ *
837
+ * When an entry has an `identifierType`, it is written in the refining form
838
+ * (a `meta` with `property="identifier-type"`, carrying the `scheme`
839
+ * attribute when provided). An entry with only a `scheme` is written using
840
+ * the legacy `opf:scheme` attribute.
841
+ *
842
+ * @link https://www.w3.org/TR/epub-33/#sec-opf-dcidentifier
843
+ */
844
+ async setIdentifiers(identifiers) {
845
+ const uniqueId = await this.getUniqueIdentifierId();
846
+ await this.withPackage((packageElement) => {
847
+ var _a;
848
+ const metadata = Epub.findXmlChildByName(
849
+ "metadata",
850
+ Epub.getXmlChildren(packageElement)
851
+ );
852
+ if (!metadata)
853
+ throw new Error(
854
+ "Failed to parse EPUB: found no metadata element in package document"
855
+ );
856
+ const children = Epub.getXmlChildren(metadata);
857
+ const removedIds = /* @__PURE__ */ new Set();
858
+ for (let i = children.length - 1; i >= 0; i--) {
859
+ const node = children[i];
860
+ if (Epub.isXmlTextNode(node) || Epub.getXmlElementName(node) !== "dc:identifier") {
861
+ continue;
862
+ }
863
+ const id = (_a = node[":@"]) == null ? void 0 : _a["@_id"];
864
+ if (id && id === uniqueId) {
865
+ continue;
866
+ }
867
+ if (id) {
868
+ removedIds.add(id);
869
+ }
870
+ children.splice(i, 1);
871
+ }
872
+ Epub.removeRefiningMetas(children, removedIds, ["identifier-type"]);
873
+ for (const identifier of identifiers) {
874
+ if (identifier.id && identifier.id === uniqueId) continue;
875
+ const id = identifier.id ?? (identifier.identifierType !== void 0 ? (0, import_nanoid.nanoid)() : void 0);
876
+ children.push(
877
+ Epub.createXmlElement(
878
+ "dc:identifier",
879
+ {
880
+ ...id && { id },
881
+ ...identifier.scheme && identifier.identifierType === void 0 && {
882
+ "opf:scheme": identifier.scheme
883
+ }
884
+ },
885
+ [Epub.createXmlTextNode(identifier.value)]
886
+ )
887
+ );
888
+ if (identifier.identifierType !== void 0 && id) {
889
+ children.push(
890
+ Epub.createXmlElement(
891
+ "meta",
892
+ {
893
+ refines: `#${id}`,
894
+ property: "identifier-type",
895
+ ...identifier.scheme && { scheme: identifier.scheme }
896
+ },
897
+ [Epub.createXmlTextNode(identifier.identifierType)]
898
+ )
899
+ );
900
+ }
901
+ }
902
+ });
903
+ }
904
+ /**
905
+ * Remove `meta` refinements pointing at any of the given ids,
906
+ * restricted to the given `property` values as a cleanup step
907
+ * Mutates `children` in place.
908
+ */
909
+ static removeRefiningMetas(children, ids, properties) {
910
+ var _a, _b, _c;
911
+ for (let i = children.length - 1; i >= 0; i--) {
912
+ const node = children[i];
913
+ if (Epub.isXmlTextNode(node) || Epub.getXmlElementName(node) !== "meta") {
914
+ continue;
915
+ }
916
+ const property = (_a = node[":@"]) == null ? void 0 : _a["@_property"];
917
+ if (!property || !properties.includes(property)) continue;
918
+ const refines = (_c = (_b = node[":@"]) == null ? void 0 : _b["@_refines"]) == null ? void 0 : _c.slice(1);
919
+ if (refines && ids.has(refines)) {
920
+ children.splice(i, 1);
921
+ }
922
+ }
923
+ }
924
+ /**
925
+ * Replace the publication's `dc:source` entries.
926
+ *
927
+ * This replaces ALL existing `dc:source` elements (and their refining
928
+ * `identifier-type` / `source-of` metas). `dc:identifier` entries are not
929
+ * touched; use {@link setIdentifiers} for those. Pass an empty array to
930
+ * remove all sources.
931
+ *
932
+ * When an entry has an `identifierType`, it is written in the refining form.
933
+ * A `sourceOf` value is written as a refining `source-of` meta (spec D.3.11).
934
+ * An entry with only a `scheme` uses the legacy `opf:scheme` attribute.
935
+ *
936
+ * @link https://www.w3.org/TR/epub-33/#sec-opf-dcsource
937
+ */
938
+ async setSources(sources) {
939
+ await this.withPackage((packageElement) => {
940
+ var _a;
941
+ const metadata = Epub.findXmlChildByName(
942
+ "metadata",
943
+ Epub.getXmlChildren(packageElement)
944
+ );
945
+ if (!metadata)
946
+ throw new Error(
947
+ "Failed to parse EPUB: found no metadata element in package document"
948
+ );
949
+ const children = Epub.getXmlChildren(metadata);
950
+ const removedIds = /* @__PURE__ */ new Set();
951
+ for (let i = children.length - 1; i >= 0; i--) {
952
+ const node = children[i];
953
+ if (Epub.isXmlTextNode(node) || Epub.getXmlElementName(node) !== "dc:source") {
954
+ continue;
955
+ }
956
+ const id = (_a = node[":@"]) == null ? void 0 : _a["@_id"];
957
+ if (id) {
958
+ removedIds.add(id);
959
+ }
960
+ children.splice(i, 1);
961
+ }
962
+ Epub.removeRefiningMetas(children, removedIds, [
963
+ "identifier-type",
964
+ "source-of"
965
+ ]);
966
+ for (const source of sources) {
967
+ const needsId = source.identifierType !== void 0 || source.isPageBreakSource;
968
+ const id = source.id ?? (needsId ? (0, import_nanoid.nanoid)() : void 0);
969
+ children.push(
970
+ Epub.createXmlElement(
971
+ "dc:source",
972
+ {
973
+ ...id && { id },
974
+ ...source.scheme && source.identifierType === void 0 && {
975
+ "opf:scheme": source.scheme
976
+ }
977
+ },
978
+ [Epub.createXmlTextNode(source.value)]
979
+ )
980
+ );
981
+ if (source.identifierType !== void 0 && id) {
982
+ children.push(
983
+ Epub.createXmlElement(
984
+ "meta",
985
+ {
986
+ refines: `#${id}`,
987
+ property: "identifier-type",
988
+ ...source.scheme && { scheme: source.scheme }
989
+ },
990
+ [Epub.createXmlTextNode(source.identifierType)]
991
+ )
992
+ );
993
+ }
994
+ if (source.isPageBreakSource && id) {
995
+ children.push(
996
+ Epub.createXmlElement(
997
+ "meta",
998
+ { refines: `#${id}`, property: "source-of" },
999
+ [Epub.createXmlTextNode("pagination")]
1000
+ )
1001
+ );
1002
+ }
1003
+ }
1004
+ });
1005
+ }
612
1006
  /**
613
1007
  * Even "EPUB 3" publications sometimes still only use the
614
1008
  * EPUB 2 specification for identifying the cover image.
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import 'fast-xml-parser';
2
2
  export { AdapterOptions, EpubListEntry, EpubStorageAdapter, EpubStorageAdapterClass, EpubStorageCapabilities, EpubStorageKind } from './adapters/interface.cjs';
3
- export { A as AlternateScript, C as Collection, e as DcCreator, D as DcSubject, f as DublinCore, E as ElementName, m as Epub, Epub2UpgradeOptions, o as EpubFactory, n as EpubInstanceFor, d as EpubMetadata, l as EpubReadOnlyError, j as EpubReader, k as EpubVersionError, F as FromOptions, I as InMemoryEpubReader, Landmark, M as ManifestItem, c as MetadataEntry, h as Navigation, N as NavigationItem, g as NavigationList, i as PackageElement, P as ParsedXml, X as XmlElement, b as XmlNode, a as XmlTextNode } from './upgrade.cjs';
3
+ export { A as AlternateScript, C as Collection, g as DcCreator, D as DcSubject, h as DublinCore, E as ElementName, o as Epub, Epub2UpgradeOptions, q as EpubFactory, e as EpubIdentifier, p as EpubInstanceFor, d as EpubMetadata, n as EpubReadOnlyError, l as EpubReader, f as EpubSource, m as EpubVersionError, F as FromOptions, I as InMemoryEpubReader, Landmark, M as ManifestItem, c as MetadataEntry, j as Navigation, N as NavigationItem, i as NavigationList, k as PackageElement, P as ParsedXml, X as XmlElement, b as XmlNode, a as XmlTextNode } from './upgrade.cjs';
4
4
  export { MemoryAdapter, MemoryAdapterOptions } from './adapters/memory.cjs';
5
5
  export { TmpFsAdapter } from './adapters/tmpfs.cjs';
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import 'fast-xml-parser';
2
2
  export { AdapterOptions, EpubListEntry, EpubStorageAdapter, EpubStorageAdapterClass, EpubStorageCapabilities, EpubStorageKind } from './adapters/interface.js';
3
- export { A as AlternateScript, C as Collection, e as DcCreator, D as DcSubject, f as DublinCore, E as ElementName, m as Epub, Epub2UpgradeOptions, o as EpubFactory, n as EpubInstanceFor, d as EpubMetadata, l as EpubReadOnlyError, j as EpubReader, k as EpubVersionError, F as FromOptions, I as InMemoryEpubReader, Landmark, M as ManifestItem, c as MetadataEntry, h as Navigation, N as NavigationItem, g as NavigationList, i as PackageElement, P as ParsedXml, X as XmlElement, b as XmlNode, a as XmlTextNode } from './upgrade.js';
3
+ export { A as AlternateScript, C as Collection, g as DcCreator, D as DcSubject, h as DublinCore, E as ElementName, o as Epub, Epub2UpgradeOptions, q as EpubFactory, e as EpubIdentifier, p as EpubInstanceFor, d as EpubMetadata, n as EpubReadOnlyError, l as EpubReader, f as EpubSource, m as EpubVersionError, F as FromOptions, I as InMemoryEpubReader, Landmark, M as ManifestItem, c as MetadataEntry, j as Navigation, N as NavigationItem, i as NavigationList, k as PackageElement, P as ParsedXml, X as XmlElement, b as XmlNode, a as XmlTextNode } from './upgrade.js';
4
4
  export { MemoryAdapter, MemoryAdapterOptions } from './adapters/memory.js';
5
5
  export { TmpFsAdapter } from './adapters/tmpfs.js';