@storyteller-platform/epub 0.4.0 → 0.4.2
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 +134 -142
- package/dist/index.cjs +195 -69
- package/dist/index.d.cts +2 -5
- package/dist/index.d.ts +2 -5
- package/dist/index.js +197 -71
- package/package.json +6 -4
package/dist/index.js
CHANGED
|
@@ -1,18 +1,87 @@
|
|
|
1
|
+
var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
|
|
2
|
+
var __typeError = (msg) => {
|
|
3
|
+
throw TypeError(msg);
|
|
4
|
+
};
|
|
5
|
+
var __using = (stack, value, async) => {
|
|
6
|
+
if (value != null) {
|
|
7
|
+
if (typeof value !== "object" && typeof value !== "function") __typeError("Object expected");
|
|
8
|
+
var dispose, inner;
|
|
9
|
+
if (async) dispose = value[__knownSymbol("asyncDispose")];
|
|
10
|
+
if (dispose === void 0) {
|
|
11
|
+
dispose = value[__knownSymbol("dispose")];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") __typeError("Object not disposable");
|
|
15
|
+
if (inner) dispose = function() {
|
|
16
|
+
try {
|
|
17
|
+
inner.call(this);
|
|
18
|
+
} catch (e) {
|
|
19
|
+
return Promise.reject(e);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
stack.push([async, dispose, value]);
|
|
23
|
+
} else if (async) {
|
|
24
|
+
stack.push([async]);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
};
|
|
28
|
+
var __callDispose = (stack, error, hasError) => {
|
|
29
|
+
var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
|
|
30
|
+
return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
|
|
31
|
+
};
|
|
32
|
+
var fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e);
|
|
33
|
+
var next = (it) => {
|
|
34
|
+
while (it = stack.pop()) {
|
|
35
|
+
try {
|
|
36
|
+
var result = it[1] && it[1].call(it[2]);
|
|
37
|
+
if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next()));
|
|
38
|
+
} catch (e) {
|
|
39
|
+
fail(e);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (hasError) throw error;
|
|
43
|
+
};
|
|
44
|
+
return next();
|
|
45
|
+
};
|
|
1
46
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { rmSync } from "node:fs";
|
|
3
|
-
import { cp, writeFile } from "node:fs/promises";
|
|
47
|
+
import { createWriteStream, rmSync } from "node:fs";
|
|
48
|
+
import { cp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
4
49
|
import { tmpdir } from "node:os";
|
|
5
|
-
import
|
|
6
|
-
import { DEFAULT_COMPRESSION_LEVEL, ZipFS } from "@yarnpkg/libzip";
|
|
50
|
+
import * as util from "node:util";
|
|
7
51
|
import { Mutex } from "async-mutex";
|
|
8
52
|
import { XMLBuilder, XMLParser } from "fast-xml-parser";
|
|
9
53
|
import memoize from "mem";
|
|
10
54
|
import { lookup } from "mime-types";
|
|
11
55
|
import { nanoid } from "nanoid";
|
|
56
|
+
import yauzl from "yauzl";
|
|
57
|
+
import { ZipFile } from "yazl";
|
|
58
|
+
import { dirname, join, resolve } from "@storyteller-platform/path";
|
|
59
|
+
function promisify(api) {
|
|
60
|
+
return function(arg, options) {
|
|
61
|
+
return new Promise(function(resolve2, reject) {
|
|
62
|
+
api(arg, options, function(err, response) {
|
|
63
|
+
if (err) {
|
|
64
|
+
reject(err);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
resolve2(response);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const unzipFromBuffer = promisify(
|
|
73
|
+
(arg, options, callback) => {
|
|
74
|
+
yauzl.fromBuffer(arg, options, callback);
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
const unzipFromPath = promisify(
|
|
78
|
+
(arg, options, callback) => {
|
|
79
|
+
yauzl.open(arg, options, callback);
|
|
80
|
+
}
|
|
81
|
+
);
|
|
12
82
|
class Epub {
|
|
13
|
-
constructor(
|
|
14
|
-
this.
|
|
15
|
-
this.zipPath = zipPath;
|
|
83
|
+
constructor(extractPath, inputPath) {
|
|
84
|
+
this.extractPath = extractPath;
|
|
16
85
|
this.inputPath = inputPath;
|
|
17
86
|
this.readXhtmlItemContents = memoize(
|
|
18
87
|
this.readXhtmlItemContents.bind(this),
|
|
@@ -189,11 +258,10 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
189
258
|
creators,
|
|
190
259
|
contributors
|
|
191
260
|
}, additionalMetadata = []) {
|
|
192
|
-
const
|
|
261
|
+
const extractPath = join(
|
|
193
262
|
tmpdir(),
|
|
194
|
-
`storyteller-platform-epub-${randomUUID()}
|
|
263
|
+
`storyteller-platform-epub-${randomUUID()}`
|
|
195
264
|
);
|
|
196
|
-
const zipFs = new ZipFS(npath.toPortablePath(tmp), { create: true });
|
|
197
265
|
const encoder = new TextEncoder();
|
|
198
266
|
const container = encoder.encode(`<?xml version="1.0"?>
|
|
199
267
|
<container>
|
|
@@ -202,11 +270,8 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
202
270
|
</rootfiles>
|
|
203
271
|
</container>
|
|
204
272
|
`);
|
|
205
|
-
await
|
|
206
|
-
await
|
|
207
|
-
ppath.join(PortablePath.root, "META-INF", "container.xml"),
|
|
208
|
-
container
|
|
209
|
-
);
|
|
273
|
+
await mkdir(join(extractPath, "META-INF"), { recursive: true });
|
|
274
|
+
await writeFile(join(extractPath, "META-INF", "container.xml"), container);
|
|
210
275
|
const packageDocument = encoder.encode(`<?xml version="1.0"?>
|
|
211
276
|
<package unique-identifier="pub-id" dir="${language.textInfo.direction}" xml:lang=${language.toString()} version="3.0">
|
|
212
277
|
<metadata>
|
|
@@ -217,12 +282,9 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
217
282
|
</spine>
|
|
218
283
|
</package>
|
|
219
284
|
`);
|
|
220
|
-
await
|
|
221
|
-
await
|
|
222
|
-
|
|
223
|
-
packageDocument
|
|
224
|
-
);
|
|
225
|
-
const epub = new this(zipFs, tmp, path);
|
|
285
|
+
await mkdir(join(extractPath, "OEBPS"));
|
|
286
|
+
await writeFile(join(extractPath, "OEBPS", "content.opf"), packageDocument);
|
|
287
|
+
const epub = new this(extractPath, path);
|
|
226
288
|
const metadata = [
|
|
227
289
|
{
|
|
228
290
|
id: "pub-id",
|
|
@@ -260,35 +322,66 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
260
322
|
*/
|
|
261
323
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
262
324
|
static async from(pathOrData) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
325
|
+
var _stack = [];
|
|
326
|
+
try {
|
|
327
|
+
const extractPath = join(
|
|
328
|
+
tmpdir(),
|
|
329
|
+
`storyteller-platform-epub-${randomUUID()}.epub`
|
|
330
|
+
);
|
|
331
|
+
const zipfile = typeof pathOrData === "string" ? await unzipFromPath(pathOrData, { lazyEntries: true }) : await unzipFromBuffer(Buffer.from(pathOrData), { lazyEntries: true });
|
|
332
|
+
const stack = __using(_stack, new DisposableStack());
|
|
333
|
+
stack.defer(() => {
|
|
334
|
+
zipfile.close();
|
|
335
|
+
});
|
|
336
|
+
const { promise, resolve: resolve2 } = Promise.withResolvers();
|
|
337
|
+
zipfile.on("end", () => {
|
|
338
|
+
resolve2();
|
|
339
|
+
});
|
|
340
|
+
const openReadStream = util.promisify(zipfile.openReadStream.bind(zipfile));
|
|
341
|
+
zipfile.readEntry();
|
|
342
|
+
zipfile.on("entry", async (entry) => {
|
|
343
|
+
if (entry.fileName.endsWith("/")) {
|
|
344
|
+
zipfile.readEntry();
|
|
345
|
+
} else {
|
|
346
|
+
const writePath = join(extractPath, entry.fileName);
|
|
347
|
+
const readStream = await openReadStream(entry);
|
|
348
|
+
await mkdir(dirname(writePath), { recursive: true });
|
|
349
|
+
await new Promise((resolvePipe) => {
|
|
350
|
+
const writePath2 = join(extractPath, entry.fileName);
|
|
351
|
+
const writeStream = createWriteStream(writePath2);
|
|
352
|
+
writeStream.on("finish", () => {
|
|
353
|
+
resolvePipe();
|
|
354
|
+
});
|
|
355
|
+
readStream.pipe(writeStream);
|
|
356
|
+
}).finally(() => {
|
|
357
|
+
zipfile.readEntry();
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
await promise;
|
|
362
|
+
return new this(
|
|
363
|
+
extractPath,
|
|
364
|
+
typeof pathOrData === "string" ? pathOrData : void 0
|
|
365
|
+
);
|
|
366
|
+
} catch (_) {
|
|
367
|
+
var _error = _, _hasError = true;
|
|
368
|
+
} finally {
|
|
369
|
+
__callDispose(_stack, _error, _hasError);
|
|
271
370
|
}
|
|
272
|
-
const zipFs = new ZipFS(npath.toPortablePath(tmp));
|
|
273
|
-
return new this(
|
|
274
|
-
zipFs,
|
|
275
|
-
tmp,
|
|
276
|
-
typeof pathOrData === "string" ? pathOrData : void 0
|
|
277
|
-
);
|
|
278
371
|
}
|
|
279
372
|
async removeEntry(href) {
|
|
280
373
|
const rootfile = await this.getRootfile();
|
|
281
374
|
const filename = this.resolveHref(rootfile, href);
|
|
282
|
-
await
|
|
375
|
+
await rm(filename);
|
|
283
376
|
}
|
|
284
377
|
async getFileData(path, encoding) {
|
|
285
|
-
return await
|
|
378
|
+
return await readFile(path, encoding);
|
|
286
379
|
}
|
|
287
380
|
async getRootfile() {
|
|
288
381
|
var _a;
|
|
289
382
|
if (this.rootfile !== null) return this.rootfile;
|
|
290
383
|
const containerString = await this.getFileData(
|
|
291
|
-
|
|
384
|
+
join(this.extractPath, "META-INF", "container.xml"),
|
|
292
385
|
"utf-8"
|
|
293
386
|
);
|
|
294
387
|
if (!containerString)
|
|
@@ -320,7 +413,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
320
413
|
throw new Error(
|
|
321
414
|
"Failed to parse EPUB container.xml: Found no rootfile element"
|
|
322
415
|
);
|
|
323
|
-
this.rootfile =
|
|
416
|
+
this.rootfile = resolve(this.extractPath, fullPath);
|
|
324
417
|
return this.rootfile;
|
|
325
418
|
}
|
|
326
419
|
migratePackageDocument(packageDocument) {
|
|
@@ -803,7 +896,11 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
803
896
|
if (!primaryLanguage) return null;
|
|
804
897
|
const locale = primaryLanguage.value;
|
|
805
898
|
if (!locale || locale.toLowerCase() === "und") return null;
|
|
806
|
-
|
|
899
|
+
try {
|
|
900
|
+
return new Intl.Locale(locale);
|
|
901
|
+
} catch {
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
807
904
|
}
|
|
808
905
|
/**
|
|
809
906
|
* Update the Epub's language metadata entry.
|
|
@@ -1443,8 +1540,8 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1443
1540
|
* Returns a Zip Entry path for an HREF
|
|
1444
1541
|
*/
|
|
1445
1542
|
resolveHref(from, href) {
|
|
1446
|
-
const startPath =
|
|
1447
|
-
return
|
|
1543
|
+
const startPath = dirname(from);
|
|
1544
|
+
return resolve(this.extractPath, startPath, href);
|
|
1448
1545
|
}
|
|
1449
1546
|
async readItemContents(id, encoding) {
|
|
1450
1547
|
const rootfile = await this.getRootfile();
|
|
@@ -1490,8 +1587,8 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1490
1587
|
return Epub.getXhtmlTextContent(body);
|
|
1491
1588
|
}
|
|
1492
1589
|
async writeEntryContents(path, contents, encoding) {
|
|
1493
|
-
await
|
|
1494
|
-
await
|
|
1590
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1591
|
+
await writeFile(path, contents, encoding);
|
|
1495
1592
|
}
|
|
1496
1593
|
async writeItemContents(id, contents, encoding) {
|
|
1497
1594
|
const rootfile = await this.getRootfile();
|
|
@@ -1581,8 +1678,8 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1581
1678
|
contents
|
|
1582
1679
|
)
|
|
1583
1680
|
) : contents;
|
|
1584
|
-
await
|
|
1585
|
-
await
|
|
1681
|
+
await mkdir(dirname(filename), { recursive: true });
|
|
1682
|
+
await writeFile(filename, data);
|
|
1586
1683
|
}
|
|
1587
1684
|
/**
|
|
1588
1685
|
* Update the manifest entry for an existing item.
|
|
@@ -1733,11 +1830,10 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1733
1830
|
});
|
|
1734
1831
|
}
|
|
1735
1832
|
discardAndClose() {
|
|
1736
|
-
this.zipFs.discardAndClose();
|
|
1737
1833
|
this.rootfile = null;
|
|
1738
1834
|
this.manifest = null;
|
|
1739
1835
|
this.spine = null;
|
|
1740
|
-
rmSync(this.
|
|
1836
|
+
rmSync(this.extractPath, { recursive: true, force: true });
|
|
1741
1837
|
}
|
|
1742
1838
|
/**
|
|
1743
1839
|
* Write the current contents of the Epub to a new
|
|
@@ -1748,32 +1844,62 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1748
1844
|
* timestamp.
|
|
1749
1845
|
*/
|
|
1750
1846
|
async saveAndClose() {
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
(entry) => entry.properties["property"] === "dcterms:modified",
|
|
1756
|
-
{
|
|
1757
|
-
type: "meta",
|
|
1758
|
-
properties: { property: "dcterms:modified" },
|
|
1759
|
-
// We need UTC with integer seconds, but toISOString gives UTC with ms
|
|
1760
|
-
value: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+/, "")
|
|
1847
|
+
var _stack = [];
|
|
1848
|
+
try {
|
|
1849
|
+
if (!this.inputPath) {
|
|
1850
|
+
throw new Error("In-memory EPUB files cannot be saved to disk");
|
|
1761
1851
|
}
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1852
|
+
await this.replaceMetadata(
|
|
1853
|
+
(entry) => entry.properties["property"] === "dcterms:modified",
|
|
1854
|
+
{
|
|
1855
|
+
type: "meta",
|
|
1856
|
+
properties: { property: "dcterms:modified" },
|
|
1857
|
+
// We need UTC with integer seconds, but toISOString gives UTC with ms
|
|
1858
|
+
value: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+/, "")
|
|
1859
|
+
}
|
|
1860
|
+
);
|
|
1861
|
+
const tmpArchivePath = join(
|
|
1862
|
+
tmpdir(),
|
|
1863
|
+
`storyteller-platform-epub-${randomUUID()}`
|
|
1864
|
+
);
|
|
1865
|
+
const { promise, resolve: resolve2 } = Promise.withResolvers();
|
|
1866
|
+
const zipfile = new ZipFile();
|
|
1867
|
+
const writeStream = createWriteStream(tmpArchivePath);
|
|
1868
|
+
writeStream.on("close", () => {
|
|
1869
|
+
resolve2();
|
|
1870
|
+
});
|
|
1871
|
+
const stack = __using(_stack, new AsyncDisposableStack(), true);
|
|
1872
|
+
stack.defer(async () => {
|
|
1873
|
+
writeStream.close();
|
|
1874
|
+
await rm(tmpArchivePath, { force: true });
|
|
1875
|
+
});
|
|
1876
|
+
zipfile.outputStream.pipe(writeStream);
|
|
1877
|
+
zipfile.addBuffer(Buffer.from("application/epub+zip"), "mimetype", {
|
|
1878
|
+
compress: false
|
|
1879
|
+
});
|
|
1880
|
+
const entries = await readdir(this.extractPath, {
|
|
1881
|
+
recursive: true,
|
|
1882
|
+
withFileTypes: true
|
|
1883
|
+
});
|
|
1884
|
+
for (const entry of entries) {
|
|
1885
|
+
if (entry.name === "mimetype" || entry.isDirectory()) continue;
|
|
1886
|
+
zipfile.addFile(
|
|
1887
|
+
join(entry.parentPath, entry.name),
|
|
1888
|
+
join(entry.parentPath, entry.name).replace(`${this.extractPath}/`, "")
|
|
1889
|
+
);
|
|
1890
|
+
}
|
|
1891
|
+
zipfile.end();
|
|
1892
|
+
await promise;
|
|
1893
|
+
await cp(tmpArchivePath, this.inputPath);
|
|
1894
|
+
} catch (_) {
|
|
1895
|
+
var _error = _, _hasError = true;
|
|
1896
|
+
} finally {
|
|
1897
|
+
var _promise = __callDispose(_stack, _error, _hasError);
|
|
1898
|
+
_promise && await _promise;
|
|
1899
|
+
}
|
|
1772
1900
|
}
|
|
1773
1901
|
[Symbol.dispose]() {
|
|
1774
|
-
|
|
1775
|
-
this.discardAndClose();
|
|
1776
|
-
}
|
|
1902
|
+
this.discardAndClose();
|
|
1777
1903
|
}
|
|
1778
1904
|
}
|
|
1779
1905
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@storyteller-platform/epub",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"module": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"@tsconfig/strictest": "^2.0.5",
|
|
33
33
|
"@types/mime-types": "^2",
|
|
34
34
|
"@types/node": "^24.0.0",
|
|
35
|
+
"@types/yazl": "^3",
|
|
35
36
|
"markdown-toc": "^1.2.0",
|
|
36
37
|
"remark-toc": "^9.0.0",
|
|
37
38
|
"tsup": "^8.5.0",
|
|
@@ -44,14 +45,15 @@
|
|
|
44
45
|
"dependencies": {
|
|
45
46
|
"@storyteller-platform/fs": "^0.1.3",
|
|
46
47
|
"@storyteller-platform/path": "^0.1.1",
|
|
47
|
-
"@
|
|
48
|
-
"@yarnpkg/libzip": "^3.2.2",
|
|
48
|
+
"@types/yauzl": "^2.10.3",
|
|
49
49
|
"@zip.js/zip.js": "^2.0.0",
|
|
50
50
|
"async-mutex": "^0.5.0",
|
|
51
51
|
"fast-xml-parser": "^4.0.0",
|
|
52
52
|
"mem": "^8.0.0",
|
|
53
53
|
"mime-types": "^3.0.1",
|
|
54
|
-
"nanoid": "^5.1.5"
|
|
54
|
+
"nanoid": "^5.1.5",
|
|
55
|
+
"yauzl": "^3.2.0",
|
|
56
|
+
"yazl": "^3.3.1"
|
|
55
57
|
},
|
|
56
58
|
"publishConfig": {
|
|
57
59
|
"access": "public",
|