@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/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 = (
|
|
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
|
|
383
|
-
if (!
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
package/dist/zip.d.ts
ADDED
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.
|
|
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.
|
|
52
|
-
"@storyteller-platform/
|
|
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"
|