@storyteller-platform/epub 0.2.1 → 0.4.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 +270 -473
- package/dist/index.cjs +112 -133
- package/dist/index.d.cts +46 -33
- package/dist/index.d.ts +46 -33
- package/dist/index.js +112 -139
- package/package.json +25 -29
- package/dist/node.cjs +0 -66
- package/dist/node.d.cts +0 -28
- package/dist/node.d.ts +0 -28
- package/dist/node.js +0 -42
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NativePath } from '@yarnpkg/fslib';
|
|
2
|
+
import { ZipFS } from '@yarnpkg/libzip';
|
|
2
3
|
import { XMLParser, XMLBuilder } from 'fast-xml-parser';
|
|
3
4
|
|
|
4
5
|
declare global {
|
|
@@ -14,9 +15,10 @@ type Letter = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" |
|
|
|
14
15
|
type QuestionMark = "?";
|
|
15
16
|
/** A valid name for an XML element (must start with a letter) */
|
|
16
17
|
type ElementName = `${Letter | Uppercase<Letter> | QuestionMark}${string}`;
|
|
18
|
+
type PropertyPrefix = "@_";
|
|
17
19
|
/** An XML element */
|
|
18
20
|
type XmlElement<Name extends ElementName = ElementName> = {
|
|
19
|
-
":@"?: Record
|
|
21
|
+
":@"?: Record<`${PropertyPrefix}${string}`, string>;
|
|
20
22
|
} & {
|
|
21
23
|
[key in Name]: ParsedXml;
|
|
22
24
|
};
|
|
@@ -36,17 +38,6 @@ type ManifestItem = {
|
|
|
36
38
|
mediaOverlay?: string | undefined;
|
|
37
39
|
properties?: string[] | undefined;
|
|
38
40
|
};
|
|
39
|
-
declare class EpubEntry {
|
|
40
|
-
filename: string;
|
|
41
|
-
private entry;
|
|
42
|
-
private data;
|
|
43
|
-
getData(): Promise<Uint8Array<ArrayBufferLike>>;
|
|
44
|
-
setData(data: Uint8Array): void;
|
|
45
|
-
constructor(entry: Entry | {
|
|
46
|
-
filename: string;
|
|
47
|
-
data: Uint8Array;
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
41
|
type MetadataEntry = {
|
|
51
42
|
id?: string | undefined;
|
|
52
43
|
type: ElementName;
|
|
@@ -94,7 +85,7 @@ type PackageElement = XmlElement<"package"> | XmlElement<"opf:package">;
|
|
|
94
85
|
* Example usage:
|
|
95
86
|
*
|
|
96
87
|
* ```ts
|
|
97
|
-
* import { Epub, getBody, findByName, textContent } from '@
|
|
88
|
+
* import { Epub, getBody, findByName, textContent } from '@storyteller-platform/epub';
|
|
98
89
|
*
|
|
99
90
|
* const epub = await Epub.from('./path/to/book.epub');
|
|
100
91
|
* const title = await epub.getTitle();
|
|
@@ -114,8 +105,9 @@ type PackageElement = XmlElement<"package"> | XmlElement<"opf:package">;
|
|
|
114
105
|
* @link https://www.w3.org/TR/epub-33/
|
|
115
106
|
*/
|
|
116
107
|
declare class Epub {
|
|
117
|
-
|
|
118
|
-
|
|
108
|
+
protected zipFs: ZipFS;
|
|
109
|
+
protected zipPath: NativePath;
|
|
110
|
+
protected inputPath: string | undefined;
|
|
119
111
|
static xmlParser: XMLParser;
|
|
120
112
|
static xhtmlParser: XMLParser;
|
|
121
113
|
static xmlBuilder: XMLBuilder;
|
|
@@ -171,18 +163,11 @@ declare class Epub {
|
|
|
171
163
|
* a text node or an XML element.
|
|
172
164
|
*/
|
|
173
165
|
static isXmlTextNode(node: XmlNode): node is XmlTextNode;
|
|
174
|
-
private zipWriter;
|
|
175
|
-
private dataWriter;
|
|
176
166
|
private rootfile;
|
|
177
167
|
private manifest;
|
|
178
168
|
private spine;
|
|
179
169
|
private packageMutex;
|
|
180
|
-
protected constructor(
|
|
181
|
-
/**
|
|
182
|
-
* Close the Epub. Must be called before the Epub goes out
|
|
183
|
-
* of scope/is garbage collected.
|
|
184
|
-
*/
|
|
185
|
-
close(): Promise<void>;
|
|
170
|
+
protected constructor(zipFs: ZipFS, zipPath: NativePath, inputPath: string | undefined);
|
|
186
171
|
/**
|
|
187
172
|
* Construct an Epub instance, optionally beginning
|
|
188
173
|
* with the provided metadata.
|
|
@@ -190,7 +175,7 @@ declare class Epub {
|
|
|
190
175
|
* @param dublinCore Core metadata terms
|
|
191
176
|
* @param additionalMetadata An array of additional metadata entries
|
|
192
177
|
*/
|
|
193
|
-
static create({ title, language, identifier, date, subjects, type, creators, contributors, }: DublinCore, additionalMetadata?: EpubMetadata): Promise<Epub>;
|
|
178
|
+
static create(path: string, { title, language, identifier, date, subjects, type, creators, contributors, }: DublinCore, additionalMetadata?: EpubMetadata): Promise<Epub>;
|
|
194
179
|
/**
|
|
195
180
|
* Construct an Epub instance by reading an existing EPUB
|
|
196
181
|
* publication.
|
|
@@ -200,7 +185,6 @@ declare class Epub {
|
|
|
200
185
|
* the data of the EPUB publication.
|
|
201
186
|
*/
|
|
202
187
|
static from(pathOrData: string | Uint8Array): Promise<Epub>;
|
|
203
|
-
private getEntry;
|
|
204
188
|
private removeEntry;
|
|
205
189
|
private getFileData;
|
|
206
190
|
private getRootfile;
|
|
@@ -277,6 +261,24 @@ declare class Epub {
|
|
|
277
261
|
* @link https://www.w3.org/TR/epub-33/#sec-pkg-metadata
|
|
278
262
|
*/
|
|
279
263
|
getMetadata(): Promise<EpubMetadata>;
|
|
264
|
+
/**
|
|
265
|
+
* Retrieve the identifier from the dc:identifier element
|
|
266
|
+
* in the EPUB metadata.
|
|
267
|
+
*
|
|
268
|
+
* If there is no dc:identifier element, returns null.
|
|
269
|
+
*
|
|
270
|
+
* @link https://www.w3.org/TR/epub-33/#sec-opf-dcidentifier
|
|
271
|
+
*/
|
|
272
|
+
getIdentifier(): Promise<string | null>;
|
|
273
|
+
/**
|
|
274
|
+
* Set the dc:identifier metadata element with the provided string.
|
|
275
|
+
*
|
|
276
|
+
* Updates the existing dc:identifier element if one exists.
|
|
277
|
+
* Otherwise creates a new element
|
|
278
|
+
*
|
|
279
|
+
* @link https://www.w3.org/TR/epub-33/#sec-opf-dcidentifier
|
|
280
|
+
*/
|
|
281
|
+
setIdentifier(identifier: string): Promise<void>;
|
|
280
282
|
/**
|
|
281
283
|
* Even "EPUB 3" publications sometimes still only use the
|
|
282
284
|
* EPUB 2 specification for identifying the cover image.
|
|
@@ -601,7 +603,7 @@ declare class Epub {
|
|
|
601
603
|
* @param head Optional - the XMl nodes to place in the head
|
|
602
604
|
* @param language Optional - defaults to the EPUB's language
|
|
603
605
|
*/
|
|
604
|
-
createXhtmlDocument(body: ParsedXml, head?: ParsedXml, language?: Intl.Locale): Promise<(XmlElement<"
|
|
606
|
+
createXhtmlDocument(body: ParsedXml, head?: ParsedXml, language?: Intl.Locale): Promise<(XmlElement<"html"> | XmlElement<"?xml">)[]>;
|
|
605
607
|
/**
|
|
606
608
|
* Retrieves the contents of an XHTML item, given its manifest id.
|
|
607
609
|
*
|
|
@@ -697,18 +699,29 @@ declare class Epub {
|
|
|
697
699
|
*/
|
|
698
700
|
replaceMetadata(predicate: (entry: MetadataEntry) => boolean, entry: MetadataEntry): Promise<void>;
|
|
699
701
|
/**
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
+
* Remove one or more metadata entries.
|
|
703
|
+
*
|
|
704
|
+
* The `predicate` argument will be used to determine which entries
|
|
705
|
+
* to remove. The all metadata entries that match the
|
|
706
|
+
* predicate will be removed.
|
|
707
|
+
*
|
|
708
|
+
* @param predicate Calls predicate once for each metadata entry,
|
|
709
|
+
* removing any for which it returns true
|
|
702
710
|
*
|
|
703
|
-
*
|
|
704
|
-
|
|
705
|
-
|
|
711
|
+
* @link https://www.w3.org/TR/epub-33/#sec-pkg-metadata
|
|
712
|
+
*/
|
|
713
|
+
removeMetadata(predicate: (entry: MetadataEntry) => boolean): Promise<void>;
|
|
714
|
+
discardAndClose(): void;
|
|
715
|
+
/**
|
|
716
|
+
* Write the current contents of the Epub to a new
|
|
717
|
+
* EPUB archive on disk.
|
|
706
718
|
*
|
|
707
719
|
* When this method is called, the "dcterms:modified"
|
|
708
720
|
* meta tag is automatically updated to the current UTC
|
|
709
721
|
* timestamp.
|
|
710
722
|
*/
|
|
711
|
-
|
|
723
|
+
saveAndClose(): Promise<void>;
|
|
724
|
+
[Symbol.dispose](): void;
|
|
712
725
|
}
|
|
713
726
|
|
|
714
727
|
export { type AlternateScript, type Collection, type DcCreator, type DcSubject, type DublinCore, type ElementName, Epub, type EpubMetadata, type ManifestItem, type MetadataEntry, type PackageElement, type ParsedXml, type XmlElement, type XmlNode, type XmlTextNode };
|
package/dist/index.js
CHANGED
|
@@ -1,45 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from "@zip.js/zip.js";
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { rmSync } from "node:fs";
|
|
3
|
+
import { cp, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { PortablePath, npath, ppath } from "@yarnpkg/fslib";
|
|
6
|
+
import { DEFAULT_COMPRESSION_LEVEL, ZipFS } from "@yarnpkg/libzip";
|
|
8
7
|
import { Mutex } from "async-mutex";
|
|
9
8
|
import { XMLBuilder, XMLParser } from "fast-xml-parser";
|
|
10
9
|
import memoize from "mem";
|
|
11
10
|
import { lookup } from "mime-types";
|
|
12
11
|
import { nanoid } from "nanoid";
|
|
13
|
-
import { dirname, resolve } from "@storyteller-platform/path";
|
|
14
|
-
class EpubEntry {
|
|
15
|
-
filename;
|
|
16
|
-
entry = null;
|
|
17
|
-
data = null;
|
|
18
|
-
async getData() {
|
|
19
|
-
if (this.data) return this.data;
|
|
20
|
-
const writer = new Uint8ArrayWriter();
|
|
21
|
-
const data = await this.entry.getData(writer);
|
|
22
|
-
this.data = data;
|
|
23
|
-
return this.data;
|
|
24
|
-
}
|
|
25
|
-
setData(data) {
|
|
26
|
-
this.data = data;
|
|
27
|
-
}
|
|
28
|
-
constructor(entry) {
|
|
29
|
-
this.filename = entry.filename;
|
|
30
|
-
if ("data" in entry) {
|
|
31
|
-
this.data = entry.data;
|
|
32
|
-
} else {
|
|
33
|
-
this.entry = entry;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
12
|
class Epub {
|
|
38
|
-
constructor(
|
|
39
|
-
this.
|
|
40
|
-
this.
|
|
41
|
-
this.
|
|
42
|
-
this.zipWriter = new ZipWriter(this.dataWriter);
|
|
13
|
+
constructor(zipFs, zipPath, inputPath) {
|
|
14
|
+
this.zipFs = zipFs;
|
|
15
|
+
this.zipPath = zipPath;
|
|
16
|
+
this.inputPath = inputPath;
|
|
43
17
|
this.readXhtmlItemContents = memoize(
|
|
44
18
|
this.readXhtmlItemContents.bind(this),
|
|
45
19
|
// This isn't unnecessary, the generic here just isn't handling the
|
|
@@ -194,21 +168,10 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
194
168
|
static isXmlTextNode(node) {
|
|
195
169
|
return "#text" in node;
|
|
196
170
|
}
|
|
197
|
-
zipWriter;
|
|
198
|
-
dataWriter;
|
|
199
171
|
rootfile = null;
|
|
200
172
|
manifest = null;
|
|
201
173
|
spine = null;
|
|
202
174
|
packageMutex = new Mutex();
|
|
203
|
-
/**
|
|
204
|
-
* Close the Epub. Must be called before the Epub goes out
|
|
205
|
-
* of scope/is garbage collected.
|
|
206
|
-
*/
|
|
207
|
-
async close() {
|
|
208
|
-
var _a;
|
|
209
|
-
await ((_a = this.onClose) == null ? void 0 : _a.call(this));
|
|
210
|
-
await this.zipWriter.close();
|
|
211
|
-
}
|
|
212
175
|
/**
|
|
213
176
|
* Construct an Epub instance, optionally beginning
|
|
214
177
|
* with the provided metadata.
|
|
@@ -216,7 +179,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
216
179
|
* @param dublinCore Core metadata terms
|
|
217
180
|
* @param additionalMetadata An array of additional metadata entries
|
|
218
181
|
*/
|
|
219
|
-
static async create({
|
|
182
|
+
static async create(path, {
|
|
220
183
|
title,
|
|
221
184
|
language,
|
|
222
185
|
identifier,
|
|
@@ -226,7 +189,11 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
226
189
|
creators,
|
|
227
190
|
contributors
|
|
228
191
|
}, additionalMetadata = []) {
|
|
229
|
-
const
|
|
192
|
+
const tmp = npath.join(
|
|
193
|
+
tmpdir(),
|
|
194
|
+
`storyteller-platform-epub-${randomUUID()}.epub`
|
|
195
|
+
);
|
|
196
|
+
const zipFs = new ZipFS(npath.toPortablePath(tmp), { create: true });
|
|
230
197
|
const encoder = new TextEncoder();
|
|
231
198
|
const container = encoder.encode(`<?xml version="1.0"?>
|
|
232
199
|
<container>
|
|
@@ -235,8 +202,10 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
235
202
|
</rootfiles>
|
|
236
203
|
</container>
|
|
237
204
|
`);
|
|
238
|
-
|
|
239
|
-
|
|
205
|
+
await zipFs.mkdirpPromise(ppath.join(PortablePath.root, "META-INF"));
|
|
206
|
+
await zipFs.writeFilePromise(
|
|
207
|
+
ppath.join(PortablePath.root, "META-INF", "container.xml"),
|
|
208
|
+
container
|
|
240
209
|
);
|
|
241
210
|
const packageDocument = encoder.encode(`<?xml version="1.0"?>
|
|
242
211
|
<package unique-identifier="pub-id" dir="${language.textInfo.direction}" xml:lang=${language.toString()} version="3.0">
|
|
@@ -248,10 +217,12 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
248
217
|
</spine>
|
|
249
218
|
</package>
|
|
250
219
|
`);
|
|
251
|
-
|
|
252
|
-
|
|
220
|
+
await zipFs.mkdirpPromise(ppath.join(PortablePath.root, "OEBPS"));
|
|
221
|
+
await zipFs.writeFilePromise(
|
|
222
|
+
ppath.join(PortablePath.root, "OEBPS", "content.opf"),
|
|
223
|
+
packageDocument
|
|
253
224
|
);
|
|
254
|
-
const epub = new this(
|
|
225
|
+
const epub = new this(zipFs, tmp, path);
|
|
255
226
|
const metadata = [
|
|
256
227
|
{
|
|
257
228
|
id: "pub-id",
|
|
@@ -287,42 +258,37 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
287
258
|
* path to an EPUB file on disk, or a Uint8Array representing
|
|
288
259
|
* the data of the EPUB publication.
|
|
289
260
|
*/
|
|
261
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
290
262
|
static async from(pathOrData) {
|
|
291
|
-
|
|
292
|
-
|
|
263
|
+
const tmp = npath.join(
|
|
264
|
+
tmpdir(),
|
|
265
|
+
`storyteller-platform-epub-${randomUUID()}.epub`
|
|
266
|
+
);
|
|
267
|
+
if (typeof pathOrData !== "string") {
|
|
268
|
+
await writeFile(tmp, pathOrData);
|
|
269
|
+
} else {
|
|
270
|
+
await cp(pathOrData, tmp);
|
|
293
271
|
}
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
return epub;
|
|
301
|
-
}
|
|
302
|
-
getEntry(path) {
|
|
303
|
-
return this.entries.find((entry) => entry.filename === path);
|
|
272
|
+
const zipFs = new ZipFS(npath.toPortablePath(tmp));
|
|
273
|
+
return new this(
|
|
274
|
+
zipFs,
|
|
275
|
+
tmp,
|
|
276
|
+
typeof pathOrData === "string" ? pathOrData : void 0
|
|
277
|
+
);
|
|
304
278
|
}
|
|
305
279
|
async removeEntry(href) {
|
|
306
280
|
const rootfile = await this.getRootfile();
|
|
307
281
|
const filename = this.resolveHref(rootfile, href);
|
|
308
|
-
|
|
309
|
-
if (index === -1) return;
|
|
310
|
-
this.entries.splice(index, 1);
|
|
282
|
+
await this.zipFs.removePromise(filename);
|
|
311
283
|
}
|
|
312
284
|
async getFileData(path, encoding) {
|
|
313
|
-
|
|
314
|
-
if (!containerEntry)
|
|
315
|
-
throw new Error(
|
|
316
|
-
`Could not get file data for entry ${path}: entry not found`
|
|
317
|
-
);
|
|
318
|
-
const containerContents = await containerEntry.getData();
|
|
319
|
-
return encoding === "utf-8" ? new TextDecoder("utf-8").decode(containerContents) : containerContents;
|
|
285
|
+
return await this.zipFs.readFilePromise(path, encoding);
|
|
320
286
|
}
|
|
321
287
|
async getRootfile() {
|
|
322
288
|
var _a;
|
|
323
289
|
if (this.rootfile !== null) return this.rootfile;
|
|
324
290
|
const containerString = await this.getFileData(
|
|
325
|
-
"META-INF
|
|
291
|
+
ppath.join(PortablePath.root, "META-INF", "container.xml"),
|
|
326
292
|
"utf-8"
|
|
327
293
|
);
|
|
328
294
|
if (!containerString)
|
|
@@ -349,11 +315,12 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
349
315
|
return !Epub.isXmlTextNode(node) && ((_a2 = node[":@"]) == null ? void 0 : _a2["@_media-type"]) === "application/oebps-package+xml";
|
|
350
316
|
}
|
|
351
317
|
);
|
|
352
|
-
|
|
318
|
+
const fullPath = (_a = rootfile == null ? void 0 : rootfile[":@"]) == null ? void 0 : _a["@_full-path"];
|
|
319
|
+
if (!fullPath)
|
|
353
320
|
throw new Error(
|
|
354
321
|
"Failed to parse EPUB container.xml: Found no rootfile element"
|
|
355
322
|
);
|
|
356
|
-
this.rootfile =
|
|
323
|
+
this.rootfile = ppath.resolve(PortablePath.root, fullPath);
|
|
357
324
|
return this.rootfile;
|
|
358
325
|
}
|
|
359
326
|
migratePackageDocument(packageDocument) {
|
|
@@ -416,7 +383,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
416
383
|
produced ?? packageDocument
|
|
417
384
|
);
|
|
418
385
|
const rootfile = await this.getRootfile();
|
|
419
|
-
this.writeEntryContents(rootfile, updatedPackageDocument, "utf-8");
|
|
386
|
+
await this.writeEntryContents(rootfile, updatedPackageDocument, "utf-8");
|
|
420
387
|
});
|
|
421
388
|
}
|
|
422
389
|
/**
|
|
@@ -555,6 +522,34 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
555
522
|
const metadata = metadataElement.metadata.map((node) => Epub.parseMetadataItem(node)).filter((node) => !!node);
|
|
556
523
|
return metadata;
|
|
557
524
|
}
|
|
525
|
+
/**
|
|
526
|
+
* Retrieve the identifier from the dc:identifier element
|
|
527
|
+
* in the EPUB metadata.
|
|
528
|
+
*
|
|
529
|
+
* If there is no dc:identifier element, returns null.
|
|
530
|
+
*
|
|
531
|
+
* @link https://www.w3.org/TR/epub-33/#sec-opf-dcidentifier
|
|
532
|
+
*/
|
|
533
|
+
async getIdentifier() {
|
|
534
|
+
const metadata = await this.getMetadata();
|
|
535
|
+
const entry = metadata.find(({ type }) => type === "dc:identifier");
|
|
536
|
+
return (entry == null ? void 0 : entry.value) ?? null;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Set the dc:identifier metadata element with the provided string.
|
|
540
|
+
*
|
|
541
|
+
* Updates the existing dc:identifier element if one exists.
|
|
542
|
+
* Otherwise creates a new element
|
|
543
|
+
*
|
|
544
|
+
* @link https://www.w3.org/TR/epub-33/#sec-opf-dcidentifier
|
|
545
|
+
*/
|
|
546
|
+
async setIdentifier(identifier) {
|
|
547
|
+
await this.replaceMetadata(({ type }) => type === "dc:identifier", {
|
|
548
|
+
type: "dc:identifier",
|
|
549
|
+
properties: {},
|
|
550
|
+
value: identifier
|
|
551
|
+
});
|
|
552
|
+
}
|
|
558
553
|
/**
|
|
559
554
|
* Even "EPUB 3" publications sometimes still only use the
|
|
560
555
|
* EPUB 2 specification for identifying the cover image.
|
|
@@ -1448,9 +1443,8 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1448
1443
|
* Returns a Zip Entry path for an HREF
|
|
1449
1444
|
*/
|
|
1450
1445
|
resolveHref(from, href) {
|
|
1451
|
-
const startPath = dirname(from);
|
|
1452
|
-
|
|
1453
|
-
return resolve(absoluteStartPath, href).slice(1);
|
|
1446
|
+
const startPath = ppath.dirname(from);
|
|
1447
|
+
return ppath.resolve(PortablePath.root, startPath, href);
|
|
1454
1448
|
}
|
|
1455
1449
|
async readItemContents(id, encoding) {
|
|
1456
1450
|
const rootfile = await this.getRootfile();
|
|
@@ -1495,11 +1489,9 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1495
1489
|
const body = Epub.getXhtmlBody(xml);
|
|
1496
1490
|
return Epub.getXhtmlTextContent(body);
|
|
1497
1491
|
}
|
|
1498
|
-
writeEntryContents(path, contents, encoding) {
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
if (!entry) throw new Error(`Could not find file at ${path} in EPUB`);
|
|
1502
|
-
entry.setData(data);
|
|
1492
|
+
async writeEntryContents(path, contents, encoding) {
|
|
1493
|
+
await this.zipFs.mkdirpPromise(ppath.dirname(path));
|
|
1494
|
+
await this.zipFs.writeFilePromise(path, contents, encoding);
|
|
1503
1495
|
}
|
|
1504
1496
|
async writeItemContents(id, contents, encoding) {
|
|
1505
1497
|
const rootfile = await this.getRootfile();
|
|
@@ -1510,9 +1502,9 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1510
1502
|
memoize.clear(this.readXhtmlItemContents);
|
|
1511
1503
|
const href = this.resolveHref(rootfile, manifestItem.href);
|
|
1512
1504
|
if (encoding === "utf-8") {
|
|
1513
|
-
this.writeEntryContents(href, contents, encoding);
|
|
1505
|
+
await this.writeEntryContents(href, contents, encoding);
|
|
1514
1506
|
} else {
|
|
1515
|
-
this.writeEntryContents(href, contents);
|
|
1507
|
+
await this.writeEntryContents(href, contents);
|
|
1516
1508
|
}
|
|
1517
1509
|
}
|
|
1518
1510
|
/**
|
|
@@ -1575,7 +1567,9 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1575
1567
|
...item.mediaType && { "media-type": item.mediaType },
|
|
1576
1568
|
...item.fallback && { fallback: item.fallback },
|
|
1577
1569
|
...item.mediaOverlay && { "media-overlay": item.mediaOverlay },
|
|
1578
|
-
...item.properties && {
|
|
1570
|
+
...item.properties && {
|
|
1571
|
+
properties: item.properties.join(" ")
|
|
1572
|
+
}
|
|
1579
1573
|
})
|
|
1580
1574
|
);
|
|
1581
1575
|
});
|
|
@@ -1587,7 +1581,8 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1587
1581
|
contents
|
|
1588
1582
|
)
|
|
1589
1583
|
) : contents;
|
|
1590
|
-
this.
|
|
1584
|
+
await this.zipFs.mkdirpPromise(ppath.dirname(filename));
|
|
1585
|
+
await this.zipFs.writeFilePromise(filename, data);
|
|
1591
1586
|
}
|
|
1592
1587
|
/**
|
|
1593
1588
|
* Update the manifest entry for an existing item.
|
|
@@ -1737,19 +1732,25 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1737
1732
|
}
|
|
1738
1733
|
});
|
|
1739
1734
|
}
|
|
1735
|
+
discardAndClose() {
|
|
1736
|
+
this.zipFs.discardAndClose();
|
|
1737
|
+
this.rootfile = null;
|
|
1738
|
+
this.manifest = null;
|
|
1739
|
+
this.spine = null;
|
|
1740
|
+
rmSync(this.zipPath, { recursive: true, force: true });
|
|
1741
|
+
}
|
|
1740
1742
|
/**
|
|
1741
1743
|
* Write the current contents of the Epub to a new
|
|
1742
|
-
*
|
|
1743
|
-
*
|
|
1744
|
-
* This _does not_ close the Epub. It can continue to
|
|
1745
|
-
* be modified after it has been written to disk. Use
|
|
1746
|
-
* `epub.close()` to close the Epub.
|
|
1744
|
+
* EPUB archive on disk.
|
|
1747
1745
|
*
|
|
1748
1746
|
* When this method is called, the "dcterms:modified"
|
|
1749
1747
|
* meta tag is automatically updated to the current UTC
|
|
1750
1748
|
* timestamp.
|
|
1751
1749
|
*/
|
|
1752
|
-
async
|
|
1750
|
+
async saveAndClose() {
|
|
1751
|
+
if (!this.inputPath) {
|
|
1752
|
+
throw new Error("In-memory EPUB files cannot be saved to disk");
|
|
1753
|
+
}
|
|
1753
1754
|
await this.replaceMetadata(
|
|
1754
1755
|
(entry) => entry.properties["property"] === "dcterms:modified",
|
|
1755
1756
|
{
|
|
@@ -1759,48 +1760,20 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1759
1760
|
value: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+/, "")
|
|
1760
1761
|
}
|
|
1761
1762
|
);
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
});
|
|
1768
|
-
this.entries.push(mimetypeEntry);
|
|
1769
|
-
}
|
|
1770
|
-
const mimetypeReader = new Uint8ArrayReader(await mimetypeEntry.getData());
|
|
1771
|
-
try {
|
|
1772
|
-
await this.zipWriter.add(mimetypeEntry.filename, mimetypeReader, {
|
|
1773
|
-
level: 0,
|
|
1774
|
-
extendedTimestamp: false
|
|
1775
|
-
});
|
|
1776
|
-
} catch (e) {
|
|
1777
|
-
if (e instanceof Error && e.message === ERR_DUPLICATED_NAME) {
|
|
1778
|
-
throw new Error(
|
|
1779
|
-
`Failed to add file "${mimetypeEntry.filename}" to zip archive: ${e.message}`
|
|
1780
|
-
);
|
|
1781
|
-
}
|
|
1782
|
-
throw e;
|
|
1783
|
-
}
|
|
1784
|
-
await Promise.all(
|
|
1785
|
-
this.entries.map(async (entry) => {
|
|
1786
|
-
if (entry.filename === "mimetype") return;
|
|
1787
|
-
const reader = new Uint8ArrayReader(await entry.getData());
|
|
1788
|
-
try {
|
|
1789
|
-
return await this.zipWriter.add(entry.filename, reader);
|
|
1790
|
-
} catch (e) {
|
|
1791
|
-
if (e instanceof Error && e.message === ERR_DUPLICATED_NAME) {
|
|
1792
|
-
throw new Error(
|
|
1793
|
-
`Failed to add file "${entry.filename}" to zip archive: ${e.message}`
|
|
1794
|
-
);
|
|
1795
|
-
}
|
|
1796
|
-
throw e;
|
|
1797
|
-
}
|
|
1798
|
-
})
|
|
1763
|
+
this.zipFs.level = 0;
|
|
1764
|
+
await this.zipFs.writeFilePromise(
|
|
1765
|
+
ppath.join(PortablePath.root, "mimetype"),
|
|
1766
|
+
"application/epub+zip",
|
|
1767
|
+
"utf-8"
|
|
1799
1768
|
);
|
|
1800
|
-
|
|
1801
|
-
this.
|
|
1802
|
-
this.
|
|
1803
|
-
|
|
1769
|
+
this.zipFs.level = DEFAULT_COMPRESSION_LEVEL;
|
|
1770
|
+
this.zipFs.saveAndClose();
|
|
1771
|
+
await cp(this.zipPath, this.inputPath, { force: true });
|
|
1772
|
+
}
|
|
1773
|
+
[Symbol.dispose]() {
|
|
1774
|
+
if (this.zipFs.ready) {
|
|
1775
|
+
this.discardAndClose();
|
|
1776
|
+
}
|
|
1804
1777
|
}
|
|
1805
1778
|
}
|
|
1806
1779
|
export {
|
package/package.json
CHANGED
|
@@ -1,34 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@storyteller-platform/epub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"module": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"exports": {
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"import": {
|
|
13
|
-
"types": "./dist/index.d.ts",
|
|
14
|
-
"default": "./dist/index.js"
|
|
15
|
-
},
|
|
16
|
-
"require": {
|
|
17
|
-
"types": "./dist/index.d.cts",
|
|
18
|
-
"default": "./dist/index.cjs"
|
|
19
|
-
}
|
|
9
|
+
"import": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
20
12
|
},
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"import": {
|
|
25
|
-
"types": "./dist/node.d.ts",
|
|
26
|
-
"default": "./dist/node.js"
|
|
27
|
-
},
|
|
28
|
-
"require": {
|
|
29
|
-
"types": "./dist/node.d.cts",
|
|
30
|
-
"default": "./dist/node.cjs"
|
|
31
|
-
}
|
|
13
|
+
"require": {
|
|
14
|
+
"types": "./dist/index.d.cts",
|
|
15
|
+
"default": "./dist/index.cjs"
|
|
32
16
|
}
|
|
33
17
|
},
|
|
34
18
|
"files": [
|
|
@@ -47,19 +31,21 @@
|
|
|
47
31
|
"@storyteller-platform/tsup": "^0.1.0",
|
|
48
32
|
"@tsconfig/strictest": "^2.0.5",
|
|
49
33
|
"@types/mime-types": "^2",
|
|
50
|
-
"@types/node": "^
|
|
34
|
+
"@types/node": "^24.0.0",
|
|
51
35
|
"markdown-toc": "^1.2.0",
|
|
52
36
|
"remark-toc": "^9.0.0",
|
|
53
37
|
"tsup": "^8.5.0",
|
|
54
38
|
"tsx": "^4.19.2",
|
|
55
|
-
"typedoc": "^0.28.
|
|
39
|
+
"typedoc": "^0.28.13",
|
|
56
40
|
"typedoc-plugin-markdown": "^4.6.3",
|
|
57
41
|
"typedoc-plugin-remark": "^1.2.0",
|
|
58
|
-
"typescript": "^5.
|
|
42
|
+
"typescript": "^5.9.2"
|
|
59
43
|
},
|
|
60
44
|
"dependencies": {
|
|
61
|
-
"@storyteller-platform/fs": "^0.1.
|
|
62
|
-
"@storyteller-platform/path": "^0.1.
|
|
45
|
+
"@storyteller-platform/fs": "^0.1.3",
|
|
46
|
+
"@storyteller-platform/path": "^0.1.1",
|
|
47
|
+
"@yarnpkg/fslib": "^3.1.3",
|
|
48
|
+
"@yarnpkg/libzip": "^3.2.2",
|
|
63
49
|
"@zip.js/zip.js": "^2.0.0",
|
|
64
50
|
"async-mutex": "^0.5.0",
|
|
65
51
|
"fast-xml-parser": "^4.0.0",
|
|
@@ -68,6 +54,16 @@
|
|
|
68
54
|
"nanoid": "^5.1.5"
|
|
69
55
|
},
|
|
70
56
|
"publishConfig": {
|
|
71
|
-
"access": "public"
|
|
57
|
+
"access": "public",
|
|
58
|
+
"exports": {
|
|
59
|
+
"import": {
|
|
60
|
+
"types": "./dist/index.d.ts",
|
|
61
|
+
"default": "./dist/index.js"
|
|
62
|
+
},
|
|
63
|
+
"require": {
|
|
64
|
+
"types": "./dist/index.d.cts",
|
|
65
|
+
"default": "./dist/index.cjs"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
72
68
|
}
|
|
73
69
|
}
|