@storyteller-platform/epub 0.8.1 → 1.0.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.
- package/README.md +8243 -572
- package/dist/adapters/interface.d.ts +20 -1
- package/dist/adapters/memory.d.ts +2 -2
- package/dist/adapters/memory.js +16 -30
- package/dist/adapters/tmpfs.browser.d.ts +11 -0
- package/dist/adapters/tmpfs.browser.js +9 -0
- package/dist/adapters/tmpfs.d.ts +2 -2
- package/dist/adapters/tmpfs.js +22 -35
- package/dist/files.browser.d.ts +2 -0
- package/dist/files.browser.js +7 -0
- package/dist/files.node.d.ts +2 -0
- package/dist/files.node.js +10 -0
- package/dist/index.d.ts +48 -120
- package/dist/index.js +401 -646
- package/dist/jsx-dev-runtime.d.ts +9 -0
- package/dist/jsx-dev-runtime.js +9 -0
- package/dist/jsx-runtime.d.ts +42 -0
- package/dist/jsx-runtime.js +69 -0
- package/dist/upgrade.d.ts +26 -17
- package/dist/upgrade.js +117 -157
- package/dist/xml.d.ts +103 -0
- package/dist/xml.js +248 -0
- package/dist/zip.browser.d.ts +2 -0
- package/dist/zip.browser.js +33 -0
- package/dist/zip.node.d.ts +9 -0
- package/dist/zip.node.js +246 -0
- package/package.json +92 -10
- package/dist/epubcheck.d.ts +0 -21
- package/dist/epubcheck.js +0 -34
- package/dist/zip.d.ts +0 -2
- package/dist/zip.js +0 -137
package/dist/xml.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import xmldom from "@xmldom/xmldom";
|
|
2
|
+
const { DOMParser } = xmldom;
|
|
3
|
+
export const { XMLSerializer } = xmldom;
|
|
4
|
+
export function isElement(n) {
|
|
5
|
+
return n.nodeType === 1;
|
|
6
|
+
}
|
|
7
|
+
export function isText(n) {
|
|
8
|
+
return n.nodeType === 3;
|
|
9
|
+
}
|
|
10
|
+
export function isCdata(n) {
|
|
11
|
+
return n.nodeType === 4;
|
|
12
|
+
}
|
|
13
|
+
export function isComment(n) {
|
|
14
|
+
return n.nodeType === 8;
|
|
15
|
+
}
|
|
16
|
+
function setNamespace(element, namespace) {
|
|
17
|
+
;
|
|
18
|
+
element.namespaceURI = namespace;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Give every element in `node`'s subtree the namespace its position implies,
|
|
22
|
+
* without overwriting any element that already has one.
|
|
23
|
+
*
|
|
24
|
+
* @privateremarks
|
|
25
|
+
*
|
|
26
|
+
* `x()` builds elements in no namespace. That serializes correctly, because a
|
|
27
|
+
* child in no namespace inherits the enclosing default namespace on the way
|
|
28
|
+
* out, but in memory the elements report `namespaceURI === null` and so are
|
|
29
|
+
* invisible to namespace-aware queries such as `getElementsByTagNameNS`.
|
|
30
|
+
* Calling this after inserting a node makes the tree report the namespace its
|
|
31
|
+
* position implies.
|
|
32
|
+
*/
|
|
33
|
+
export function adoptNamespaces(node) {
|
|
34
|
+
const { parentNode } = node;
|
|
35
|
+
const inherited = parentNode && isElement(parentNode) ? parentNode.namespaceURI : null;
|
|
36
|
+
if (inherited && !node.namespaceURI) {
|
|
37
|
+
setNamespace(node, inherited);
|
|
38
|
+
}
|
|
39
|
+
for (const child of Array.from(node.children)) {
|
|
40
|
+
adoptNamespaces(child);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Declare `uri` as the default namespace of `element` and bring the elements
|
|
45
|
+
* already under it in line with that declaration.
|
|
46
|
+
*
|
|
47
|
+
* xmldom records a namespace declaration without applying it to the tree, so a
|
|
48
|
+
* document read without `xmlns` stays in no namespace in memory even once the
|
|
49
|
+
* declaration is added. Namespace-aware lookups cannot see any of it until the
|
|
50
|
+
* subtree is fixed up, and the document element has no element parent for
|
|
51
|
+
* `adoptNamespaces` to inherit from, so it has to be bound explicitly.
|
|
52
|
+
*/
|
|
53
|
+
export function declareDefaultNamespace(element, uri) {
|
|
54
|
+
element.setAttributeNS(NS.xmlns, "xmlns", uri);
|
|
55
|
+
setNamespace(element, uri);
|
|
56
|
+
adoptNamespaces(element);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Insert `child` into `parent`, then adopt its namespace.
|
|
60
|
+
*
|
|
61
|
+
* If `reference` is omitted, appends child to parent.
|
|
62
|
+
*/
|
|
63
|
+
export function insert(parent, child, reference) {
|
|
64
|
+
parent.insertBefore(child, reference ?? null);
|
|
65
|
+
if (isElement(child))
|
|
66
|
+
adoptNamespaces(child);
|
|
67
|
+
return child;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Replace `oldChild` with `newChild` in `parent`, then adopt the new child's
|
|
71
|
+
* namespace.
|
|
72
|
+
*/
|
|
73
|
+
export function replace(parent, newChild, oldChild) {
|
|
74
|
+
parent.replaceChild(newChild, oldChild);
|
|
75
|
+
if (isElement(newChild))
|
|
76
|
+
adoptNamespaces(newChild);
|
|
77
|
+
return newChild;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Given an XHTML document, return the body element.
|
|
81
|
+
*/
|
|
82
|
+
export function getXhtmlBody(document) {
|
|
83
|
+
const html = document.documentElement;
|
|
84
|
+
if (!html)
|
|
85
|
+
throw new Error("Invalid XHTML document: no html element");
|
|
86
|
+
const body = html.getElementsByTagName("body").item(0);
|
|
87
|
+
if (!body)
|
|
88
|
+
throw new Error("Invalid XHTML document: No body element");
|
|
89
|
+
return body;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Given an Element, find the first child matching
|
|
93
|
+
* the provided namespace, name and optional filter.
|
|
94
|
+
*/
|
|
95
|
+
export function findXmlChildByNameNS(namespaceUri, name, element, filter) {
|
|
96
|
+
for (const child of Array.from(element.children)) {
|
|
97
|
+
if ((!namespaceUri || namespaceUri === child.namespaceURI) &&
|
|
98
|
+
name === child.localName &&
|
|
99
|
+
(!filter || filter(child))) {
|
|
100
|
+
return child;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Given an Element, find the first child matching
|
|
107
|
+
* the provided name and optional filter.
|
|
108
|
+
*/
|
|
109
|
+
export function findXmlChildByName(name, element, filter) {
|
|
110
|
+
return findXmlChildByNameNS(null, name, element, filter);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Given an Element, find the first descendant matching
|
|
114
|
+
* the provided name and optional filter.
|
|
115
|
+
*
|
|
116
|
+
* Will perform a breadth first search for the element, returning
|
|
117
|
+
* the highest element in the tree matching the name and filter.
|
|
118
|
+
*/
|
|
119
|
+
export function findXmlDescendantByNameNS(namespaceUri, name, element, filter) {
|
|
120
|
+
const queue = Array.from(element.children);
|
|
121
|
+
while (queue.length > 0) {
|
|
122
|
+
const candidate = queue.shift();
|
|
123
|
+
if (!candidate)
|
|
124
|
+
continue;
|
|
125
|
+
if ((!namespaceUri || candidate.namespaceURI === namespaceUri) &&
|
|
126
|
+
candidate.localName === name &&
|
|
127
|
+
(!filter || filter(candidate))) {
|
|
128
|
+
return candidate;
|
|
129
|
+
}
|
|
130
|
+
queue.push(...Array.from(candidate.children));
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Given an Element, find the first descendant matching
|
|
136
|
+
* the provided name and optional filter.
|
|
137
|
+
*
|
|
138
|
+
* Will perform a breadth first search for the element, returning
|
|
139
|
+
* the highest element in the tree matching the name and filter.
|
|
140
|
+
*/
|
|
141
|
+
export function findXmlDescendantByName(name, element, filter) {
|
|
142
|
+
return findXmlDescendantByNameNS(null, name, element, filter);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Given an XHTML document, add a `link` element to the
|
|
146
|
+
* `head` of the document.
|
|
147
|
+
*/
|
|
148
|
+
export function addLinkToXhtmlHead(document, link) {
|
|
149
|
+
const html = document.documentElement;
|
|
150
|
+
if (!html)
|
|
151
|
+
throw new Error("Invalid XHTML document: no html element");
|
|
152
|
+
const head = html.getElementsByTagName("head").item(0);
|
|
153
|
+
if (!head)
|
|
154
|
+
throw new Error("Invalid XHTML document: no head element");
|
|
155
|
+
const linkEl = document.createElement("link");
|
|
156
|
+
linkEl.setAttribute("rel", link.rel);
|
|
157
|
+
linkEl.setAttribute("href", link.href);
|
|
158
|
+
linkEl.setAttribute("type", link.type);
|
|
159
|
+
head.appendChild(linkEl);
|
|
160
|
+
}
|
|
161
|
+
export function parse(xml, mediaType, onError) {
|
|
162
|
+
// A byte order mark ahead of the XML declaration is legal in a file, but once
|
|
163
|
+
// the file has been decoded into a string the mark occupies offset 0, and
|
|
164
|
+
// xmldom then rejects the declaration for not being at the start.
|
|
165
|
+
const source = xml.charCodeAt(0) === 0xfeff ? xml.slice(1) : xml;
|
|
166
|
+
return new DOMParser({
|
|
167
|
+
onError: (level, message) => {
|
|
168
|
+
onError?.({
|
|
169
|
+
level,
|
|
170
|
+
message,
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
}).parseFromString(source, mediaType);
|
|
174
|
+
}
|
|
175
|
+
export function serialize(doc, options) {
|
|
176
|
+
const serialized = new XMLSerializer().serializeToString(doc);
|
|
177
|
+
if (options?.validate) {
|
|
178
|
+
new DOMParser({
|
|
179
|
+
onError: (level, message) => {
|
|
180
|
+
if (level === "error" || level === "fatalError") {
|
|
181
|
+
throw new Error(message);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
}).parseFromString(serialized, "text/xml");
|
|
185
|
+
}
|
|
186
|
+
return serialized;
|
|
187
|
+
}
|
|
188
|
+
const xmlDoc = new DOMParser({ onError: () => { } }).parseFromString("<root/>", "text/xml");
|
|
189
|
+
export const NS = {
|
|
190
|
+
xmlns: "http://www.w3.org/2000/xmlns/",
|
|
191
|
+
xml: "http://www.w3.org/XML/1998/namespace",
|
|
192
|
+
xhtml: "http://www.w3.org/1999/xhtml",
|
|
193
|
+
opf: "http://www.idpf.org/2007/opf",
|
|
194
|
+
dc: "http://purl.org/dc/elements/1.1/",
|
|
195
|
+
epub: "http://www.idpf.org/2007/ops",
|
|
196
|
+
};
|
|
197
|
+
const nsPrefixes = Object.keys(NS);
|
|
198
|
+
export function x(name, attributes, ...children) {
|
|
199
|
+
if (name === "xmlns" || name.startsWith("xmlns:")) {
|
|
200
|
+
throw new Error(`Invalid element name ${name}`);
|
|
201
|
+
}
|
|
202
|
+
const nameNs = nsPrefixes.find((prefix) => name.startsWith(`${prefix}:`));
|
|
203
|
+
const element = nameNs
|
|
204
|
+
? xmlDoc.createElementNS(NS[nameNs], name)
|
|
205
|
+
: xmlDoc.createElement(name);
|
|
206
|
+
for (const [attrName, value] of Object.entries(attributes ?? {})) {
|
|
207
|
+
if (value === undefined)
|
|
208
|
+
continue;
|
|
209
|
+
const attrNs = attrName === "xmlns"
|
|
210
|
+
? "xmlns"
|
|
211
|
+
: nsPrefixes.find((prefix) => attrName.startsWith(`${prefix}:`));
|
|
212
|
+
if (attrNs) {
|
|
213
|
+
element.setAttributeNS(NS[attrNs], attrName, value);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
element.setAttribute(attrName, value);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// An `xmlns` attribute declares this element's own default namespace, but
|
|
220
|
+
// xmldom does not bind it the way the DOM requires. Without this the element
|
|
221
|
+
// reports `namespaceURI === null`, so `adoptNamespaces` re-declares over the
|
|
222
|
+
// top of it: the output carries two `xmlns` attributes, which fails to parse
|
|
223
|
+
// (`Attribute xmlns redefined`), and the element lands in the wrong namespace.
|
|
224
|
+
const defaultNamespace = (attributes ?? {})["xmlns"];
|
|
225
|
+
if (defaultNamespace !== undefined) {
|
|
226
|
+
setNamespace(element, defaultNamespace);
|
|
227
|
+
}
|
|
228
|
+
for (const child of children) {
|
|
229
|
+
if (child == null)
|
|
230
|
+
continue;
|
|
231
|
+
if (typeof child === "string") {
|
|
232
|
+
element.appendChild(xmlDoc.createTextNode(child));
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
element.appendChild(child);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return element;
|
|
239
|
+
}
|
|
240
|
+
export function text(data) {
|
|
241
|
+
return xmlDoc.createTextNode(data);
|
|
242
|
+
}
|
|
243
|
+
export function createDocument(namespaceUri, qualifiedName, docType) {
|
|
244
|
+
return xmlDoc.implementation.createDocument(namespaceUri, qualifiedName, docType);
|
|
245
|
+
}
|
|
246
|
+
export function createDocumentType(qualifiedName, publicId, systemId) {
|
|
247
|
+
return xmlDoc.implementation.createDocumentType(qualifiedName, publicId, systemId);
|
|
248
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { BlobReader, Uint8ArrayReader, Uint8ArrayWriter, ZipReader, } from "@zip.js/zip.js/lib/zip-core-native.js";
|
|
2
|
+
import { openFile } from "#files";
|
|
3
|
+
const MAX_APPENDED_DATA = 1024 * 1024;
|
|
4
|
+
function toZipEntry(entry) {
|
|
5
|
+
return {
|
|
6
|
+
filename: entry.filename,
|
|
7
|
+
compressedSize: entry.compressedSize,
|
|
8
|
+
uncompressedSize: entry.uncompressedSize,
|
|
9
|
+
read: () => entry.getData(new Uint8ArrayWriter()),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export async function openZip(source) {
|
|
13
|
+
const data = typeof source === "string" ? await openFile(source) : source;
|
|
14
|
+
const reader = new ZipReader(data instanceof Uint8Array
|
|
15
|
+
? new Uint8ArrayReader(data)
|
|
16
|
+
: new BlobReader(data), {
|
|
17
|
+
useWebWorkers: false,
|
|
18
|
+
// this in combination with maxAppendedDataSize is basically the manual fix
|
|
19
|
+
// we used to have for yauzl: sometimes zips has garbage at the end that
|
|
20
|
+
// would cause an error. zip.js has native support for this
|
|
21
|
+
strictness: "tolerant",
|
|
22
|
+
maxAppendedDataSize: MAX_APPENDED_DATA,
|
|
23
|
+
filenameValidation: "balanced",
|
|
24
|
+
checkCrc32: true,
|
|
25
|
+
});
|
|
26
|
+
const entries = await reader.getEntries();
|
|
27
|
+
return {
|
|
28
|
+
entries: entries
|
|
29
|
+
.filter((entry) => !entry.directory)
|
|
30
|
+
.map(toZipEntry),
|
|
31
|
+
close: () => reader.close(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
2
|
+
import type { ArchiveSource, ZipArchive, ZipEntry } from "./adapters/interface.ts";
|
|
3
|
+
export interface NodeZipEntry extends ZipEntry {
|
|
4
|
+
openReadStream(): Promise<Readable>;
|
|
5
|
+
}
|
|
6
|
+
export interface NodeZipArchive extends ZipArchive, AsyncDisposable {
|
|
7
|
+
readonly entries: readonly NodeZipEntry[];
|
|
8
|
+
}
|
|
9
|
+
export declare function openZip(source: ArchiveSource): Promise<NodeZipArchive>;
|
package/dist/zip.node.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
2
|
+
if (value !== null && value !== void 0) {
|
|
3
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
+
var dispose, inner;
|
|
5
|
+
if (async) {
|
|
6
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
+
dispose = value[Symbol.asyncDispose];
|
|
8
|
+
}
|
|
9
|
+
if (dispose === void 0) {
|
|
10
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
+
dispose = value[Symbol.dispose];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
16
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
17
|
+
}
|
|
18
|
+
else if (async) {
|
|
19
|
+
env.stack.push({ async: true });
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
24
|
+
return function (env) {
|
|
25
|
+
function fail(e) {
|
|
26
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
27
|
+
env.hasError = true;
|
|
28
|
+
}
|
|
29
|
+
var r, s = 0;
|
|
30
|
+
function next() {
|
|
31
|
+
while (r = env.stack.pop()) {
|
|
32
|
+
try {
|
|
33
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
34
|
+
if (r.dispose) {
|
|
35
|
+
var result = r.dispose.call(r.value);
|
|
36
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
37
|
+
}
|
|
38
|
+
else s |= 1;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
fail(e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
45
|
+
if (env.hasError) throw env.error;
|
|
46
|
+
}
|
|
47
|
+
return next();
|
|
48
|
+
};
|
|
49
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
});
|
|
53
|
+
import { createReadStream } from "node:fs";
|
|
54
|
+
import { open as openFile, stat } from "node:fs/promises";
|
|
55
|
+
import { Readable } from "node:stream";
|
|
56
|
+
import { Reader, fromBuffer, fromReader, } from "yauzl-promise";
|
|
57
|
+
const EOCDR_SIGNATURE = Buffer.from([0x50, 0x4b, 0x05, 0x06]);
|
|
58
|
+
const EOCDR_SIZE = 22;
|
|
59
|
+
const MAX_TAIL_SCAN = 1024 * 1024;
|
|
60
|
+
function toBuffer(source) {
|
|
61
|
+
return Buffer.isBuffer(source)
|
|
62
|
+
? source
|
|
63
|
+
: Buffer.from(source.buffer, source.byteOffset, source.byteLength);
|
|
64
|
+
}
|
|
65
|
+
function findArchiveEnd(tail, tailStart, fileSize) {
|
|
66
|
+
let searchFrom = tail.length - EOCDR_SIZE;
|
|
67
|
+
while (searchFrom >= 0) {
|
|
68
|
+
const index = tail.lastIndexOf(EOCDR_SIGNATURE, searchFrom);
|
|
69
|
+
if (index === -1)
|
|
70
|
+
return null;
|
|
71
|
+
const commentLength = tail.readUInt16LE(index + 20);
|
|
72
|
+
const end = tailStart + index + EOCDR_SIZE + commentLength;
|
|
73
|
+
if (end < fileSize)
|
|
74
|
+
return end;
|
|
75
|
+
searchFrom = index - 1;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
async function readExactly(handle, length, position) {
|
|
80
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
81
|
+
let offset = 0;
|
|
82
|
+
while (offset < length) {
|
|
83
|
+
const { bytesRead } = await handle.read(buffer, offset, length - offset, position + offset);
|
|
84
|
+
if (bytesRead === 0)
|
|
85
|
+
return null;
|
|
86
|
+
offset += bytesRead;
|
|
87
|
+
}
|
|
88
|
+
return buffer;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Basically sometimes EPUB 2s have some garbage at the end of the file after
|
|
92
|
+
* the End of Central Directory Record, which leads to
|
|
93
|
+
* `yauzl` throwing an "End of Central Directory Record not found" error
|
|
94
|
+
*
|
|
95
|
+
* This is an attempt to, when that error happens, try to read the file from the end and find the End of Central Directory Record, then clean up the garbage at the end of the file.
|
|
96
|
+
*/
|
|
97
|
+
async function openTolerant(size, open, readTail) {
|
|
98
|
+
try {
|
|
99
|
+
return await open(size);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
const tailStart = Math.max(0, size - MAX_TAIL_SCAN);
|
|
103
|
+
const tail = await readTail(tailStart).catch(() => null);
|
|
104
|
+
const end = tail && findArchiveEnd(tail, tailStart, size);
|
|
105
|
+
if (!end)
|
|
106
|
+
throw error;
|
|
107
|
+
return await open(end).catch(() => {
|
|
108
|
+
throw error;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
class FileHandleReader extends Reader {
|
|
113
|
+
path;
|
|
114
|
+
handle = null;
|
|
115
|
+
constructor(path) {
|
|
116
|
+
super();
|
|
117
|
+
this.path = path;
|
|
118
|
+
}
|
|
119
|
+
async _open() {
|
|
120
|
+
this.handle = await openFile(this.path, "r");
|
|
121
|
+
return {};
|
|
122
|
+
}
|
|
123
|
+
async _close() {
|
|
124
|
+
const handle = this.handle;
|
|
125
|
+
this.handle = null;
|
|
126
|
+
if (handle)
|
|
127
|
+
await handle.close();
|
|
128
|
+
return {};
|
|
129
|
+
}
|
|
130
|
+
async _read(start, length) {
|
|
131
|
+
if (!this.handle)
|
|
132
|
+
throw new Error(`reader for ${this.path} is not open`);
|
|
133
|
+
const buffer = await readExactly(this.handle, length, start);
|
|
134
|
+
if (!buffer) {
|
|
135
|
+
throw new Error(`unexpected end of file reading ${this.path}`);
|
|
136
|
+
}
|
|
137
|
+
return buffer;
|
|
138
|
+
}
|
|
139
|
+
_createReadStream(start, length) {
|
|
140
|
+
return createReadStream(this.path, { start, end: start + length - 1 });
|
|
141
|
+
}
|
|
142
|
+
async [Symbol.asyncDispose]() {
|
|
143
|
+
await this._close();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// yauzl-promise's own `open(path)` leaves the file open when parsing fails,
|
|
147
|
+
// so we hand it a reader we close ourselves unless the zip takes it over
|
|
148
|
+
async function openFileZip(path, size) {
|
|
149
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
150
|
+
try {
|
|
151
|
+
const cleanup = __addDisposableResource(env_1, new AsyncDisposableStack(), true);
|
|
152
|
+
const reader = cleanup.use(new FileHandleReader(path));
|
|
153
|
+
const zip = await fromReader(reader, size);
|
|
154
|
+
cleanup.move();
|
|
155
|
+
return zip;
|
|
156
|
+
}
|
|
157
|
+
catch (e_1) {
|
|
158
|
+
env_1.error = e_1;
|
|
159
|
+
env_1.hasError = true;
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
const result_1 = __disposeResources(env_1);
|
|
163
|
+
if (result_1)
|
|
164
|
+
await result_1;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function readFileTail(path, start) {
|
|
168
|
+
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
169
|
+
try {
|
|
170
|
+
const handle = __addDisposableResource(env_2, await openFile(path, "r"), true);
|
|
171
|
+
const { size } = await handle.stat();
|
|
172
|
+
return await readExactly(handle, size - start, start);
|
|
173
|
+
}
|
|
174
|
+
catch (e_2) {
|
|
175
|
+
env_2.error = e_2;
|
|
176
|
+
env_2.hasError = true;
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
const result_2 = __disposeResources(env_2);
|
|
180
|
+
if (result_2)
|
|
181
|
+
await result_2;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
class BlobReader extends Reader {
|
|
185
|
+
blob;
|
|
186
|
+
constructor(blob) {
|
|
187
|
+
super();
|
|
188
|
+
this.blob = blob;
|
|
189
|
+
}
|
|
190
|
+
async _read(start, length) {
|
|
191
|
+
return Buffer.from(await this.blob.slice(start, start + length).arrayBuffer());
|
|
192
|
+
}
|
|
193
|
+
_createReadStream(start, length) {
|
|
194
|
+
return Readable.fromWeb(this.blob.slice(start, start + length).stream());
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function toZipEntry(entry) {
|
|
198
|
+
return {
|
|
199
|
+
filename: entry.filename,
|
|
200
|
+
compressedSize: entry.compressedSize,
|
|
201
|
+
uncompressedSize: entry.uncompressedSize,
|
|
202
|
+
openReadStream: () => entry.openReadStream(),
|
|
203
|
+
async read() {
|
|
204
|
+
const chunks = [];
|
|
205
|
+
for await (const chunk of await entry.openReadStream()) {
|
|
206
|
+
chunks.push(chunk);
|
|
207
|
+
}
|
|
208
|
+
return Buffer.concat(chunks);
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
async function openYauzl(source) {
|
|
213
|
+
if (typeof source === "string") {
|
|
214
|
+
return await openTolerant((await stat(source)).size, (size) => openFileZip(source, size), (start) => readFileTail(source, start));
|
|
215
|
+
}
|
|
216
|
+
if (source instanceof Uint8Array) {
|
|
217
|
+
const data = toBuffer(source);
|
|
218
|
+
return await openTolerant(data.length, (size) => fromBuffer(data.subarray(0, size)), (start) => Promise.resolve(data.subarray(start)));
|
|
219
|
+
}
|
|
220
|
+
return await openTolerant(source.size, (size) => fromReader(new BlobReader(source), size), async (start) => Buffer.from(await source.slice(start).arrayBuffer()));
|
|
221
|
+
}
|
|
222
|
+
export async function openZip(source) {
|
|
223
|
+
const env_3 = { stack: [], error: void 0, hasError: false };
|
|
224
|
+
try {
|
|
225
|
+
const cleanup = __addDisposableResource(env_3, new AsyncDisposableStack(), true);
|
|
226
|
+
const zip = cleanup.adopt(await openYauzl(source), (zip) => zip.close());
|
|
227
|
+
const entries = await zip.readEntries();
|
|
228
|
+
const owned = cleanup.move();
|
|
229
|
+
return {
|
|
230
|
+
entries: entries
|
|
231
|
+
.filter((entry) => !entry.filename.endsWith("/"))
|
|
232
|
+
.map(toZipEntry),
|
|
233
|
+
close: () => owned.disposeAsync(),
|
|
234
|
+
[Symbol.asyncDispose]: () => owned.disposeAsync(),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
catch (e_3) {
|
|
238
|
+
env_3.error = e_3;
|
|
239
|
+
env_3.hasError = true;
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
const result_3 = __disposeResources(env_3);
|
|
243
|
+
if (result_3)
|
|
244
|
+
await result_3;
|
|
245
|
+
}
|
|
246
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@storyteller-platform/epub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"exports": {
|
|
8
|
-
"
|
|
9
|
-
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./jsx-runtime": {
|
|
13
|
+
"types": "./dist/jsx-runtime.d.ts",
|
|
14
|
+
"default": "./dist/jsx-runtime.js"
|
|
15
|
+
},
|
|
16
|
+
"./jsx-dev-runtime": {
|
|
17
|
+
"types": "./dist/jsx-dev-runtime.d.ts",
|
|
18
|
+
"default": "./dist/jsx-dev-runtime.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"imports": {
|
|
22
|
+
"#files": {
|
|
23
|
+
"node": {
|
|
24
|
+
"types": "./dist/files.node.d.ts",
|
|
25
|
+
"default": "./dist/files.node.js"
|
|
26
|
+
},
|
|
27
|
+
"default": {
|
|
28
|
+
"types": "./dist/files.browser.d.ts",
|
|
29
|
+
"default": "./dist/files.browser.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"#tmpfs": {
|
|
33
|
+
"node": {
|
|
34
|
+
"types": "./dist/adapters/tmpfs.d.ts",
|
|
35
|
+
"default": "./dist/adapters/tmpfs.js"
|
|
36
|
+
},
|
|
37
|
+
"default": {
|
|
38
|
+
"types": "./dist/adapters/tmpfs.browser.d.ts",
|
|
39
|
+
"default": "./dist/adapters/tmpfs.browser.js"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"#zip": {
|
|
43
|
+
"node": {
|
|
44
|
+
"types": "./dist/zip.node.d.ts",
|
|
45
|
+
"default": "./dist/zip.node.js"
|
|
46
|
+
},
|
|
47
|
+
"default": {
|
|
48
|
+
"types": "./dist/zip.browser.d.ts",
|
|
49
|
+
"default": "./dist/zip.browser.js"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
10
52
|
},
|
|
11
53
|
"files": [
|
|
12
54
|
"dist/"
|
|
@@ -29,7 +71,7 @@
|
|
|
29
71
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
30
72
|
"markdown-toc": "^1.2.0",
|
|
31
73
|
"remark-toc": "^9.0.0",
|
|
32
|
-
"tsx": "^4.
|
|
74
|
+
"tsx": "^4.23.12",
|
|
33
75
|
"typedoc": "^0.28.20",
|
|
34
76
|
"typedoc-plugin-markdown": "^4.6.3",
|
|
35
77
|
"typedoc-plugin-remark": "^1.2.0",
|
|
@@ -38,14 +80,12 @@
|
|
|
38
80
|
"vitest": "^5.0.0"
|
|
39
81
|
},
|
|
40
82
|
"dependencies": {
|
|
41
|
-
"@readium/shared": "^2.2.0",
|
|
42
83
|
"@storyteller-platform/fs": "^0.3.0",
|
|
43
84
|
"@storyteller-platform/media-types": "^0.2.1",
|
|
44
85
|
"@storyteller-platform/path": "^0.3.1",
|
|
45
|
-
"@
|
|
86
|
+
"@xmldom/xmldom": "^0.9.12",
|
|
87
|
+
"@zip.js/zip.js": "^2.18.2",
|
|
46
88
|
"async-mutex": "^0.5.0",
|
|
47
|
-
"fast-xml-builder": "^1.3.1",
|
|
48
|
-
"fast-xml-parser": "^5.11.1",
|
|
49
89
|
"mem": "^8.0.0",
|
|
50
90
|
"nanoid": "^5.1.5",
|
|
51
91
|
"yauzl-promise": "^4.0.0",
|
|
@@ -54,8 +94,50 @@
|
|
|
54
94
|
"publishConfig": {
|
|
55
95
|
"access": "public",
|
|
56
96
|
"exports": {
|
|
57
|
-
"
|
|
58
|
-
|
|
97
|
+
".": {
|
|
98
|
+
"types": "./dist/index.d.ts",
|
|
99
|
+
"default": "./dist/index.js"
|
|
100
|
+
},
|
|
101
|
+
"./jsx-runtime": {
|
|
102
|
+
"types": "./dist/jsx-runtime.d.ts",
|
|
103
|
+
"default": "./dist/jsx-runtime.js"
|
|
104
|
+
},
|
|
105
|
+
"./jsx-dev-runtime": {
|
|
106
|
+
"types": "./dist/jsx-dev-runtime.d.ts",
|
|
107
|
+
"default": "./dist/jsx-dev-runtime.js"
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
"imports": {
|
|
111
|
+
"#files": {
|
|
112
|
+
"node": {
|
|
113
|
+
"types": "./dist/files.node.d.ts",
|
|
114
|
+
"default": "./dist/files.node.js"
|
|
115
|
+
},
|
|
116
|
+
"default": {
|
|
117
|
+
"types": "./dist/files.browser.d.ts",
|
|
118
|
+
"default": "./dist/files.browser.js"
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
"#tmpfs": {
|
|
122
|
+
"node": {
|
|
123
|
+
"types": "./dist/adapters/tmpfs.d.ts",
|
|
124
|
+
"default": "./dist/adapters/tmpfs.js"
|
|
125
|
+
},
|
|
126
|
+
"default": {
|
|
127
|
+
"types": "./dist/adapters/tmpfs.browser.d.ts",
|
|
128
|
+
"default": "./dist/adapters/tmpfs.browser.js"
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
"#zip": {
|
|
132
|
+
"node": {
|
|
133
|
+
"types": "./dist/zip.node.d.ts",
|
|
134
|
+
"default": "./dist/zip.node.js"
|
|
135
|
+
},
|
|
136
|
+
"default": {
|
|
137
|
+
"types": "./dist/zip.browser.d.ts",
|
|
138
|
+
"default": "./dist/zip.browser.js"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
59
141
|
}
|
|
60
142
|
}
|
|
61
143
|
}
|