@storyteller-platform/epub 0.7.0 → 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/upgrade.d.ts CHANGED
@@ -339,10 +339,6 @@ declare class Epub {
339
339
  static assertEpub3(epub: Epub): Promise<void>;
340
340
  copy(path?: string): Promise<Epub>;
341
341
  private removeEntry;
342
- /**
343
- * Read raw bytes (or utf-8 text) from the underlying adapter
344
- */
345
- private getFileData;
346
342
  /**
347
343
  * Length of the underlying archive entry for a manifest item, in bytes
348
344
  * Necessary to compute the readium page count which is for COMPRESSED content
@@ -391,7 +387,7 @@ declare class Epub {
391
387
  */
392
388
  findMetadataItem(predicate: (entry: MetadataEntry) => boolean): Promise<{
393
389
  id: string | undefined;
394
- type: `?${string}` | `a${string}` | `b${string}` | `c${string}` | `d${string}` | `e${string}` | `f${string}` | `g${string}` | `h${string}` | `i${string}` | `j${string}` | `k${string}` | `l${string}` | `m${string}` | `n${string}` | `o${string}` | `p${string}` | `q${string}` | `r${string}` | `s${string}` | `t${string}` | `u${string}` | `v${string}` | `w${string}` | `x${string}` | `y${string}` | `z${string}` | `A${string}` | `B${string}` | `C${string}` | `D${string}` | `E${string}` | `F${string}` | `G${string}` | `H${string}` | `I${string}` | `J${string}` | `K${string}` | `L${string}` | `M${string}` | `N${string}` | `O${string}` | `P${string}` | `Q${string}` | `R${string}` | `S${string}` | `T${string}` | `U${string}` | `V${string}` | `W${string}` | `X${string}` | `Y${string}` | `Z${string}`;
390
+ type: `a${string}` | `b${string}` | `c${string}` | `d${string}` | `e${string}` | `f${string}` | `g${string}` | `h${string}` | `i${string}` | `j${string}` | `k${string}` | `l${string}` | `m${string}` | `n${string}` | `o${string}` | `p${string}` | `q${string}` | `r${string}` | `s${string}` | `t${string}` | `u${string}` | `v${string}` | `w${string}` | `x${string}` | `y${string}` | `z${string}` | `A${string}` | `B${string}` | `C${string}` | `D${string}` | `E${string}` | `F${string}` | `G${string}` | `H${string}` | `I${string}` | `J${string}` | `K${string}` | `L${string}` | `M${string}` | `N${string}` | `O${string}` | `P${string}` | `Q${string}` | `R${string}` | `S${string}` | `T${string}` | `U${string}` | `V${string}` | `W${string}` | `X${string}` | `Y${string}` | `Z${string}` | `?${string}`;
395
391
  properties: {
396
392
  [k: string]: string;
397
393
  };
@@ -403,7 +399,7 @@ declare class Epub {
403
399
  */
404
400
  findAllMetadataItems(predicate: (entry: MetadataEntry) => boolean): Promise<{
405
401
  id: string | undefined;
406
- type: `?${string}` | `a${string}` | `b${string}` | `c${string}` | `d${string}` | `e${string}` | `f${string}` | `g${string}` | `h${string}` | `i${string}` | `j${string}` | `k${string}` | `l${string}` | `m${string}` | `n${string}` | `o${string}` | `p${string}` | `q${string}` | `r${string}` | `s${string}` | `t${string}` | `u${string}` | `v${string}` | `w${string}` | `x${string}` | `y${string}` | `z${string}` | `A${string}` | `B${string}` | `C${string}` | `D${string}` | `E${string}` | `F${string}` | `G${string}` | `H${string}` | `I${string}` | `J${string}` | `K${string}` | `L${string}` | `M${string}` | `N${string}` | `O${string}` | `P${string}` | `Q${string}` | `R${string}` | `S${string}` | `T${string}` | `U${string}` | `V${string}` | `W${string}` | `X${string}` | `Y${string}` | `Z${string}`;
402
+ type: `a${string}` | `b${string}` | `c${string}` | `d${string}` | `e${string}` | `f${string}` | `g${string}` | `h${string}` | `i${string}` | `j${string}` | `k${string}` | `l${string}` | `m${string}` | `n${string}` | `o${string}` | `p${string}` | `q${string}` | `r${string}` | `s${string}` | `t${string}` | `u${string}` | `v${string}` | `w${string}` | `x${string}` | `y${string}` | `z${string}` | `A${string}` | `B${string}` | `C${string}` | `D${string}` | `E${string}` | `F${string}` | `G${string}` | `H${string}` | `I${string}` | `J${string}` | `K${string}` | `L${string}` | `M${string}` | `N${string}` | `O${string}` | `P${string}` | `Q${string}` | `R${string}` | `S${string}` | `T${string}` | `U${string}` | `V${string}` | `W${string}` | `X${string}` | `Y${string}` | `Z${string}` | `?${string}`;
407
403
  properties: {
408
404
  [k: string]: string;
409
405
  };
@@ -922,13 +918,31 @@ declare class Epub {
922
918
  resolveToRoot?: boolean;
923
919
  }): Promise<Navigation | null>;
924
920
  /**
925
- * Returns a Zip Entry path for an HREF
921
+ * Name the entry a resolved URL addresses.
922
+ *
923
+ * A correctly authored publication percent-encodes its hrefs, so the
924
+ * decoded reading is the one the spec calls for and the one we prefer.
925
+ * Some publications instead write the entry name verbatim, so when the two
926
+ * readings differ we ask the archive which one it holds. A verbatim name
927
+ * carrying a bare `%`, as `100%.xhtml` does, is not a valid encoding at all
928
+ * and can only be its own answer.
926
929
  */
927
- private resolveInternalHref;
930
+ private entryForUrl;
931
+ /**
932
+ * Resolve an href to the name of the archive entry it addresses.
933
+ *
934
+ * @param from The entry the href appears in; it resolves against that
935
+ * entry's directory
936
+ * @throws when the href addresses something outside the publication
937
+ */
938
+ private resolveEntry;
928
939
  /**
929
940
  * Returns a path-relative-scheme-less URL, relative to the
930
941
  * container root.
931
942
  *
943
+ * An href that points outside the publication, such as an external link in
944
+ * a navigation document, is handed back unchanged.
945
+ *
932
946
  * @param href The href to resolve
933
947
  * @param [relativeTo] Optional - The href to resolve this href relative to.
934
948
  Use if resolving a relative href from a file other than the package document.
package/dist/upgrade.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import "./chunk-BIEQXUOY.js";
2
2
  import { nanoid } from "nanoid";
3
+ import { MediaType } from "@storyteller-platform/media-types";
3
4
  import {
4
5
  Epub
5
6
  } from "./index.js";
@@ -49,31 +50,7 @@ const GUIDE_TO_EPUBTYPE = {
49
50
  "other.volume": "volume",
50
51
  "other.warning": "warning"
51
52
  };
52
- const XHTML_MEDIA_TYPES = /* @__PURE__ */ new Set([
53
- "application/xhtml+xml",
54
- "application/vnd.adobe-page-template+xml",
55
- "text/html"
56
- ]);
57
- const OEB_FONTS = /* @__PURE__ */ new Set([
58
- "application/vnd.ms-opentype",
59
- "application/x-font-ttf",
60
- "application/x-font-otf",
61
- "application/x-font-truetype",
62
- "application/font-sfnt",
63
- "application/font-woff",
64
- "application/font-woff2",
65
- "font/woff",
66
- "font/woff2",
67
- "font/otf",
68
- "font/ttf",
69
- "font/sfnt"
70
- ]);
71
- const FONT_MIME_BY_EXT = {
72
- ".ttf": "application/font-sfnt",
73
- ".otf": "application/font-sfnt",
74
- ".woff": "application/font-woff",
75
- ".woff2": "font/woff2"
76
- };
53
+ const XHTML_MEDIA_TYPES = /* @__PURE__ */ new Set([MediaType.XHTML, MediaType.HTML]);
77
54
  function getMetadataElement(pkg) {
78
55
  return Epub.findXmlChildByName("metadata", Epub.getXmlChildren(pkg));
79
56
  }
@@ -350,11 +327,10 @@ function removeSpineTocRef(pkg) {
350
327
  delete spine[":@"]["@_toc"];
351
328
  }
352
329
  async function collectManifestProperties(epub) {
353
- var _a;
354
330
  const manifest = await epub.getManifest();
355
331
  for (const item of Object.values(manifest)) {
356
- const mediaType = ((_a = item.mediaType) == null ? void 0 : _a.toLowerCase()) ?? "";
357
- if (!XHTML_MEDIA_TYPES.has(mediaType)) continue;
332
+ const mediaType = MediaType.fromMime(item.mediaType ?? "");
333
+ if (!mediaType || !XHTML_MEDIA_TYPES.has(mediaType)) continue;
358
334
  let xml;
359
335
  try {
360
336
  xml = await epub.readXhtmlItemContents(item.id);
@@ -375,21 +351,16 @@ async function collectManifestProperties(epub) {
375
351
  }
376
352
  }
377
353
  function fixFontMimeTypes(pkg) {
378
- var _a, _b;
354
+ var _a, _b, _c;
379
355
  const manifest = getManifestElement(pkg);
380
356
  if (!manifest) return;
381
357
  for (const item of findAllByName("item", manifest.manifest)) {
382
- const mt = (((_a = item[":@"]) == null ? void 0 : _a["@_media-type"]) ?? "").toLowerCase();
383
- if (!OEB_FONTS.has(mt)) continue;
384
- const href = ((_b = item[":@"]) == null ? void 0 : _b["@_href"]) ?? "";
385
- const dotIdx = href.lastIndexOf(".");
386
- if (dotIdx === -1) continue;
387
- const ext = href.slice(dotIdx).toLowerCase();
388
- const corrected = FONT_MIME_BY_EXT[ext];
389
- if (corrected && corrected !== mt) {
390
- item[":@"] ??= {};
391
- item[":@"]["@_media-type"] = corrected;
392
- }
358
+ const declared = ((_a = item[":@"]) == null ? void 0 : _a["@_media-type"]) ?? "";
359
+ if (!((_b = MediaType.fromMime(declared)) == null ? void 0 : _b.is("font"))) continue;
360
+ const corrected = MediaType.fromPath(((_c = item[":@"]) == null ? void 0 : _c["@_href"]) ?? "");
361
+ if (!(corrected == null ? void 0 : corrected.is("font")) || corrected.mime === declared) continue;
362
+ item[":@"] ??= {};
363
+ item[":@"]["@_media-type"] = corrected.mime;
393
364
  }
394
365
  }
395
366
  function buildTocOl(entries) {
@@ -482,10 +453,7 @@ async function chooseNavHref(epub) {
482
453
  async function removeNcx(epub) {
483
454
  const manifest = await epub.getManifest();
484
455
  const ncxItem = Object.values(manifest).find(
485
- (item) => {
486
- var _a;
487
- return ((_a = item.mediaType) == null ? void 0 : _a.toLowerCase()) === "application/x-dtbncx+xml";
488
- }
456
+ (item) => MediaType.fromMime(item.mediaType ?? "") === MediaType.NCX
489
457
  );
490
458
  if (ncxItem) {
491
459
  await epub.removeManifestItem(ncxItem.id);
package/dist/zip.cjs ADDED
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var zip_exports = {};
20
+ __export(zip_exports, {
21
+ openZip: () => openZip
22
+ });
23
+ module.exports = __toCommonJS(zip_exports);
24
+ var import_node_fs = require("node:fs");
25
+ var import_promises = require("node:fs/promises");
26
+ var import_yauzl_promise = require("yauzl-promise");
27
+ const EOCDR_SIGNATURE = Buffer.from([80, 75, 5, 6]);
28
+ const EOCDR_SIZE = 22;
29
+ const MAX_TAIL_SCAN = 1024 * 1024;
30
+ function toBuffer(source) {
31
+ return Buffer.isBuffer(source) ? source : Buffer.from(source.buffer, source.byteOffset, source.byteLength);
32
+ }
33
+ function findArchiveEnd(tail, tailStart, fileSize) {
34
+ let searchFrom = tail.length - EOCDR_SIZE;
35
+ while (searchFrom >= 0) {
36
+ const index = tail.lastIndexOf(EOCDR_SIGNATURE, searchFrom);
37
+ if (index === -1) return null;
38
+ const commentLength = tail.readUInt16LE(index + 20);
39
+ const end = tailStart + index + EOCDR_SIZE + commentLength;
40
+ if (end < fileSize) return end;
41
+ searchFrom = index - 1;
42
+ }
43
+ return null;
44
+ }
45
+ async function readExactly(handle, length, position) {
46
+ const buffer = Buffer.allocUnsafe(length);
47
+ let offset = 0;
48
+ while (offset < length) {
49
+ const { bytesRead } = await handle.read(
50
+ buffer,
51
+ offset,
52
+ length - offset,
53
+ position + offset
54
+ );
55
+ if (bytesRead === 0) return null;
56
+ offset += bytesRead;
57
+ }
58
+ return buffer;
59
+ }
60
+ async function findFileArchiveEnd(path) {
61
+ let handle;
62
+ try {
63
+ handle = await (0, import_promises.open)(path, "r");
64
+ } catch {
65
+ return null;
66
+ }
67
+ try {
68
+ const { size } = await handle.stat();
69
+ const tailLength = Math.min(size, MAX_TAIL_SCAN);
70
+ if (tailLength < EOCDR_SIZE) return null;
71
+ const tailStart = size - tailLength;
72
+ const tail = await readExactly(handle, tailLength, tailStart);
73
+ if (!tail) return null;
74
+ return findArchiveEnd(tail, tailStart, size);
75
+ } finally {
76
+ await handle.close();
77
+ }
78
+ }
79
+ class TruncatedFileReader extends import_yauzl_promise.Reader {
80
+ constructor(path) {
81
+ super();
82
+ this.path = path;
83
+ }
84
+ handle = null;
85
+ async _open() {
86
+ this.handle = await (0, import_promises.open)(this.path, "r");
87
+ return {};
88
+ }
89
+ async _close() {
90
+ const handle = this.handle;
91
+ this.handle = null;
92
+ if (handle) await handle.close();
93
+ return {};
94
+ }
95
+ async _read(start, length) {
96
+ if (!this.handle) throw new Error(`reader for ${this.path} is not open`);
97
+ const buffer = await readExactly(this.handle, length, start);
98
+ if (!buffer) {
99
+ throw new Error(`unexpected end of file reading ${this.path}`);
100
+ }
101
+ return buffer;
102
+ }
103
+ _createReadStream(start, length) {
104
+ return (0, import_node_fs.createReadStream)(this.path, { start, end: start + length - 1 });
105
+ }
106
+ }
107
+ async function openBufferZip(data) {
108
+ try {
109
+ return await (0, import_yauzl_promise.fromBuffer)(data);
110
+ } catch (error) {
111
+ const tailStart = Math.max(0, data.length - MAX_TAIL_SCAN);
112
+ const end = findArchiveEnd(data.subarray(tailStart), tailStart, data.length);
113
+ if (end === null) throw error;
114
+ try {
115
+ return await (0, import_yauzl_promise.fromBuffer)(data.subarray(0, end));
116
+ } catch {
117
+ throw error;
118
+ }
119
+ }
120
+ }
121
+ async function openFileZip(path) {
122
+ try {
123
+ return await (0, import_yauzl_promise.open)(path);
124
+ } catch (error) {
125
+ const end = await findFileArchiveEnd(path);
126
+ if (end === null) throw error;
127
+ const reader = new TruncatedFileReader(path);
128
+ try {
129
+ return await (0, import_yauzl_promise.fromReader)(reader, end);
130
+ } catch {
131
+ await reader._close().catch(() => void 0);
132
+ throw error;
133
+ }
134
+ }
135
+ }
136
+ async function openZip(source) {
137
+ return typeof source === "string" ? openFileZip(source) : openBufferZip(toBuffer(source));
138
+ }
139
+ // Annotate the CommonJS export names for ESM import in node:
140
+ 0 && (module.exports = {
141
+ openZip
142
+ });
package/dist/zip.d.cts ADDED
@@ -0,0 +1,5 @@
1
+ import { ZipFile } from 'yauzl-promise';
2
+
3
+ declare function openZip(source: string | Uint8Array): Promise<ZipFile>;
4
+
5
+ export { openZip };
package/dist/zip.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { ZipFile } from 'yauzl-promise';
2
+
3
+ declare function openZip(source: string | Uint8Array): Promise<ZipFile>;
4
+
5
+ export { openZip };
package/dist/zip.js ADDED
@@ -0,0 +1,124 @@
1
+ import "./chunk-BIEQXUOY.js";
2
+ import { createReadStream } from "node:fs";
3
+ import { open as openFile } from "node:fs/promises";
4
+ import {
5
+ Reader,
6
+ fromBuffer,
7
+ fromReader,
8
+ open
9
+ } from "yauzl-promise";
10
+ const EOCDR_SIGNATURE = Buffer.from([80, 75, 5, 6]);
11
+ const EOCDR_SIZE = 22;
12
+ const MAX_TAIL_SCAN = 1024 * 1024;
13
+ function toBuffer(source) {
14
+ return Buffer.isBuffer(source) ? source : Buffer.from(source.buffer, source.byteOffset, source.byteLength);
15
+ }
16
+ function findArchiveEnd(tail, tailStart, fileSize) {
17
+ let searchFrom = tail.length - EOCDR_SIZE;
18
+ while (searchFrom >= 0) {
19
+ const index = tail.lastIndexOf(EOCDR_SIGNATURE, searchFrom);
20
+ if (index === -1) return null;
21
+ const commentLength = tail.readUInt16LE(index + 20);
22
+ const end = tailStart + index + EOCDR_SIZE + commentLength;
23
+ if (end < fileSize) return end;
24
+ searchFrom = index - 1;
25
+ }
26
+ return null;
27
+ }
28
+ async function readExactly(handle, length, position) {
29
+ const buffer = Buffer.allocUnsafe(length);
30
+ let offset = 0;
31
+ while (offset < length) {
32
+ const { bytesRead } = await handle.read(
33
+ buffer,
34
+ offset,
35
+ length - offset,
36
+ position + offset
37
+ );
38
+ if (bytesRead === 0) return null;
39
+ offset += bytesRead;
40
+ }
41
+ return buffer;
42
+ }
43
+ async function findFileArchiveEnd(path) {
44
+ let handle;
45
+ try {
46
+ handle = await openFile(path, "r");
47
+ } catch {
48
+ return null;
49
+ }
50
+ try {
51
+ const { size } = await handle.stat();
52
+ const tailLength = Math.min(size, MAX_TAIL_SCAN);
53
+ if (tailLength < EOCDR_SIZE) return null;
54
+ const tailStart = size - tailLength;
55
+ const tail = await readExactly(handle, tailLength, tailStart);
56
+ if (!tail) return null;
57
+ return findArchiveEnd(tail, tailStart, size);
58
+ } finally {
59
+ await handle.close();
60
+ }
61
+ }
62
+ class TruncatedFileReader extends Reader {
63
+ constructor(path) {
64
+ super();
65
+ this.path = path;
66
+ }
67
+ handle = null;
68
+ async _open() {
69
+ this.handle = await openFile(this.path, "r");
70
+ return {};
71
+ }
72
+ async _close() {
73
+ const handle = this.handle;
74
+ this.handle = null;
75
+ if (handle) await handle.close();
76
+ return {};
77
+ }
78
+ async _read(start, length) {
79
+ if (!this.handle) throw new Error(`reader for ${this.path} is not open`);
80
+ const buffer = await readExactly(this.handle, length, start);
81
+ if (!buffer) {
82
+ throw new Error(`unexpected end of file reading ${this.path}`);
83
+ }
84
+ return buffer;
85
+ }
86
+ _createReadStream(start, length) {
87
+ return createReadStream(this.path, { start, end: start + length - 1 });
88
+ }
89
+ }
90
+ async function openBufferZip(data) {
91
+ try {
92
+ return await fromBuffer(data);
93
+ } catch (error) {
94
+ const tailStart = Math.max(0, data.length - MAX_TAIL_SCAN);
95
+ const end = findArchiveEnd(data.subarray(tailStart), tailStart, data.length);
96
+ if (end === null) throw error;
97
+ try {
98
+ return await fromBuffer(data.subarray(0, end));
99
+ } catch {
100
+ throw error;
101
+ }
102
+ }
103
+ }
104
+ async function openFileZip(path) {
105
+ try {
106
+ return await open(path);
107
+ } catch (error) {
108
+ const end = await findFileArchiveEnd(path);
109
+ if (end === null) throw error;
110
+ const reader = new TruncatedFileReader(path);
111
+ try {
112
+ return await fromReader(reader, end);
113
+ } catch {
114
+ await reader._close().catch(() => void 0);
115
+ throw error;
116
+ }
117
+ }
118
+ }
119
+ async function openZip(source) {
120
+ return typeof source === "string" ? openFileZip(source) : openBufferZip(toBuffer(source));
121
+ }
122
+ export {
123
+ openZip
124
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storyteller-platform/epub",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "type": "module",
5
5
  "module": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -32,7 +32,6 @@
32
32
  "@storyteller-platform/eslint": "0.1.0",
33
33
  "@storyteller-platform/tsup": "^0.1.0",
34
34
  "@tsconfig/strictest": "^2.0.5",
35
- "@types/mime-types": "^2",
36
35
  "@types/node": "^24.0.0",
37
36
  "@types/yauzl-promise": "^4",
38
37
  "@types/yazl": "^3",
@@ -48,13 +47,13 @@
48
47
  },
49
48
  "dependencies": {
50
49
  "@readium/shared": "^2.2.0",
51
- "@storyteller-platform/fs": "^0.1.4",
52
- "@storyteller-platform/path": "^0.1.3",
50
+ "@storyteller-platform/fs": "^0.2.0",
51
+ "@storyteller-platform/media-types": "^0.1.0",
52
+ "@storyteller-platform/path": "^0.2.0",
53
53
  "@zip.js/zip.js": "^2.0.0",
54
54
  "async-mutex": "^0.5.0",
55
55
  "fast-xml-parser": "^4.0.1",
56
56
  "mem": "^8.0.0",
57
- "mime-types": "^3.0.1",
58
57
  "nanoid": "^5.1.5",
59
58
  "yauzl-promise": "^4.0.0",
60
59
  "yazl": "^3.3.1"