@storyteller-platform/epub 0.2.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/dist/index.cjs ADDED
@@ -0,0 +1,1838 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var index_exports = {};
30
+ __export(index_exports, {
31
+ Epub: () => Epub
32
+ });
33
+ module.exports = __toCommonJS(index_exports);
34
+ var import_zip = require("@zip.js/zip.js");
35
+ var import_async_mutex = require("async-mutex");
36
+ var import_fast_xml_parser = require("fast-xml-parser");
37
+ var import_he = __toESM(require("he"), 1);
38
+ var import_mem = __toESM(require("mem"), 1);
39
+ var import_mime_types = require("mime-types");
40
+ var import_nanoid = require("nanoid");
41
+ var import_path = require("@storyteller-platform/path");
42
+ class EpubEntry {
43
+ filename;
44
+ entry = null;
45
+ data = null;
46
+ async getData() {
47
+ if (this.data) return this.data;
48
+ const writer = new import_zip.Uint8ArrayWriter();
49
+ const data = await this.entry.getData(writer);
50
+ this.data = data;
51
+ return this.data;
52
+ }
53
+ setData(data) {
54
+ this.data = data;
55
+ }
56
+ constructor(entry) {
57
+ this.filename = entry.filename;
58
+ if ("data" in entry) {
59
+ this.data = entry.data;
60
+ } else {
61
+ this.entry = entry;
62
+ }
63
+ }
64
+ }
65
+ class Epub {
66
+ constructor(entries, onClose) {
67
+ this.entries = entries;
68
+ this.onClose = onClose;
69
+ this.dataWriter = new import_zip.Uint8ArrayWriter();
70
+ this.zipWriter = new import_zip.ZipWriter(this.dataWriter);
71
+ this.readXhtmlItemContents = (0, import_mem.default)(
72
+ this.readXhtmlItemContents.bind(this),
73
+ // This isn't unnecessary, the generic here just isn't handling the
74
+ // overloaded method type correctly
75
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
76
+ { cacheKey: ([id, as]) => `${id}:${as ?? "xhtml"}` }
77
+ );
78
+ }
79
+ static xmlParser = new import_fast_xml_parser.XMLParser({
80
+ allowBooleanAttributes: true,
81
+ preserveOrder: true,
82
+ ignoreAttributes: false,
83
+ parseTagValue: false
84
+ });
85
+ static xhtmlParser = new import_fast_xml_parser.XMLParser({
86
+ allowBooleanAttributes: true,
87
+ alwaysCreateTextNode: true,
88
+ preserveOrder: true,
89
+ ignoreAttributes: false,
90
+ htmlEntities: true,
91
+ trimValues: false,
92
+ stopNodes: ["*.pre", "*.script"],
93
+ parseTagValue: false,
94
+ updateTag(_tagName, _jPath, attrs) {
95
+ if (attrs && "@_/" in attrs) {
96
+ delete attrs["@_/"];
97
+ }
98
+ return true;
99
+ }
100
+ });
101
+ static xmlBuilder = new import_fast_xml_parser.XMLBuilder({
102
+ preserveOrder: true,
103
+ format: true,
104
+ ignoreAttributes: false,
105
+ suppressEmptyNode: true
106
+ });
107
+ static xhtmlBuilder = new import_fast_xml_parser.XMLBuilder({
108
+ preserveOrder: true,
109
+ ignoreAttributes: false,
110
+ stopNodes: ["*.pre", "*.script"],
111
+ suppressEmptyNode: true
112
+ });
113
+ /**
114
+ * Format a duration, provided as a number of seconds, as
115
+ * a SMIL clock value, to be used for Media Overlays.
116
+ *
117
+ * @link https://www.w3.org/TR/epub-33/#sec-duration
118
+ */
119
+ static formatSmilDuration(duration) {
120
+ const hours = Math.floor(duration / 3600);
121
+ const minutes = Math.floor(duration / 60 - hours * 60);
122
+ const secondsAndMillis = duration - minutes * 60 - hours * 3600;
123
+ const [seconds, millis] = secondsAndMillis.toFixed(2).split(".");
124
+ return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.padStart(2, "0")}.${millis ?? "0"}`;
125
+ }
126
+ /**
127
+ * Given an XML structure representing a complete XHTML document,
128
+ * add a `link` element to the `head` of the document.
129
+ *
130
+ * This method modifies the provided XML structure.
131
+ */
132
+ static addLinkToXhtmlHead(xml, link) {
133
+ const html = Epub.findXmlChildByName("html", xml);
134
+ if (!html) throw new Error("Invalid XHTML document: no html element");
135
+ const head = Epub.findXmlChildByName("head", html.html);
136
+ if (!head) throw new Error("Invalid XHTML document: no head element");
137
+ head["head"].push({
138
+ link: [],
139
+ ":@": {
140
+ "@_rel": link.rel,
141
+ "@_href": link.href,
142
+ "@_type": link.type
143
+ }
144
+ });
145
+ }
146
+ /**
147
+ * Given an XML structure representing a complete XHTML document,
148
+ * return the sub-structure representing the children of the
149
+ * document's body element.
150
+ */
151
+ static getXhtmlBody(xml) {
152
+ const html = Epub.findXmlChildByName("html", xml);
153
+ if (!html) throw new Error("Invalid XHTML document: no html element");
154
+ const body = Epub.findXmlChildByName("body", html["html"]);
155
+ if (!body) throw new Error("Invalid XHTML document: No body element");
156
+ return body["body"];
157
+ }
158
+ static createXmlElement(name, properties, children = []) {
159
+ return {
160
+ ":@": Object.fromEntries(
161
+ Object.entries(properties).map(([prop, value]) => [`@_${prop}`, value])
162
+ ),
163
+ [name]: children
164
+ };
165
+ }
166
+ static createXmlTextNode(text) {
167
+ return { ["#text"]: text };
168
+ }
169
+ /**
170
+ * Given an XML structure representing a complete XHTML document,
171
+ * return a string representing the concatenation of all text nodes
172
+ * in the document.
173
+ */
174
+ static getXhtmlTextContent(xml) {
175
+ let text = "";
176
+ for (const child of xml) {
177
+ if (Epub.isXmlTextNode(child)) {
178
+ text += child["#text"];
179
+ continue;
180
+ }
181
+ const children = Epub.getXmlChildren(child);
182
+ text += Epub.getXhtmlTextContent(children);
183
+ }
184
+ return text;
185
+ }
186
+ /**
187
+ * Given an XMLElement, return its tag name.
188
+ */
189
+ static getXmlElementName(element) {
190
+ const keys = Object.keys(element);
191
+ const elementName = keys.find((key) => key !== ":@" && key !== "#text");
192
+ if (!elementName)
193
+ throw new Error(
194
+ `Invalid XML Element: missing tag name
195
+ ${JSON.stringify(element, null, 2)}`
196
+ );
197
+ return elementName;
198
+ }
199
+ /**
200
+ * Given an XMLElement, return a list of its children
201
+ */
202
+ static getXmlChildren(element) {
203
+ const elementName = Epub.getXmlElementName(element);
204
+ return element[elementName];
205
+ }
206
+ static replaceXmlChildren(element, children) {
207
+ const elementName = Epub.getXmlElementName(element);
208
+ element[elementName] = children;
209
+ }
210
+ /**
211
+ * Given an XML structure, find the first child matching
212
+ * the provided name and optional filter.
213
+ */
214
+ static findXmlChildByName(name, xml, filter) {
215
+ const element = xml.find((e) => name in e && (filter ? filter(e) : true));
216
+ return element;
217
+ }
218
+ /**
219
+ * Given an XMLNode, determine whether it represents
220
+ * a text node or an XML element.
221
+ */
222
+ static isXmlTextNode(node) {
223
+ return "#text" in node;
224
+ }
225
+ zipWriter;
226
+ dataWriter;
227
+ rootfile = null;
228
+ manifest = null;
229
+ spine = null;
230
+ packageMutex = new import_async_mutex.Mutex();
231
+ /**
232
+ * Close the Epub. Must be called before the Epub goes out
233
+ * of scope/is garbage collected.
234
+ */
235
+ async close() {
236
+ var _a;
237
+ await ((_a = this.onClose) == null ? void 0 : _a.call(this));
238
+ await this.zipWriter.close();
239
+ }
240
+ /**
241
+ * Construct an Epub instance, optionally beginning
242
+ * with the provided metadata.
243
+ *
244
+ * @param dublinCore Core metadata terms
245
+ * @param additionalMetadata An array of additional metadata entries
246
+ */
247
+ static async create({
248
+ title,
249
+ language,
250
+ identifier,
251
+ date,
252
+ subjects,
253
+ type,
254
+ creators,
255
+ contributors
256
+ }, additionalMetadata = []) {
257
+ const entries = [];
258
+ const encoder = new TextEncoder();
259
+ const container = encoder.encode(`<?xml version="1.0"?>
260
+ <container>
261
+ <rootfiles>
262
+ <rootfile media-type="application/oebps-package+xml" full-path="OEBPS/content.opf"/>
263
+ </rootfiles>
264
+ </container>
265
+ `);
266
+ entries.push(
267
+ new EpubEntry({ filename: "META-INF/container.xml", data: container })
268
+ );
269
+ const packageDocument = encoder.encode(`<?xml version="1.0"?>
270
+ <package unique-identifier="pub-id" dir="${language.textInfo.direction}" xml:lang=${language.toString()} version="3.0">
271
+ <metadata>
272
+ </metadata>
273
+ <manifest>
274
+ </manifest>
275
+ <spine>
276
+ </spine>
277
+ </package>
278
+ `);
279
+ entries.push(
280
+ new EpubEntry({ filename: "OEBPS/content.opf", data: packageDocument })
281
+ );
282
+ const epub = new this(entries);
283
+ const metadata = [
284
+ {
285
+ id: "pub-id",
286
+ type: "dc:identifier",
287
+ properties: {},
288
+ value: identifier
289
+ },
290
+ ...additionalMetadata
291
+ ];
292
+ await Promise.all(metadata.map((entry) => epub.addMetadata(entry)));
293
+ await epub.setTitle(title);
294
+ await epub.setLanguage(language);
295
+ if (date) await epub.setPublicationDate(date);
296
+ if (type) await epub.setType(type);
297
+ if (subjects) {
298
+ await Promise.all(subjects.map((subject) => epub.addSubject(subject)));
299
+ }
300
+ if (creators) {
301
+ await Promise.all(creators.map((creator) => epub.addCreator(creator)));
302
+ }
303
+ if (contributors) {
304
+ await Promise.all(
305
+ contributors.map((contributor) => epub.addCreator(contributor))
306
+ );
307
+ }
308
+ return epub;
309
+ }
310
+ /**
311
+ * Construct an Epub instance by reading an existing EPUB
312
+ * publication.
313
+ *
314
+ * @param pathOrData Must be either a string representing the
315
+ * path to an EPUB file on disk, or a Uint8Array representing
316
+ * the data of the EPUB publication.
317
+ */
318
+ static async from(pathOrData) {
319
+ if (typeof pathOrData === "string") {
320
+ throw new Error("Import from /node to construct from a file");
321
+ }
322
+ const fileData = pathOrData;
323
+ const dataReader = new import_zip.Uint8ArrayReader(fileData);
324
+ const zipReader = new import_zip.ZipReader(dataReader);
325
+ const zipEntries = await zipReader.getEntries();
326
+ const epubEntries = zipEntries.map((entry) => new EpubEntry(entry));
327
+ const epub = new this(epubEntries, () => zipReader.close());
328
+ return epub;
329
+ }
330
+ getEntry(path) {
331
+ return this.entries.find((entry) => entry.filename === path);
332
+ }
333
+ async removeEntry(href) {
334
+ const rootfile = await this.getRootfile();
335
+ const filename = this.resolveHref(rootfile, href);
336
+ const index = this.entries.findIndex((entry) => entry.filename === filename);
337
+ if (index === -1) return;
338
+ this.entries.splice(index, 1);
339
+ }
340
+ async getFileData(path, encoding) {
341
+ const containerEntry = this.getEntry(path);
342
+ if (!containerEntry)
343
+ throw new Error(
344
+ `Could not get file data for entry ${path}: entry not found`
345
+ );
346
+ const containerContents = await containerEntry.getData();
347
+ return encoding === "utf-8" ? new TextDecoder("utf-8").decode(containerContents) : containerContents;
348
+ }
349
+ async getRootfile() {
350
+ var _a;
351
+ if (this.rootfile !== null) return this.rootfile;
352
+ const containerString = await this.getFileData(
353
+ "META-INF/container.xml",
354
+ "utf-8"
355
+ );
356
+ if (!containerString)
357
+ throw new Error("Failed to parse EPUB: Missing META-INF/container.xml");
358
+ const containerDocument = Epub.xmlParser.parse(containerString);
359
+ const container = Epub.findXmlChildByName("container", containerDocument);
360
+ if (!container)
361
+ throw new Error(
362
+ "Failed to parse EPUB container.xml: Found no container element"
363
+ );
364
+ const rootfiles = Epub.findXmlChildByName(
365
+ "rootfiles",
366
+ Epub.getXmlChildren(container)
367
+ );
368
+ if (!rootfiles)
369
+ throw new Error(
370
+ "Failed to parse EPUB container.xml: Found no rootfiles element"
371
+ );
372
+ const rootfile = Epub.findXmlChildByName(
373
+ "rootfile",
374
+ Epub.getXmlChildren(rootfiles),
375
+ (node) => {
376
+ var _a2;
377
+ return !Epub.isXmlTextNode(node) && ((_a2 = node[":@"]) == null ? void 0 : _a2["@_media-type"]) === "application/oebps-package+xml";
378
+ }
379
+ );
380
+ if (!((_a = rootfile == null ? void 0 : rootfile[":@"]) == null ? void 0 : _a["@_full-path"]))
381
+ throw new Error(
382
+ "Failed to parse EPUB container.xml: Found no rootfile element"
383
+ );
384
+ this.rootfile = rootfile[":@"]["@_full-path"];
385
+ return this.rootfile;
386
+ }
387
+ migratePackageDocument(packageDocument) {
388
+ for (const element of packageDocument) {
389
+ if (Epub.isXmlTextNode(element)) continue;
390
+ const elementName = Epub.getXmlElementName(element);
391
+ if (elementName.startsWith("opf:")) {
392
+ element[elementName.replace("opf:", "")] = Epub.getXmlChildren(element);
393
+ delete element[elementName];
394
+ this.migratePackageDocument(Epub.getXmlChildren(element));
395
+ }
396
+ }
397
+ }
398
+ async getPackageDocument() {
399
+ const rootfile = await this.getRootfile();
400
+ const packageDocumentString = await this.getFileData(rootfile, "utf-8");
401
+ if (!packageDocumentString)
402
+ throw new Error(
403
+ `Failed to parse EPUB: could not find package document at ${rootfile}`
404
+ );
405
+ const packageDocument = Epub.xmlParser.parse(
406
+ packageDocumentString
407
+ );
408
+ return packageDocument;
409
+ }
410
+ async getPackageElement() {
411
+ const packageDocument = await this.getPackageDocument();
412
+ const packageElement = Epub.findXmlChildByName("package", packageDocument) ?? Epub.findXmlChildByName("opf:package", packageDocument);
413
+ if (!packageElement) {
414
+ throw new Error(
415
+ "Failed to parse EPUB: Found no package element in package document"
416
+ );
417
+ }
418
+ this.migratePackageDocument(packageDocument);
419
+ return packageElement;
420
+ }
421
+ /**
422
+ * Safely modify the package document, without race conditions.
423
+ *
424
+ * Since the reading the package document is an async process,
425
+ * multiple simultaneously dispatched function calls that all
426
+ * attempt to modify it can clobber each other's changes. This
427
+ * method uses a mutex to ensure that each update runs exclusively.
428
+ *
429
+ * @param producer The function to update the package document. If
430
+ * it returns a new package document, that will be persisted, otherwise
431
+ * it will be assumed that the package document was modified in place.
432
+ */
433
+ async withPackage(producer) {
434
+ await this.packageMutex.runExclusive(async () => {
435
+ const packageDocument = await this.getPackageDocument();
436
+ const packageElement = Epub.findXmlChildByName("package", packageDocument) ?? Epub.findXmlChildByName("opf:package", packageDocument);
437
+ if (!packageElement) {
438
+ throw new Error(
439
+ "Failed to parse EPUB: Found no package element in package document"
440
+ );
441
+ }
442
+ const produced = await producer(packageElement);
443
+ const updatedPackageDocument = await Epub.xmlBuilder.build(
444
+ produced ?? packageDocument
445
+ );
446
+ const rootfile = await this.getRootfile();
447
+ this.writeEntryContents(rootfile, updatedPackageDocument, "utf-8");
448
+ });
449
+ }
450
+ /**
451
+ * Retrieve the manifest for the Epub.
452
+ *
453
+ * This is represented as a map from each manifest items'
454
+ * id to the rest of its properties.
455
+ *
456
+ * @link https://www.w3.org/TR/epub-33/#sec-pkg-manifest
457
+ */
458
+ async getManifest() {
459
+ if (this.manifest !== null) return this.manifest;
460
+ const packageElement = await this.getPackageElement();
461
+ const manifest = Epub.findXmlChildByName(
462
+ "manifest",
463
+ Epub.getXmlChildren(packageElement)
464
+ );
465
+ if (!manifest)
466
+ throw new Error(
467
+ "Failed to parse EPUB: Found no manifest element in package document"
468
+ );
469
+ this.manifest = Epub.getXmlChildren(manifest).reduce((acc, item) => {
470
+ var _a, _b;
471
+ if (Epub.isXmlTextNode(item)) return acc;
472
+ if (!((_a = item[":@"]) == null ? void 0 : _a["@_id"]) || !item[":@"]["@_href"]) {
473
+ return acc;
474
+ }
475
+ return {
476
+ ...acc,
477
+ [item[":@"]["@_id"]]: {
478
+ id: item[":@"]["@_id"],
479
+ href: item[":@"]["@_href"],
480
+ mediaType: item[":@"]["@_media-type"],
481
+ mediaOverlay: item[":@"]["@_media-overlay"],
482
+ fallback: item[":@"]["@_fallback"],
483
+ properties: (_b = item[":@"]["@_properties"]) == null ? void 0 : _b.split(" ")
484
+ }
485
+ };
486
+ }, {});
487
+ return this.manifest;
488
+ }
489
+ /**
490
+ * Returns the first index in the metadata element's children array
491
+ * that matches the provided predicate.
492
+ *
493
+ * Note: This may technically be different than the index in the
494
+ * getMetadata() array, as it includes non-metadata nodes, like
495
+ * text nodes. These are technically not allowed, but may exist,
496
+ * nonetheless. As consumers only ever see the getMetadata()
497
+ * array, this method is only meant to be used internally.
498
+ */
499
+ findMetadataIndex(packageElement, predicate) {
500
+ const metadataElement = Epub.findXmlChildByName(
501
+ "metadata",
502
+ Epub.getXmlChildren(packageElement)
503
+ );
504
+ if (!metadataElement)
505
+ throw new Error(
506
+ "Failed to parse EPUB: Found no metadata element in package document"
507
+ );
508
+ return metadataElement.metadata.findIndex((node) => {
509
+ const item = Epub.parseMetadataItem(node);
510
+ if (!item) return false;
511
+ return predicate(item);
512
+ });
513
+ }
514
+ /**
515
+ * Returns the item in the metadata element's children array
516
+ * that matches the provided predicate.
517
+ */
518
+ async findMetadataItem(predicate) {
519
+ const [first] = await this.findAllMetadataItems(predicate);
520
+ return first ?? null;
521
+ }
522
+ /**
523
+ * Returns the item in the metadata element's children array
524
+ * that matches the provided predicate.
525
+ */
526
+ async findAllMetadataItems(predicate) {
527
+ const packageElement = await this.getPackageElement();
528
+ const metadataElement = Epub.findXmlChildByName(
529
+ "metadata",
530
+ Epub.getXmlChildren(packageElement)
531
+ );
532
+ if (!metadataElement)
533
+ throw new Error(
534
+ "Failed to parse EPUB: Found no metadata element in package document"
535
+ );
536
+ const elements = metadataElement.metadata.filter((node) => {
537
+ const item = Epub.parseMetadataItem(node);
538
+ if (!item) return false;
539
+ return predicate(item);
540
+ });
541
+ return elements.map((element) => Epub.parseMetadataItem(element)).filter((item) => !!item);
542
+ }
543
+ static parseMetadataItem(node) {
544
+ if (Epub.isXmlTextNode(node)) return null;
545
+ const elementName = Epub.getXmlElementName(node);
546
+ const textNode = Epub.getXmlChildren(node)[0];
547
+ const value = !textNode || !Epub.isXmlTextNode(textNode) ? void 0 : textNode["#text"].replaceAll(/\s+/g, " ");
548
+ const attributes = node[":@"] ?? {};
549
+ const { id, ...properties } = Object.fromEntries(
550
+ Object.entries(attributes).map(([attrName, value2]) => [
551
+ attrName.slice(2),
552
+ value2
553
+ ])
554
+ );
555
+ return {
556
+ id,
557
+ type: elementName,
558
+ properties,
559
+ value
560
+ };
561
+ }
562
+ /**
563
+ * Retrieve the metadata entries for the Epub.
564
+ *
565
+ * This is represented as an array of metadata entries,
566
+ * in the order that they're presented in the Epub package document.
567
+ *
568
+ * For more useful semantic representations of metadata, use
569
+ * specific methods such as `getTitle()` and `getAuthors()`.
570
+ *
571
+ * @link https://www.w3.org/TR/epub-33/#sec-pkg-metadata
572
+ */
573
+ async getMetadata() {
574
+ const packageElement = await this.getPackageElement();
575
+ const metadataElement = Epub.findXmlChildByName(
576
+ "metadata",
577
+ Epub.getXmlChildren(packageElement)
578
+ );
579
+ if (!metadataElement)
580
+ throw new Error(
581
+ "Failed to parse EPUB: Found no metadata element in package document"
582
+ );
583
+ const metadata = metadataElement.metadata.map((node) => Epub.parseMetadataItem(node)).filter((node) => !!node);
584
+ return metadata;
585
+ }
586
+ /**
587
+ * Even "EPUB 3" publications sometimes still only use the
588
+ * EPUB 2 specification for identifying the cover image.
589
+ * This is a private method that is used as a fallback if
590
+ * we fail to find the cover image according to the EPUB 3
591
+ * spec.
592
+ */
593
+ async getEpub2CoverImage() {
594
+ var _a;
595
+ const packageElement = await this.getPackageElement();
596
+ const metadataElement = Epub.findXmlChildByName(
597
+ "metadata",
598
+ Epub.getXmlChildren(packageElement)
599
+ );
600
+ if (!metadataElement)
601
+ throw new Error(
602
+ "Failed to parse EPUB: Found no metadata element in package document"
603
+ );
604
+ const coverImageElement = Epub.getXmlChildren(metadataElement).find(
605
+ (node) => {
606
+ var _a2;
607
+ return !Epub.isXmlTextNode(node) && ((_a2 = node[":@"]) == null ? void 0 : _a2["@_name"]) === "cover";
608
+ }
609
+ );
610
+ const manifestItemId = (_a = coverImageElement == null ? void 0 : coverImageElement[":@"]) == null ? void 0 : _a["@_content"];
611
+ if (!manifestItemId) return null;
612
+ const manifest = await this.getManifest();
613
+ return Object.values(manifest).find((item) => item.id === manifestItemId) ?? null;
614
+ }
615
+ /**
616
+ * Retrieve the cover image manifest item.
617
+ *
618
+ * This does not return the actual image data. To
619
+ * retrieve the image data, pass this item's id to
620
+ * epub.readItemContents, or use epub.getCoverImage()
621
+ * instead.
622
+ *
623
+ * @link https://www.w3.org/TR/epub-33/#sec-cover-image
624
+ */
625
+ async getCoverImageItem() {
626
+ const manifest = await this.getManifest();
627
+ const coverImage = Object.values(manifest).find(
628
+ (item) => {
629
+ var _a;
630
+ return (_a = item.properties) == null ? void 0 : _a.includes("cover-image");
631
+ }
632
+ );
633
+ if (coverImage) return coverImage;
634
+ return this.getEpub2CoverImage();
635
+ }
636
+ /**
637
+ * Retrieve the cover image data as a byte array.
638
+ *
639
+ * This does not include, for example, the cover image's
640
+ * filename or mime type. To retrieve the image manifest
641
+ * item, use epub.getCoverImageItem().
642
+ *
643
+ * @link https://www.w3.org/TR/epub-33/#sec-cover-image
644
+ */
645
+ async getCoverImage() {
646
+ const coverImageItem = await this.getCoverImageItem();
647
+ if (!coverImageItem) return coverImageItem;
648
+ return this.readItemContents(coverImageItem.id);
649
+ }
650
+ /**
651
+ * Set the cover image for the EPUB.
652
+ *
653
+ * Adds a manifest item with the `cover-image` property, per
654
+ * the EPUB 3 spec, and then writes the provided image data to
655
+ * the provided href within the publication.
656
+ */
657
+ async setCoverImage(href, data) {
658
+ const coverImageItem = await this.getCoverImageItem();
659
+ if (coverImageItem) {
660
+ await this.removeManifestItem(coverImageItem.id);
661
+ }
662
+ const mediaType = (0, import_mime_types.lookup)(href);
663
+ if (!mediaType)
664
+ throw new Error(`Invalid file extension for cover image: ${href}`);
665
+ await this.addManifestItem(
666
+ { id: "cover-image", href, mediaType, properties: ["cover-image"] },
667
+ data
668
+ );
669
+ }
670
+ /**
671
+ * Retrieve the publication date from the dc:date element
672
+ * in the EPUB metadata as a Date object.
673
+ *
674
+ * If there is no dc:date element, returns null.
675
+ *
676
+ * @link https://www.w3.org/TR/epub-33/#sec-opf-dcdate
677
+ */
678
+ async getPublicationDate() {
679
+ const metadata = await this.getMetadata();
680
+ const entry = metadata.find(({ type }) => type === "dc:date");
681
+ if (!(entry == null ? void 0 : entry.value)) return null;
682
+ return new Date(entry.value);
683
+ }
684
+ /**
685
+ * Set the dc:date metadata element with the provided date.
686
+ *
687
+ * Updates the existing dc:date element if one exists.
688
+ * Otherwise creates a new element
689
+ *
690
+ * @link https://www.w3.org/TR/epub-33/#sec-opf-dcdate
691
+ */
692
+ async setPublicationDate(date) {
693
+ await this.replaceMetadata(({ type }) => type === "dc:date", {
694
+ type: "dc:date",
695
+ properties: {},
696
+ value: date.toISOString()
697
+ });
698
+ }
699
+ /**
700
+ * Set the dc:type metadata element.
701
+ *
702
+ * Updates the existing dc:type element if one exists.
703
+ * Otherwise creates a new element.
704
+ *
705
+ * @link https://www.w3.org/TR/epub-33/#sec-opf-dctype
706
+ */
707
+ async setType(type) {
708
+ await this.replaceMetadata(({ type: type2 }) => type2 === "dc:type", {
709
+ type: "dc:type",
710
+ properties: {},
711
+ value: type
712
+ });
713
+ }
714
+ /**
715
+ * Retrieve the publication type from the dc:type element
716
+ * in the EPUB metadata.
717
+ *
718
+ * If there is no dc:type element, returns null.
719
+ *
720
+ * @link https://www.w3.org/TR/epub-33/#sec-opf-dctype
721
+ */
722
+ async getType() {
723
+ const metadata = await this.getMetadata();
724
+ return metadata.find(({ type }) => type === "dc:type") ?? null;
725
+ }
726
+ /**
727
+ * Add a subject to the EPUB metadata.
728
+ *
729
+ * @param subject May be a string representing just a schema-less
730
+ * subject name, or a DcSubject object
731
+ *
732
+ * @link https://www.w3.org/TR/epub-33/#sec-opf-dcsubject
733
+ */
734
+ async addSubject(subject) {
735
+ const subjectEntry = typeof subject === "string" ? {
736
+ value: subject
737
+ } : subject;
738
+ const subjectId = (0, import_nanoid.nanoid)();
739
+ await this.addMetadata({
740
+ id: subjectId,
741
+ type: "dc:subject",
742
+ properties: {},
743
+ value: subjectEntry.value
744
+ });
745
+ if ("authority" in subjectEntry) {
746
+ await this.addMetadata({
747
+ type: "meta",
748
+ properties: { refines: `#${subjectId}`, property: "authority" },
749
+ value: subjectEntry.authority
750
+ });
751
+ await this.addMetadata({
752
+ type: "meta",
753
+ properties: { refines: `#${subjectId}`, property: "term" },
754
+ value: subjectEntry.term
755
+ });
756
+ }
757
+ }
758
+ /**
759
+ * Remove a subject from the EPUB metadata.
760
+ *
761
+ * Removes the subject at the provided index. This index
762
+ * refers to the array returned by `epub.getSubjects()`.
763
+ *
764
+ * @link https://www.w3.org/TR/epub-33/#sec-opf-dccreator
765
+ */
766
+ async removeSubject(index) {
767
+ await this.withPackage((packageElement) => {
768
+ const metadata = Epub.findXmlChildByName(
769
+ "metadata",
770
+ Epub.getXmlChildren(packageElement)
771
+ );
772
+ if (!metadata)
773
+ throw new Error(
774
+ "Failed to parse EPUB: found no metadata element in package document"
775
+ );
776
+ let subjectCount = null;
777
+ let metadataIndex = null;
778
+ for (const meta of Epub.getXmlChildren(metadata)) {
779
+ if (subjectCount === index) break;
780
+ metadataIndex = metadataIndex === null ? 0 : metadataIndex + 1;
781
+ if (Epub.isXmlTextNode(meta)) continue;
782
+ if (Epub.getXmlElementName(meta) !== "dc:subject") continue;
783
+ subjectCount = subjectCount === null ? 0 : subjectCount + 1;
784
+ }
785
+ if (subjectCount === null || metadataIndex === null) return;
786
+ Epub.getXmlChildren(metadata).splice(metadataIndex, 1);
787
+ });
788
+ }
789
+ /**
790
+ * Retrieve the list of subjects for this EPUB.
791
+ *
792
+ * Subjects without associated authority and term metadata
793
+ * will be returned as strings. Otherwise, they will
794
+ * be represented as DcSubject objects, with a value,
795
+ * authority, and term.
796
+ *
797
+ * @link https://www.w3.org/TR/epub-33/#sec-opf-dcsubject
798
+ */
799
+ async getSubjects() {
800
+ const metadata = await this.getMetadata();
801
+ const subjectEntries = metadata.filter(({ type }) => type === "dc:subject");
802
+ const subjects = subjectEntries.map(({ value }) => value).filter((value) => !!value);
803
+ metadata.forEach((entry) => {
804
+ if (entry.type !== "meta" || entry.properties["property"] !== "term" && entry.properties["property"] !== "authority") {
805
+ return;
806
+ }
807
+ const subjectIdref = entry.properties["refines"];
808
+ if (!subjectIdref) return;
809
+ const subjectId = subjectIdref.slice(1);
810
+ const index = subjectEntries.findIndex((entry2) => entry2.id === subjectId);
811
+ if (index === -1) return;
812
+ const subject = typeof subjects[index] === "string" ? { value: subjects[index], authority: void 0, term: void 0 } : (
813
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
814
+ subjects[index]
815
+ );
816
+ subject[entry.properties["property"]] = entry.value;
817
+ subjects.splice(index, 1, subject);
818
+ });
819
+ return subjects;
820
+ }
821
+ /**
822
+ * Retrieve the Epub's language as specified in its
823
+ * package document metadata.
824
+ *
825
+ * If no language metadata is specified, returns null.
826
+ * Returns the language as an Intl.Locale instance.
827
+ *
828
+ * @link https://www.w3.org/TR/epub-33/#sec-opf-dclanguage
829
+ */
830
+ async getLanguage() {
831
+ const metadata = await this.getMetadata();
832
+ const languageEntries = metadata.filter(
833
+ (entry) => entry.type === "dc:language"
834
+ );
835
+ const primaryLanguage = languageEntries[0];
836
+ if (!primaryLanguage) return null;
837
+ const locale = primaryLanguage.value;
838
+ if (!locale || locale.toLowerCase() === "und") return null;
839
+ return new Intl.Locale(locale);
840
+ }
841
+ /**
842
+ * Update the Epub's language metadata entry.
843
+ *
844
+ * Updates the existing dc:language element if one exists.
845
+ * Otherwise creates a new element
846
+ *
847
+ * @link https://www.w3.org/TR/epub-33/#sec-opf-dclanguage
848
+ */
849
+ async setLanguage(locale) {
850
+ await this.replaceMetadata(({ type }) => type === "dc:language", {
851
+ type: "dc:language",
852
+ properties: {},
853
+ value: locale.toString()
854
+ });
855
+ }
856
+ /**
857
+ * Retrieve the title of the Epub.
858
+ *
859
+ * @param main Optional - whether to return only the first title segment
860
+ * if multiple are found. Otherwise, will follow the spec to combine title
861
+ * segments
862
+ *
863
+ * @link https://www.w3.org/TR/epub-33/#sec-opf-dctitle
864
+ */
865
+ async getTitle(expanded = false) {
866
+ var _a;
867
+ const entries = await this.getTitles();
868
+ if (!expanded) {
869
+ const mainEntry = entries.find((entry) => entry.type === "main");
870
+ if (mainEntry) return mainEntry.title;
871
+ const shortEntry = entries.find((entry) => entry.type === "short");
872
+ if (shortEntry) return shortEntry.title;
873
+ return ((_a = entries[0]) == null ? void 0 : _a.title) ?? null;
874
+ }
875
+ const expandedEntry = entries.find((entry) => entry.type === "expanded");
876
+ if (expandedEntry) return expandedEntry.title;
877
+ return entries.map((entry) => entry.title).join(", ");
878
+ }
879
+ /**
880
+ * Retrieve the subtitle of the Epub, if it exists.
881
+ *
882
+ * @link https://www.w3.org/TR/epub-33/#sec-opf-dctitle
883
+ */
884
+ async getSubtitle() {
885
+ const entries = await this.getTitles();
886
+ const subtitleEntry = entries.find((entry) => entry.type === "subtitle");
887
+ return (subtitleEntry == null ? void 0 : subtitleEntry.title) ?? null;
888
+ }
889
+ /**
890
+ * Retrieve all title entries of the Epub.
891
+ *
892
+ * @link https://www.w3.org/TR/epub-33/#sec-opf-dctitle
893
+ */
894
+ async getTitles() {
895
+ const metadata = await this.getMetadata();
896
+ const titleEntries = metadata.filter((entry) => entry.type === "dc:title");
897
+ const titleRefinements = metadata.filter(
898
+ (entry) => entry.type === "meta" && entry.properties["refines"] && (entry.properties["property"] === "title-type" || entry.properties["property"] === "display-seq")
899
+ );
900
+ const sortedTitleParts = titleEntries.filter(
901
+ (titleEntry) => titleEntry.id && titleRefinements.some(
902
+ (entry) => {
903
+ var _a;
904
+ return entry.value && ((_a = entry.properties["refines"]) == null ? void 0 : _a.slice(1)) === titleEntry.id && entry.properties["property"] === "display-seq" && !Number.isNaN(parseInt(entry.value, 10));
905
+ }
906
+ )
907
+ ).sort((a, b) => {
908
+ const refinementA = titleRefinements.find(
909
+ (entry) => entry.properties["property"] === "display-seq" && entry.properties["refines"].slice(1) === a.id
910
+ );
911
+ const refinementB = titleRefinements.find(
912
+ (entry) => entry.properties["property"] === "display-seq" && entry.properties["refines"].slice(1) === b.id
913
+ );
914
+ const sortA = parseInt(refinementA.value, 10);
915
+ const sortB = parseInt(refinementB.value, 10);
916
+ return sortA - sortB;
917
+ });
918
+ return (sortedTitleParts.length === 0 ? titleEntries : sortedTitleParts).map((entry) => {
919
+ const titleType = titleRefinements.find(
920
+ (refinement) => {
921
+ var _a;
922
+ return ((_a = refinement.properties["refines"]) == null ? void 0 : _a.slice(1)) === entry.id && refinement.properties["property"] === "title-type";
923
+ }
924
+ );
925
+ return {
926
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
927
+ title: entry.value,
928
+ type: (titleType == null ? void 0 : titleType.value) ?? null
929
+ };
930
+ });
931
+ }
932
+ /**
933
+ * Update the Epub's description metadata entry.
934
+ *
935
+ * Updates the existing dc:description element if one exists.
936
+ * Otherwise creates a new element. Any non-ASCII symbols,
937
+ * `&`, `<`, `>`, `"`, `'`, and `\``` will be encoded as HTML entities.
938
+ */
939
+ async setDescription(description) {
940
+ await this.replaceMetadata(({ type }) => type === "dc:description", {
941
+ type: "dc:description",
942
+ value: import_he.default.encode(description),
943
+ properties: {}
944
+ });
945
+ }
946
+ /**
947
+ * Retrieve the Epub's description as specified in its
948
+ * package document metadata.
949
+ *
950
+ * If no description metadata is specified, returns null.
951
+ * Returns the description as a string. Descriptions may
952
+ * include HTML markup.
953
+ */
954
+ async getDescription() {
955
+ const metadata = await this.getMetadata();
956
+ const descriptionEntry = metadata.find(
957
+ (entry) => entry.type === "dc:description"
958
+ );
959
+ if (!(descriptionEntry == null ? void 0 : descriptionEntry.value)) return null;
960
+ const escaped = descriptionEntry.value;
961
+ return import_he.default.decode(escaped);
962
+ }
963
+ /**
964
+ * Return the set of custom vocabulary prefixes set on this publication's
965
+ * root package element.
966
+ *
967
+ * Returns a map from prefix to URI
968
+ *
969
+ * @link https://www.w3.org/TR/epub-33/#sec-prefix-attr
970
+ */
971
+ async getPackageVocabularyPrefixes() {
972
+ var _a;
973
+ const packageElement = await this.getPackageElement();
974
+ const prefixValue = (_a = packageElement[":@"]) == null ? void 0 : _a["@_prefix"];
975
+ if (!prefixValue) return {};
976
+ const matches = prefixValue.matchAll(/(?:([a-z]+): +(\S+)\s*)/gs);
977
+ return Array.from(matches).reduce(
978
+ (acc, match) => match[1] && match[2] ? { ...acc, [match[1]]: match[2] } : acc,
979
+ {}
980
+ );
981
+ }
982
+ /**
983
+ * Set a custom vocabulary prefix on the root package element.
984
+ *
985
+ * @link https://www.w3.org/TR/epub-33/#sec-prefix-attr
986
+ */
987
+ async setPackageVocabularyPrefix(prefix, uri) {
988
+ await this.withPackage(async (packageElement) => {
989
+ const prefixes = await this.getPackageVocabularyPrefixes();
990
+ prefixes[prefix] = uri;
991
+ packageElement[":@"] ??= {};
992
+ packageElement[":@"]["@_prefix"] = Object.entries(prefixes).map(([p, u]) => `${p}: ${u}`).join("\n ");
993
+ });
994
+ }
995
+ /**
996
+ * Set the title of the Epub.
997
+ *
998
+ * This will replace all existing dc:title elements with
999
+ * this title. It will be given title-type "main".
1000
+ *
1001
+ * To set specific titles and their types, use epub.setTitles().
1002
+ *
1003
+ * @link https://www.w3.org/TR/epub-33/#sec-opf-dctitle
1004
+ */
1005
+ // TODO: This should allow users to optionally specify an array,
1006
+ // rather than a single string, to support expanded titles.
1007
+ async setTitle(title) {
1008
+ await this.withPackage((packageElement) => {
1009
+ const metadata = Epub.findXmlChildByName(
1010
+ "metadata",
1011
+ Epub.getXmlChildren(packageElement)
1012
+ );
1013
+ if (!metadata)
1014
+ throw new Error(
1015
+ "Failed to parse EPUB: found no metadata element in package document"
1016
+ );
1017
+ const titleElement = Epub.findXmlChildByName(
1018
+ "dc:title",
1019
+ metadata.metadata
1020
+ );
1021
+ if (!titleElement) {
1022
+ Epub.getXmlChildren(metadata).push(
1023
+ Epub.createXmlElement("dc:title", {}, [
1024
+ Epub.createXmlTextNode(title)
1025
+ ])
1026
+ );
1027
+ } else {
1028
+ titleElement["dc:title"] = [Epub.createXmlTextNode(title)];
1029
+ }
1030
+ });
1031
+ }
1032
+ async setTitles(entries) {
1033
+ await this.withPackage((packageElement) => {
1034
+ var _a, _b;
1035
+ const metadata = Epub.findXmlChildByName(
1036
+ "metadata",
1037
+ Epub.getXmlChildren(packageElement)
1038
+ );
1039
+ if (!metadata) {
1040
+ throw new Error(
1041
+ "Failed to parse EPUB: found no metadata element in package document"
1042
+ );
1043
+ }
1044
+ const metadataEntries = Epub.getXmlChildren(metadata);
1045
+ for (let i = metadataEntries.length - 1; i >= 0; i--) {
1046
+ const meta = metadataEntries[i];
1047
+ if (Epub.isXmlTextNode(meta)) continue;
1048
+ if (Epub.getXmlElementName(meta) === "dc:title" || ((_a = meta[":@"]) == null ? void 0 : _a["@_property"]) === "title-type" || ((_b = meta[":@"]) == null ? void 0 : _b["@_property"]) === "display-seq") {
1049
+ metadataEntries.splice(i, 1);
1050
+ }
1051
+ }
1052
+ for (let i = 0; i < entries.length; i++) {
1053
+ const entry = entries[i];
1054
+ const id = (0, import_nanoid.nanoid)();
1055
+ metadataEntries.push(
1056
+ Epub.createXmlElement("dc:title", { id }, [
1057
+ Epub.createXmlTextNode(entry.title)
1058
+ ])
1059
+ );
1060
+ if (entry.type) {
1061
+ metadataEntries.push(
1062
+ Epub.createXmlElement(
1063
+ "meta",
1064
+ { refines: `#${id}`, property: "title-type" },
1065
+ [Epub.createXmlTextNode(entry.type)]
1066
+ )
1067
+ );
1068
+ }
1069
+ metadataEntries.push(
1070
+ Epub.createXmlElement(
1071
+ "meta",
1072
+ { refines: `#${id}`, property: "display-seq" },
1073
+ [Epub.createXmlTextNode((i + 1).toString())]
1074
+ )
1075
+ );
1076
+ }
1077
+ });
1078
+ }
1079
+ /**
1080
+ * Retrieve the list of collections.
1081
+ */
1082
+ async getCollections() {
1083
+ var _a, _b;
1084
+ const metadata = await this.getMetadata();
1085
+ const collections = [];
1086
+ for (const entry of metadata) {
1087
+ if (entry.properties["property"] === "belongs-to-collection" && entry.value) {
1088
+ const type = (_a = metadata.find(
1089
+ (e) => e.properties["refines"] === `#${entry.id ?? ""}` && e.properties["property"] === "collection-type"
1090
+ )) == null ? void 0 : _a.value;
1091
+ const position = (_b = metadata.find(
1092
+ (e) => e.properties["refines"] === `#${entry.id ?? ""}` && e.properties["property"] === "group-position"
1093
+ )) == null ? void 0 : _b.value;
1094
+ collections.push({
1095
+ name: entry.value,
1096
+ ...type && { type },
1097
+ ...position && { position }
1098
+ });
1099
+ }
1100
+ }
1101
+ return collections;
1102
+ }
1103
+ /**
1104
+ * Add a collection to the EPUB metadata.
1105
+ *
1106
+ * If index is provided, the collection will be placed at
1107
+ * that index in the list of collections. Otherwise, it
1108
+ * will be added to the end of the list.
1109
+ */
1110
+ async addCollection(collection, index) {
1111
+ const collectionId = (0, import_nanoid.nanoid)();
1112
+ await this.withPackage((packageElement) => {
1113
+ var _a;
1114
+ const metadata = Epub.findXmlChildByName(
1115
+ "metadata",
1116
+ Epub.getXmlChildren(packageElement)
1117
+ );
1118
+ if (!metadata)
1119
+ throw new Error(
1120
+ "Failed to parse EPUB: found no metadata element in package document"
1121
+ );
1122
+ let collectionCount = 0;
1123
+ let metadataIndex = 0;
1124
+ for (const meta of Epub.getXmlChildren(metadata)) {
1125
+ if (collectionCount === index) break;
1126
+ metadataIndex++;
1127
+ if (Epub.isXmlTextNode(meta)) continue;
1128
+ if (Epub.getXmlElementName(meta) !== "meta") continue;
1129
+ if (((_a = meta[":@"]) == null ? void 0 : _a["@_property"]) !== "belongs-to-collection") continue;
1130
+ collectionCount++;
1131
+ }
1132
+ Epub.getXmlChildren(metadata).splice(
1133
+ metadataIndex,
1134
+ 0,
1135
+ Epub.createXmlElement(
1136
+ "meta",
1137
+ { id: collectionId, property: "belongs-to-collection" },
1138
+ [Epub.createXmlTextNode(collection.name)]
1139
+ )
1140
+ );
1141
+ });
1142
+ if (collection.position) {
1143
+ await this.addMetadata({
1144
+ type: "meta",
1145
+ properties: { refines: `#${collectionId}`, property: "group-position" },
1146
+ value: collection.position
1147
+ });
1148
+ }
1149
+ if (collection.type) {
1150
+ await this.addMetadata({
1151
+ type: "meta",
1152
+ properties: {
1153
+ refines: `#${collectionId}`,
1154
+ property: "collection-type"
1155
+ },
1156
+ value: collection.type
1157
+ });
1158
+ }
1159
+ }
1160
+ /**
1161
+ * Remove a collection from the EPUB metadata.
1162
+ *
1163
+ * Removes the collection at the provided index. This index
1164
+ * refers to the array returned by `epub.getCollections()`.
1165
+ */
1166
+ async removeCollection(index) {
1167
+ await this.withPackage((packageElement) => {
1168
+ var _a, _b;
1169
+ const metadata = Epub.findXmlChildByName(
1170
+ "metadata",
1171
+ Epub.getXmlChildren(packageElement)
1172
+ );
1173
+ if (!metadata)
1174
+ throw new Error(
1175
+ "Failed to parse EPUB: found no metadata element in package document"
1176
+ );
1177
+ let collectionCount = null;
1178
+ let metadataIndex = null;
1179
+ for (const meta of Epub.getXmlChildren(metadata)) {
1180
+ if (collectionCount === index) break;
1181
+ metadataIndex = metadataIndex === null ? 0 : metadataIndex + 1;
1182
+ if (Epub.isXmlTextNode(meta)) continue;
1183
+ if (Epub.getXmlElementName(meta) !== "meta") continue;
1184
+ if (((_a = meta[":@"]) == null ? void 0 : _a["@_property"]) !== "belongs-to-collection") continue;
1185
+ collectionCount = collectionCount === null ? 0 : collectionCount + 1;
1186
+ }
1187
+ if (collectionCount === null || metadataIndex === null) return;
1188
+ const [removed] = Epub.getXmlChildren(metadata).splice(metadataIndex, 1);
1189
+ if (removed && !Epub.isXmlTextNode(removed) && ((_b = removed[":@"]) == null ? void 0 : _b["@_id"])) {
1190
+ const id = removed[":@"]["@_id"];
1191
+ const newChildren = Epub.getXmlChildren(metadata).filter((node) => {
1192
+ var _a2;
1193
+ if (Epub.isXmlTextNode(node)) return true;
1194
+ if (Epub.getXmlElementName(node) !== "meta") return true;
1195
+ if (((_a2 = node[":@"]) == null ? void 0 : _a2["@_refines"]) !== `#${id}`) return true;
1196
+ return false;
1197
+ });
1198
+ Epub.replaceXmlChildren(metadata, newChildren);
1199
+ }
1200
+ });
1201
+ }
1202
+ /**
1203
+ * Retrieve the list of creators.
1204
+ *
1205
+ * @link https://www.w3.org/TR/epub-33/#sec-opf-dccreator
1206
+ */
1207
+ async getCreators(type = "creator") {
1208
+ const metadata = await this.getMetadata();
1209
+ const creatorEntries = metadata.filter(
1210
+ (entry) => entry.type === `dc:${type}`
1211
+ );
1212
+ const creators = creatorEntries.map(({ value }) => value).filter((value) => !!value).map((value) => ({ name: value }));
1213
+ metadata.forEach((entry) => {
1214
+ if (entry.type !== "meta" || entry.properties["property"] !== "file-as" && entry.properties["property"] !== "role" && entry.properties["property"] !== "alternate-script" || !entry.value) {
1215
+ return;
1216
+ }
1217
+ const creatorIdref = entry.properties["refines"];
1218
+ if (!creatorIdref) return;
1219
+ const creatorId = creatorIdref.slice(1);
1220
+ const index = creatorEntries.findIndex((entry2) => entry2.id === creatorId);
1221
+ if (index === -1) return;
1222
+ const creator = creators[index];
1223
+ if (entry.properties["alternate-script"]) {
1224
+ if (!entry.properties["xml:lang"]) return;
1225
+ creator.alternateScripts ??= [];
1226
+ creator.alternateScripts.push({
1227
+ name: entry.value,
1228
+ locale: new Intl.Locale(entry.properties["xml:lang"])
1229
+ });
1230
+ return;
1231
+ }
1232
+ const prop = entry.properties["property"] === "file-as" ? "fileAs" : "role";
1233
+ creator[prop] = entry.value;
1234
+ if (prop === "role" && entry.properties["scheme"]) {
1235
+ creator.roleScheme = entry.properties["scheme"];
1236
+ }
1237
+ });
1238
+ return creators;
1239
+ }
1240
+ /**
1241
+ * Retrieve the list of contributors.
1242
+ *
1243
+ * This is a convenience method for
1244
+ * `epub.getCreators('contributor')`.
1245
+ *
1246
+ * @link https://www.w3.org/TR/epub-33/#sec-opf-dccontributor
1247
+ */
1248
+ getContributors() {
1249
+ return this.getCreators("contributor");
1250
+ }
1251
+ /**
1252
+ * Add a creator to the EPUB metadata.
1253
+ *
1254
+ * If index is provided, the creator will be placed at
1255
+ * that index in the list of creators. Otherwise, it
1256
+ * will be added to the end of the list.
1257
+ *
1258
+ * @link https://www.w3.org/TR/epub-33/#sec-opf-dccreator
1259
+ */
1260
+ async addCreator(creator, index, type = "creator") {
1261
+ const creatorId = (0, import_nanoid.nanoid)();
1262
+ await this.withPackage((packageElement) => {
1263
+ const metadata = Epub.findXmlChildByName(
1264
+ "metadata",
1265
+ Epub.getXmlChildren(packageElement)
1266
+ );
1267
+ if (!metadata)
1268
+ throw new Error(
1269
+ "Failed to parse EPUB: found no metadata element in package document"
1270
+ );
1271
+ let creatorCount = 0;
1272
+ let metadataIndex = 0;
1273
+ for (const meta of Epub.getXmlChildren(metadata)) {
1274
+ if (creatorCount === index) break;
1275
+ metadataIndex++;
1276
+ if (Epub.isXmlTextNode(meta)) continue;
1277
+ if (Epub.getXmlElementName(meta) !== `dc:${type}`) continue;
1278
+ creatorCount++;
1279
+ }
1280
+ Epub.getXmlChildren(metadata).splice(
1281
+ metadataIndex,
1282
+ 0,
1283
+ Epub.createXmlElement(`dc:${type}`, { id: creatorId }, [
1284
+ Epub.createXmlTextNode(creator.name)
1285
+ ])
1286
+ );
1287
+ });
1288
+ if (creator.role) {
1289
+ await this.addMetadata({
1290
+ type: "meta",
1291
+ properties: {
1292
+ refines: `#${creatorId}`,
1293
+ property: "role",
1294
+ ...creator.roleScheme && { scheme: creator.roleScheme }
1295
+ },
1296
+ value: creator.role
1297
+ });
1298
+ }
1299
+ if (creator.fileAs) {
1300
+ await this.addMetadata({
1301
+ type: "meta",
1302
+ properties: { refines: `#${creatorId}`, property: "file-as" },
1303
+ value: creator.fileAs
1304
+ });
1305
+ }
1306
+ if (creator.alternateScripts) {
1307
+ for (const alternate of creator.alternateScripts) {
1308
+ await this.addMetadata({
1309
+ type: "meta",
1310
+ properties: {
1311
+ refines: `#${creatorId}`,
1312
+ property: "alternate-script",
1313
+ "xml:lang": alternate.locale.toString()
1314
+ },
1315
+ value: alternate.name
1316
+ });
1317
+ }
1318
+ }
1319
+ }
1320
+ /**
1321
+ * Remove a creator from the EPUB metadata.
1322
+ *
1323
+ * Removes the creator at the provided index. This index
1324
+ * refers to the array returned by `epub.getCreators()`.
1325
+ *
1326
+ * @link https://www.w3.org/TR/epub-33/#sec-opf-dccreator
1327
+ */
1328
+ async removeCreator(index, type = "creator") {
1329
+ await this.withPackage((packageElement) => {
1330
+ var _a;
1331
+ const metadata = Epub.findXmlChildByName(
1332
+ "metadata",
1333
+ Epub.getXmlChildren(packageElement)
1334
+ );
1335
+ if (!metadata)
1336
+ throw new Error(
1337
+ "Failed to parse EPUB: found no metadata element in package document"
1338
+ );
1339
+ let creatorCount = null;
1340
+ let metadataIndex = null;
1341
+ for (const meta of Epub.getXmlChildren(metadata)) {
1342
+ if (creatorCount === index) break;
1343
+ metadataIndex = metadataIndex === null ? 0 : metadataIndex + 1;
1344
+ if (Epub.isXmlTextNode(meta)) continue;
1345
+ if (Epub.getXmlElementName(meta) !== `dc:${type}`) continue;
1346
+ creatorCount = creatorCount === null ? 0 : creatorCount + 1;
1347
+ }
1348
+ if (creatorCount === null || metadataIndex === null) return;
1349
+ const [removed] = Epub.getXmlChildren(metadata).splice(metadataIndex, 1);
1350
+ if (removed && !Epub.isXmlTextNode(removed) && ((_a = removed[":@"]) == null ? void 0 : _a["@_id"])) {
1351
+ const id = removed[":@"]["@_id"];
1352
+ const newChildren = Epub.getXmlChildren(metadata).filter((node) => {
1353
+ var _a2;
1354
+ if (Epub.isXmlTextNode(node)) return true;
1355
+ if (Epub.getXmlElementName(node) !== "meta") return true;
1356
+ if (((_a2 = node[":@"]) == null ? void 0 : _a2["@_refines"]) !== `#${id}`) return true;
1357
+ return false;
1358
+ });
1359
+ Epub.replaceXmlChildren(metadata, newChildren);
1360
+ }
1361
+ });
1362
+ }
1363
+ /**
1364
+ * Remove a contributor from the EPUB metadata.
1365
+ *
1366
+ * Removes the contributor at the provided index. This index
1367
+ * refers to the array returned by `epub.getContributors()`.
1368
+ *
1369
+ * This is a convenience method for
1370
+ * `epub.removeCreator(index, 'contributor')`.
1371
+ *
1372
+ * @link https://www.w3.org/TR/epub-33/#sec-opf-dccreator
1373
+ */
1374
+ async removeContributor(index) {
1375
+ return this.removeCreator(index, "contributor");
1376
+ }
1377
+ /**
1378
+ * Add a contributor to the EPUB metadata.
1379
+ *
1380
+ * If index is provided, the creator will be placed at
1381
+ * that index in the list of creators. Otherwise, it
1382
+ * will be added to the end of the list.
1383
+ *
1384
+ * This is a convenience method for
1385
+ * `epub.addCreator(contributor, index, 'contributor')`.
1386
+ *
1387
+ * @link https://www.w3.org/TR/epub-33/#sec-opf-dccreator
1388
+ */
1389
+ addContributor(contributor, index) {
1390
+ return this.addCreator(contributor, index, "contributor");
1391
+ }
1392
+ async getSpine() {
1393
+ if (this.spine !== null) return this.spine;
1394
+ const packageElement = await this.getPackageElement();
1395
+ const spine = Epub.findXmlChildByName(
1396
+ "spine",
1397
+ Epub.getXmlChildren(packageElement)
1398
+ );
1399
+ if (!spine)
1400
+ throw new Error(
1401
+ "Failed to parse EPUB: Found no spine element in package document"
1402
+ );
1403
+ this.spine = spine["spine"].filter((node) => !Epub.isXmlTextNode(node)).map((itemref) => {
1404
+ var _a;
1405
+ return (_a = itemref[":@"]) == null ? void 0 : _a["@_idref"];
1406
+ }).filter((idref) => !!idref);
1407
+ return this.spine;
1408
+ }
1409
+ /**
1410
+ * Retrieve the manifest items that make up the Epub's spine.
1411
+ *
1412
+ * The spine specifies the order that the contents of the Epub
1413
+ * should be displayed to users by default.
1414
+ *
1415
+ * @link https://www.w3.org/TR/epub-33/#sec-spine-elem
1416
+ */
1417
+ async getSpineItems() {
1418
+ const spine = await this.getSpine();
1419
+ const manifest = await this.getManifest();
1420
+ return spine.map((itemref) => manifest[itemref]).filter((entry) => !!entry);
1421
+ }
1422
+ /**
1423
+ * Add an item to the spine of the EPUB.
1424
+ *
1425
+ * If `index` is undefined, the item will be added
1426
+ * to the end of the spine. Otherwise it will be
1427
+ * inserted at the specified index.
1428
+ *
1429
+ * If the manifestId does not correspond to an item
1430
+ * in the manifest, this will throw an error.
1431
+ *
1432
+ * @link https://www.w3.org/TR/epub-33/#sec-spine-elem
1433
+ */
1434
+ async addSpineItem(manifestId, index) {
1435
+ const item = Epub.createXmlElement("itemref", { idref: manifestId });
1436
+ const manifest = await this.getManifest();
1437
+ const manifestItem = manifest[manifestId];
1438
+ if (!manifestItem)
1439
+ throw new Error(`Manifest item not found with id "${manifestId}"`);
1440
+ await this.withPackage((packageElement) => {
1441
+ const spine = Epub.findXmlChildByName(
1442
+ "spine",
1443
+ Epub.getXmlChildren(packageElement)
1444
+ );
1445
+ if (!spine)
1446
+ throw new Error(
1447
+ "Failed to parse EPUB: Found no spine element in package document"
1448
+ );
1449
+ if (index === void 0) {
1450
+ Epub.getXmlChildren(spine).push(item);
1451
+ } else {
1452
+ Epub.getXmlChildren(spine).splice(index, 0, item);
1453
+ }
1454
+ });
1455
+ this.spine = null;
1456
+ }
1457
+ /**
1458
+ * Remove the spine item at the specified index.
1459
+ *
1460
+ * @link https://www.w3.org/TR/epub-33/#sec-spine-elem
1461
+ */
1462
+ async removeSpineItem(index) {
1463
+ await this.withPackage((packageElement) => {
1464
+ const spine = Epub.findXmlChildByName(
1465
+ "spine",
1466
+ Epub.getXmlChildren(packageElement)
1467
+ );
1468
+ if (!spine)
1469
+ throw new Error(
1470
+ "Failed to parse EPUB: Found no spine element in package document"
1471
+ );
1472
+ Epub.getXmlChildren(spine).splice(index, 1);
1473
+ });
1474
+ this.spine = null;
1475
+ }
1476
+ /**
1477
+ * Returns a Zip Entry path for an HREF
1478
+ */
1479
+ resolveHref(from, href) {
1480
+ const startPath = (0, import_path.dirname)(from);
1481
+ const absoluteStartPath = startPath.startsWith("/") ? startPath : `/${startPath}`;
1482
+ return (0, import_path.resolve)(absoluteStartPath, href).slice(1);
1483
+ }
1484
+ async readItemContents(id, encoding) {
1485
+ const rootfile = await this.getRootfile();
1486
+ const manifest = await this.getManifest();
1487
+ const manifestItem = manifest[id];
1488
+ if (!manifestItem)
1489
+ throw new Error(`Could not find item with id "${id}" in manifest`);
1490
+ const path = this.resolveHref(rootfile, manifestItem.href);
1491
+ const itemEntry = encoding ? await this.getFileData(path, encoding) : await this.getFileData(path);
1492
+ return itemEntry;
1493
+ }
1494
+ /**
1495
+ * Create a new XHTML document with the given body
1496
+ * and head.
1497
+ *
1498
+ * @param body The XML nodes to place in the body of the document
1499
+ * @param head Optional - the XMl nodes to place in the head
1500
+ * @param language Optional - defaults to the EPUB's language
1501
+ */
1502
+ async createXhtmlDocument(body, head, language) {
1503
+ const lang = language ?? await this.getLanguage();
1504
+ return [
1505
+ Epub.createXmlElement("?xml", { version: "1.0", encoding: "UTF-8" }),
1506
+ Epub.createXmlElement(
1507
+ "html",
1508
+ {
1509
+ xmlns: "http://www.w3.org/1999/xhtml",
1510
+ "xmlns:epub": "http://www.idpf.org/2007/ops",
1511
+ ...lang && { "xml:lang": lang.toString(), lang: lang.toString() }
1512
+ },
1513
+ [
1514
+ Epub.createXmlElement("head", {}, head),
1515
+ Epub.createXmlElement("body", {}, body)
1516
+ ]
1517
+ )
1518
+ ];
1519
+ }
1520
+ async readXhtmlItemContents(id, as = "xhtml") {
1521
+ const contents = await this.readItemContents(id, "utf-8");
1522
+ const xml = Epub.xhtmlParser.parse(contents);
1523
+ if (as === "xhtml") return xml;
1524
+ const body = Epub.getXhtmlBody(xml);
1525
+ return Epub.getXhtmlTextContent(body);
1526
+ }
1527
+ writeEntryContents(path, contents, encoding) {
1528
+ const data = encoding === "utf-8" ? new TextEncoder().encode(contents) : contents;
1529
+ const entry = this.getEntry(path);
1530
+ if (!entry) throw new Error(`Could not find file at ${path} in EPUB`);
1531
+ entry.setData(data);
1532
+ }
1533
+ async writeItemContents(id, contents, encoding) {
1534
+ const rootfile = await this.getRootfile();
1535
+ const manifest = await this.getManifest();
1536
+ const manifestItem = manifest[id];
1537
+ if (!manifestItem)
1538
+ throw new Error(`Could not find item with id "${id}" in manifest`);
1539
+ import_mem.default.clear(this.readXhtmlItemContents);
1540
+ const href = this.resolveHref(rootfile, manifestItem.href);
1541
+ if (encoding === "utf-8") {
1542
+ this.writeEntryContents(href, contents, encoding);
1543
+ } else {
1544
+ this.writeEntryContents(href, contents);
1545
+ }
1546
+ }
1547
+ /**
1548
+ * Write new contents for an existing XHTML item,
1549
+ * specified by its id.
1550
+ *
1551
+ * The id must reference an existing manifest item. If
1552
+ * creating a new item, use `epub.addManifestItem()` instead.
1553
+ *
1554
+ * @param id The id of the manifest item to write new contents for
1555
+ * @param contents The new contents. Must be a parsed XML tree.
1556
+ *
1557
+ * @link https://www.w3.org/TR/epub-33/#sec-xhtml
1558
+ */
1559
+ async writeXhtmlItemContents(id, contents) {
1560
+ await this.writeItemContents(
1561
+ id,
1562
+ Epub.xhtmlBuilder.build(contents),
1563
+ "utf-8"
1564
+ );
1565
+ }
1566
+ async removeManifestItem(id) {
1567
+ await this.withPackage(async (packageElement) => {
1568
+ var _a;
1569
+ const manifest = Epub.findXmlChildByName(
1570
+ "manifest",
1571
+ Epub.getXmlChildren(packageElement)
1572
+ );
1573
+ if (!manifest)
1574
+ throw new Error(
1575
+ "Failed to parse EPUB: Found no manifest element in package document"
1576
+ );
1577
+ const itemIndex = Epub.getXmlChildren(manifest).findIndex(
1578
+ (node) => {
1579
+ var _a2;
1580
+ return !Epub.isXmlTextNode(node) && ((_a2 = node[":@"]) == null ? void 0 : _a2["@_id"]) === id;
1581
+ }
1582
+ );
1583
+ if (itemIndex === -1) return;
1584
+ const [item] = Epub.getXmlChildren(manifest).splice(itemIndex, 1);
1585
+ if (!item || Epub.isXmlTextNode(item) || !((_a = item[":@"]) == null ? void 0 : _a["@_href"])) return;
1586
+ await this.removeEntry(item[":@"]["@_href"]);
1587
+ });
1588
+ this.manifest = null;
1589
+ }
1590
+ async addManifestItem(item, contents, encoding) {
1591
+ await this.withPackage((packageElement) => {
1592
+ const manifest = Epub.findXmlChildByName(
1593
+ "manifest",
1594
+ Epub.getXmlChildren(packageElement)
1595
+ );
1596
+ if (!manifest)
1597
+ throw new Error(
1598
+ "Failed to parse EPUB: Found no manifest element in package document"
1599
+ );
1600
+ Epub.getXmlChildren(manifest).push(
1601
+ Epub.createXmlElement("item", {
1602
+ id: item.id,
1603
+ href: item.href,
1604
+ ...item.mediaType && { "media-type": item.mediaType },
1605
+ ...item.fallback && { fallback: item.fallback },
1606
+ ...item.mediaOverlay && { "media-overlay": item.mediaOverlay },
1607
+ ...item.properties && { properties: item.properties.join(" ") }
1608
+ })
1609
+ );
1610
+ });
1611
+ this.manifest = null;
1612
+ const rootfile = await this.getRootfile();
1613
+ const filename = this.resolveHref(rootfile, item.href);
1614
+ const data = encoding === "utf-8" || encoding === "xml" ? new TextEncoder().encode(
1615
+ encoding === "utf-8" ? contents : await Epub.xmlBuilder.build(
1616
+ contents
1617
+ )
1618
+ ) : contents;
1619
+ this.entries.push(new EpubEntry({ filename, data }));
1620
+ }
1621
+ /**
1622
+ * Update the manifest entry for an existing item.
1623
+ *
1624
+ * To update the contents of an entry, use `epub.writeItemContents()`
1625
+ * or `epub.writeXhtmlItemContents()`
1626
+ *
1627
+ * @link https://www.w3.org/TR/epub-33/#sec-pkg-manifest
1628
+ */
1629
+ async updateManifestItem(id, newItem) {
1630
+ await this.withPackage((packageElement) => {
1631
+ const manifest = Epub.findXmlChildByName(
1632
+ "manifest",
1633
+ Epub.getXmlChildren(packageElement)
1634
+ );
1635
+ if (!manifest)
1636
+ throw new Error(
1637
+ "Failed to parse EPUB: Found no manifest element in package document"
1638
+ );
1639
+ const itemIndex = manifest["manifest"].findIndex(
1640
+ (item) => {
1641
+ var _a;
1642
+ return !Epub.isXmlTextNode(item) && ((_a = item[":@"]) == null ? void 0 : _a["@_id"]) === id;
1643
+ }
1644
+ );
1645
+ Epub.getXmlChildren(manifest).splice(
1646
+ itemIndex,
1647
+ 1,
1648
+ Epub.createXmlElement("item", {
1649
+ id,
1650
+ href: newItem.href,
1651
+ ...newItem.mediaType && { "media-type": newItem.mediaType },
1652
+ ...newItem.fallback && { fallback: newItem.fallback },
1653
+ ...newItem.mediaOverlay && {
1654
+ "media-overlay": newItem.mediaOverlay
1655
+ },
1656
+ ...newItem.properties && {
1657
+ properties: newItem.properties.join(" ")
1658
+ }
1659
+ })
1660
+ );
1661
+ });
1662
+ this.manifest = null;
1663
+ }
1664
+ /**
1665
+ * Add a new metadata entry to the Epub.
1666
+ *
1667
+ * This method, like `epub.getMetadata()`, operates on
1668
+ * metadata entries. For more useful semantic representations
1669
+ * of metadata, use specific methods such as `setTitle()` and
1670
+ * `setLanguage()`.
1671
+ *
1672
+ * @link https://www.w3.org/TR/epub-33/#sec-pkg-metadata
1673
+ */
1674
+ async addMetadata(entry) {
1675
+ await this.withPackage((packageElement) => {
1676
+ const metadata = Epub.findXmlChildByName(
1677
+ "metadata",
1678
+ Epub.getXmlChildren(packageElement)
1679
+ );
1680
+ if (!metadata)
1681
+ throw new Error(
1682
+ "Failed to parse EPUB: found no metadata element in package document"
1683
+ );
1684
+ Epub.getXmlChildren(metadata).push(
1685
+ Epub.createXmlElement(
1686
+ entry.type,
1687
+ {
1688
+ ...entry.id && { id: entry.id },
1689
+ ...entry.properties
1690
+ },
1691
+ entry.value !== void 0 ? [Epub.createXmlTextNode(entry.value)] : []
1692
+ )
1693
+ );
1694
+ });
1695
+ }
1696
+ /**
1697
+ * Replace a metadata entry with a new one.
1698
+ *
1699
+ * The `predicate` argument will be used to determine which entry
1700
+ * to replace. The first metadata entry that matches the
1701
+ * predicate will be replaced.
1702
+ *
1703
+ * @param predicate Calls predicate once for each metadata entry,
1704
+ * until it finds one where predicate returns true
1705
+ * @param entry The new entry to replace the found entry with
1706
+ *
1707
+ * @link https://www.w3.org/TR/epub-33/#sec-pkg-metadata
1708
+ */
1709
+ async replaceMetadata(predicate, entry) {
1710
+ await this.withPackage((packageElement) => {
1711
+ const metadataElement = Epub.findXmlChildByName(
1712
+ "metadata",
1713
+ Epub.getXmlChildren(packageElement)
1714
+ );
1715
+ if (!metadataElement)
1716
+ throw new Error(
1717
+ "Failed to parse EPUB: found no metadata element in package document"
1718
+ );
1719
+ const oldEntryIndex = this.findMetadataIndex(packageElement, predicate);
1720
+ const newElement = Epub.createXmlElement(
1721
+ entry.type,
1722
+ {
1723
+ ...entry.id && { id: entry.id },
1724
+ ...entry.properties
1725
+ },
1726
+ entry.value !== void 0 ? [Epub.createXmlTextNode(entry.value)] : []
1727
+ );
1728
+ if (oldEntryIndex === -1) {
1729
+ metadataElement.metadata.push(newElement);
1730
+ } else {
1731
+ metadataElement.metadata.splice(oldEntryIndex, 1, newElement);
1732
+ }
1733
+ });
1734
+ }
1735
+ /**
1736
+ * Remove one or more metadata entries.
1737
+ *
1738
+ * The `predicate` argument will be used to determine which entries
1739
+ * to remove. The all metadata entries that match the
1740
+ * predicate will be removed.
1741
+ *
1742
+ * @param predicate Calls predicate once for each metadata entry,
1743
+ * removing any for which it returns true
1744
+ *
1745
+ * @link https://www.w3.org/TR/epub-33/#sec-pkg-metadata
1746
+ */
1747
+ async removeMetadata(predicate) {
1748
+ await this.withPackage((packageElement) => {
1749
+ const metadataElement = Epub.findXmlChildByName(
1750
+ "metadata",
1751
+ Epub.getXmlChildren(packageElement)
1752
+ );
1753
+ if (!metadataElement) {
1754
+ throw new Error(
1755
+ "Failed to parse EPUB: found no metadata element in package document"
1756
+ );
1757
+ }
1758
+ const metadataEntries = Epub.getXmlChildren(metadataElement);
1759
+ for (let i = metadataEntries.length - 1; i >= 0; i--) {
1760
+ const meta = metadataEntries[i];
1761
+ const item = Epub.parseMetadataItem(meta);
1762
+ if (!item) continue;
1763
+ if (predicate(item)) {
1764
+ metadataEntries.splice(i, 1);
1765
+ }
1766
+ }
1767
+ });
1768
+ }
1769
+ /**
1770
+ * Write the current contents of the Epub to a new
1771
+ * Uint8Array.
1772
+ *
1773
+ * This _does not_ close the Epub. It can continue to
1774
+ * be modified after it has been written to disk. Use
1775
+ * `epub.close()` to close the Epub.
1776
+ *
1777
+ * When this method is called, the "dcterms:modified"
1778
+ * meta tag is automatically updated to the current UTC
1779
+ * timestamp.
1780
+ */
1781
+ async writeToArray() {
1782
+ await this.replaceMetadata(
1783
+ (entry) => entry.properties["property"] === "dcterms:modified",
1784
+ {
1785
+ type: "meta",
1786
+ properties: { property: "dcterms:modified" },
1787
+ // We need UTC with integer seconds, but toISOString gives UTC with ms
1788
+ value: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+/, "")
1789
+ }
1790
+ );
1791
+ let mimetypeEntry = this.getEntry("mimetype");
1792
+ if (!mimetypeEntry) {
1793
+ mimetypeEntry = new EpubEntry({
1794
+ filename: "mimetype",
1795
+ data: new TextEncoder().encode("application/epub+zip")
1796
+ });
1797
+ this.entries.push(mimetypeEntry);
1798
+ }
1799
+ const mimetypeReader = new import_zip.Uint8ArrayReader(await mimetypeEntry.getData());
1800
+ try {
1801
+ await this.zipWriter.add(mimetypeEntry.filename, mimetypeReader, {
1802
+ level: 0,
1803
+ extendedTimestamp: false
1804
+ });
1805
+ } catch (e) {
1806
+ if (e instanceof Error && e.message === import_zip.ERR_DUPLICATED_NAME) {
1807
+ throw new Error(
1808
+ `Failed to add file "${mimetypeEntry.filename}" to zip archive: ${e.message}`
1809
+ );
1810
+ }
1811
+ throw e;
1812
+ }
1813
+ await Promise.all(
1814
+ this.entries.map(async (entry) => {
1815
+ if (entry.filename === "mimetype") return;
1816
+ const reader = new import_zip.Uint8ArrayReader(await entry.getData());
1817
+ try {
1818
+ return await this.zipWriter.add(entry.filename, reader);
1819
+ } catch (e) {
1820
+ if (e instanceof Error && e.message === import_zip.ERR_DUPLICATED_NAME) {
1821
+ throw new Error(
1822
+ `Failed to add file "${entry.filename}" to zip archive: ${e.message}`
1823
+ );
1824
+ }
1825
+ throw e;
1826
+ }
1827
+ })
1828
+ );
1829
+ const data = await this.zipWriter.close();
1830
+ this.dataWriter = new import_zip.Uint8ArrayWriter();
1831
+ this.zipWriter = new import_zip.ZipWriter(this.dataWriter);
1832
+ return data;
1833
+ }
1834
+ }
1835
+ // Annotate the CommonJS export names for ESM import in node:
1836
+ 0 && (module.exports = {
1837
+ Epub
1838
+ });