@storyteller-platform/epub 0.8.2 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,20 +1,25 @@
1
- import { cp, mkdir } from "node:fs/promises";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@storyteller-platform/epub/jsx-runtime";
2
2
  import { Mutex } from "async-mutex";
3
- import XMLBuilder from "fast-xml-builder";
4
- import { XMLParser } from "fast-xml-parser";
5
3
  import memoize from "mem";
6
- import { nanoid } from "nanoid";
7
4
  import { MediaType } from "@storyteller-platform/media-types";
8
- import { dirname } from "@storyteller-platform/path";
9
- import { TmpFsAdapter } from "./adapters/tmpfs.js";
5
+ import { copyFile } from "#files";
6
+ import { TmpFsAdapter } from "#tmpfs";
10
7
  import * as Upgrade from "./upgrade.js";
8
+ import { createDocument, createDocumentType, declareDefaultNamespace, findXmlChildByName, findXmlChildByNameNS, findXmlDescendantByName, getXhtmlBody, insert, isElement, isText, NS, parse, replace, serialize, text, x, xmlId, } from "./xml.js";
9
+ export { TmpFsAdapter } from "#tmpfs";
11
10
  export { MemoryAdapter } from "./adapters/memory.js";
12
- export { TmpFsAdapter } from "./adapters/tmpfs.js";
11
+ /**
12
+ * The XML surface. Publication-level operations live on {@link Epub}; for
13
+ * building and inspecting XML trees directly, use these.
14
+ *
15
+ * The DOM node types are re-exported so that callers do not need to depend on
16
+ * `@xmldom/xmldom` themselves.
17
+ */
18
+ export { addLinkToXhtmlHead, adoptNamespaces, createDocument, createDocumentType, declareDefaultNamespace, findXmlChildByName, findXmlChildByNameNS, findXmlDescendantByName, findXmlDescendantByNameNS, getXhtmlBody, insert, isCdata, isComment, isElement, isText, NS, parse, replace, serialize, text, x, XMLSerializer, } from "./xml.js";
13
19
  export class EpubVersionError extends Error {
14
20
  }
15
21
  export class EpubReadOnlyError extends Error {
16
22
  }
17
- const OPF_NAMESPACE = "http://www.idpf.org/2007/opf";
18
23
  /**
19
24
  * makes it easy to distinguish normal hrefs from external/invalid ones
20
25
  */
@@ -34,52 +39,48 @@ function resolveEntryUrl(from, href) {
34
39
  const url = new URL(href, base);
35
40
  return url.href.startsWith(ENTRY_ORIGIN) ? url : null;
36
41
  }
37
- function renameOpfAttributes(attrs, prefix) {
38
- const attrPrefix = `@_${prefix}:`;
39
- const renamed = {};
40
- for (const [key, value] of Object.entries(attrs)) {
41
- if (key === `@_xmlns:${prefix}`)
42
- continue;
43
- if (key.startsWith(attrPrefix)) {
44
- renamed[`@_opf:${key.slice(attrPrefix.length)}`] = value;
45
- }
46
- else {
47
- renamed[key] = value;
48
- }
42
+ function ensureOpfNamespace(doc) {
43
+ const { documentElement } = doc;
44
+ if (!documentElement)
45
+ return;
46
+ if (!documentElement.hasAttribute("xmlns")) {
47
+ declareDefaultNamespace(documentElement, NS.opf);
49
48
  }
50
- return renamed;
51
49
  }
52
- function stripOpfPrefix(node, prefix) {
53
- if (Epub.isXmlTextNode(node))
54
- return node;
55
- const name = Epub.getXmlElementName(node);
56
- const bareName = name.startsWith(`${prefix}:`)
57
- ? name.slice(prefix.length + 1)
58
- : name;
59
- const element = {
60
- [bareName]: Epub.getXmlChildren(node).map((child) => stripOpfPrefix(child, prefix)),
61
- };
62
- if (node[":@"]) {
63
- element[":@"] = renameOpfAttributes(node[":@"], prefix);
64
- }
65
- return element;
50
+ /**
51
+ * Names within a directory must stay distinct after Unicode normalization and
52
+ * full case folding, since names that collapse onto one another overwrite each
53
+ * other on any filesystem that normalizes.
54
+ *
55
+ * @link https://www.w3.org/TR/epub-33/#sec-names-paths
56
+ */
57
+ function foldPathName(path) {
58
+ return path.normalize("NFKC").toLowerCase();
66
59
  }
67
- function normalizeOpfNamespacePrefixes(doc) {
68
- const root = doc.find((node) => !Epub.isXmlTextNode(node) &&
69
- Epub.getXmlElementName(node).endsWith(":package"));
70
- if (!root)
71
- return doc;
72
- const prefix = Epub.getXmlElementName(root).slice(0, -":package".length);
73
- if (root[":@"]?.[`@_xmlns:${prefix}`] !== OPF_NAMESPACE)
74
- return doc;
75
- const stripped = stripOpfPrefix(root, prefix);
76
- stripped[":@"] = {
77
- ...stripped[":@"],
78
- "@_xmlns": OPF_NAMESPACE,
79
- "@_xmlns:opf": OPF_NAMESPACE,
80
- };
81
- doc[doc.indexOf(root)] = stripped;
82
- return doc;
60
+ /** The media types that make a manifest item an EPUB content document. */
61
+ const CONTENT_DOCUMENT_MEDIA_TYPES = new Set([
62
+ MediaType.XHTML.mime,
63
+ MediaType.SVG.mime,
64
+ ]);
65
+ /**
66
+ * Whether a manifest item may be referenced from the spine: it must be an EPUB
67
+ * content document, or a foreign content document whose manifest fallback chain
68
+ * reaches one.
69
+ *
70
+ * @link https://www.w3.org/TR/epub-33/#sec-item-elem
71
+ */
72
+ function spineReferenceable(manifest, item) {
73
+ const visited = new Set();
74
+ let current = item;
75
+ while (current && !visited.has(current.id)) {
76
+ visited.add(current.id);
77
+ if (current.mediaType &&
78
+ CONTENT_DOCUMENT_MEDIA_TYPES.has(current.mediaType)) {
79
+ return true;
80
+ }
81
+ current = current.fallback ? manifest[current.fallback] : undefined;
82
+ }
83
+ return false;
83
84
  }
84
85
  /**
85
86
  * A single EPUB instance.
@@ -91,19 +92,18 @@ function normalizeOpfNamespacePrefixes(doc) {
91
92
  * ```ts
92
93
  * import { Epub, getBody, findByName, textContent } from '@storyteller-platform/epub';
93
94
  *
94
- * const epub = await Epub.from('./path/to/book.epub');
95
+ * using epub = await Epub.from('./path/to/book.epub');
95
96
  * const title = await epub.getTitle();
96
97
  * const spineItems = await epub.getSpineItems();
97
98
  * const chptOne = spineItems[0];
98
99
  * const chptOneXml = await epub.readXhtmlItemContents(chptOne.id);
99
100
  *
100
101
  * const body = getBody(chptOneXml);
101
- * const h1 = Epub.findXmlChildByName('h1', body);
102
+ * const h1 = findXmlChildByName('h1', body);
102
103
  * const headingText = textContent(h1);
103
104
  *
104
105
  * await epub.setTitle(headingText);
105
- * await epub.writeToFile('./path/to/updated.epub');
106
- * await epub.close();
106
+ * await epub.saveAndClose();
107
107
  * ```
108
108
  *
109
109
  * @link https://www.w3.org/TR/epub-33/
@@ -113,65 +113,6 @@ export class Epub {
113
113
  adapter;
114
114
  inputPath;
115
115
  readonlyOverride;
116
- static xmlParser = new XMLParser({
117
- allowBooleanAttributes: true,
118
- preserveOrder: true,
119
- ignoreAttributes: false,
120
- parseTagValue: false,
121
- });
122
- static xhtmlParser = (() => {
123
- const parser = new XMLParser({
124
- allowBooleanAttributes: true,
125
- alwaysCreateTextNode: true,
126
- preserveOrder: true,
127
- ignoreAttributes: false,
128
- // oxlint-disable-next-line typescript/no-deprecated entityDecoder appears completely undocumented
129
- htmlEntities: true,
130
- processEntities: {
131
- enabled: true,
132
- maxEntityCount: Infinity,
133
- maxTotalExpansions: Infinity,
134
- },
135
- trimValues: false,
136
- stopNodes: ["*.script"],
137
- parseTagValue: false,
138
- updateTag(_tagName, _jPath, attrs) {
139
- // There's never an attribute called '/';
140
- // this erroneously happens sometimes when parsing
141
- // self-closing stop nodes with ignoreAttributes: false
142
- // and allowBooleanAttributes: true.
143
- //
144
- // Also attrs is undefined if there are no attrs;
145
- // the types are wrong.
146
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
147
- if (attrs && "@_/" in attrs) {
148
- delete attrs["@_/"];
149
- }
150
- return true;
151
- },
152
- });
153
- // fast-xml-parser's htmlEntities option seems to map   to
154
- // regular space (U+0020) instead of non-breaking space (U+00A0).
155
- // Override the entity mapping to use the correct character.
156
- // oxlint-disable-next-line typescript/no-deprecated entityDecoder appears completely undocumented
157
- parser.addEntity("nbsp", "\u00A0");
158
- // oxlint-disable-next-line typescript/no-deprecated entityDecoder appears completely undocumented
159
- parser.addEntity("#160", "\u00A0");
160
- return parser;
161
- })();
162
- static xmlBuilder = new XMLBuilder({
163
- preserveOrder: true,
164
- format: true,
165
- ignoreAttributes: false,
166
- suppressEmptyNode: true,
167
- });
168
- static xhtmlBuilder = new XMLBuilder({
169
- preserveOrder: true,
170
- ignoreAttributes: false,
171
- stopNodes: ["*.script"],
172
- suppressEmptyNode: true,
173
- processEntities: true,
174
- });
175
116
  /**
176
117
  * Format a duration, provided as a number of seconds, as
177
118
  * a SMIL clock value, to be used for Media Overlays.
@@ -188,135 +129,6 @@ export class Epub {
188
129
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
189
130
  return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.padStart(2, "0")}.${millis ?? "0"}`;
190
131
  }
191
- /**
192
- * Given an XML structure representing a complete XHTML document,
193
- * add a `link` element to the `head` of the document.
194
- *
195
- * This method modifies the provided XML structure.
196
- */
197
- static addLinkToXhtmlHead(xml, link) {
198
- const html = Epub.findXmlChildByName("html", xml);
199
- if (!html)
200
- throw new Error("Invalid XHTML document: no html element");
201
- const head = Epub.findXmlChildByName("head", html.html);
202
- if (!head)
203
- throw new Error("Invalid XHTML document: no head element");
204
- head["head"].push({
205
- link: [],
206
- ":@": {
207
- "@_rel": link.rel,
208
- "@_href": link.href,
209
- "@_type": link.type,
210
- },
211
- });
212
- }
213
- /**
214
- * Given an XML structure representing a complete XHTML document,
215
- * return the sub-structure representing the children of the
216
- * document's body element.
217
- */
218
- static getXhtmlBody(xml) {
219
- const html = Epub.findXmlChildByName("html", xml);
220
- if (!html)
221
- throw new Error("Invalid XHTML document: no html element");
222
- const body = Epub.findXmlChildByName("body", html["html"]);
223
- if (!body)
224
- throw new Error("Invalid XHTML document: No body element");
225
- return body["body"];
226
- }
227
- static createXmlElement(name, properties, children = []) {
228
- return {
229
- ":@": Object.fromEntries(Object.entries(properties).map(([prop, value]) => [`@_${prop}`, value])),
230
- [name]: children,
231
- };
232
- }
233
- static createXmlTextNode(text) {
234
- return { ["#text"]: text };
235
- }
236
- /**
237
- * Given an XML structure representing a complete XHTML document,
238
- * return a string representing the concatenation of all text nodes
239
- * in the document.
240
- */
241
- static getXhtmlTextContent(xml) {
242
- let text = "";
243
- for (const child of xml) {
244
- if (Epub.isXmlTextNode(child)) {
245
- text += child["#text"];
246
- continue;
247
- }
248
- const children = Epub.getXmlChildren(child);
249
- text += Epub.getXhtmlTextContent(children);
250
- }
251
- return text;
252
- }
253
- /**
254
- * Given an XMLElement, return its attributes.
255
- */
256
- static getXmlAttributes(element) {
257
- return Object.fromEntries(Object.entries(element[":@"] ?? {}).map(([key, value]) => [
258
- key.slice(2),
259
- value,
260
- ]));
261
- }
262
- /**
263
- * Given an XMLElement, return its tag name.
264
- */
265
- static getXmlElementName(element) {
266
- const keys = Object.keys(element);
267
- const elementName = keys.find((key) => key !== ":@" && key !== "#text");
268
- if (!elementName)
269
- throw new Error(`Invalid XML Element: missing tag name\n${JSON.stringify(element, null, 2)}`);
270
- return elementName;
271
- }
272
- /**
273
- * Given an XMLElement, return a list of its children
274
- */
275
- static getXmlChildren(element) {
276
- const elementName = Epub.getXmlElementName(element);
277
- // It's not clear to me why this needs to be cast
278
- return element[elementName];
279
- }
280
- static replaceXmlChildren(element, children) {
281
- const elementName = Epub.getXmlElementName(element);
282
- element[elementName] = children;
283
- }
284
- /**
285
- * Given an XML structure, find the first child matching
286
- * the provided name and optional filter.
287
- */
288
- static findXmlChildByName(name, xml, filter) {
289
- const element = xml.find((e) => name in e && (filter ? filter(e) : true));
290
- return element;
291
- }
292
- /**
293
- * Given an XML structure, find the first descendant matching
294
- * the provided name and optional filter.
295
- *
296
- * Will perform a breadth first search for the element, returning
297
- * the highest element in the tree matching the name and filter.
298
- */
299
- static findXmlDescendantByName(name, xml, filter) {
300
- const found = Epub.findXmlChildByName(name, xml, filter);
301
- if (found)
302
- return found;
303
- for (const node of xml) {
304
- if (Epub.isXmlTextNode(node))
305
- continue;
306
- const children = Epub.getXmlChildren(node);
307
- const found = this.findXmlDescendantByName(name, children, filter);
308
- if (found)
309
- return found;
310
- }
311
- return undefined;
312
- }
313
- /**
314
- * Given an XMLNode, determine whether it represents
315
- * a text node or an XML element.
316
- */
317
- static isXmlTextNode(node) {
318
- return "#text" in node;
319
- }
320
132
  rootfile = null;
321
133
  manifest = null;
322
134
  spine = null;
@@ -425,16 +237,15 @@ export class Epub {
425
237
  const containerString = await this.adapter.read("META-INF/container.xml", "utf-8");
426
238
  if (!containerString)
427
239
  throw new Error("Failed to parse EPUB: Missing META-INF/container.xml");
428
- const containerDocument = Epub.xmlParser.parse(containerString);
429
- const container = Epub.findXmlChildByName("container", containerDocument);
240
+ const containerDocument = parse(containerString, "text/xml");
241
+ const container = containerDocument.documentElement;
430
242
  if (!container)
431
243
  throw new Error("Failed to parse EPUB container.xml: Found no container element");
432
- const rootfiles = Epub.findXmlChildByName("rootfiles", Epub.getXmlChildren(container));
244
+ const rootfiles = findXmlChildByName("rootfiles", container);
433
245
  if (!rootfiles)
434
246
  throw new Error("Failed to parse EPUB container.xml: Found no rootfiles element");
435
- const rootfile = Epub.findXmlChildByName("rootfile", Epub.getXmlChildren(rootfiles), (node) => !Epub.isXmlTextNode(node) &&
436
- node[":@"]?.["@_media-type"] === MediaType.OPF.mime);
437
- const fullPath = rootfile?.[":@"]?.["@_full-path"];
247
+ const rootfile = findXmlChildByName("rootfile", rootfiles, (element) => element.getAttribute("media-type") === MediaType.OPF.mime);
248
+ const fullPath = rootfile?.getAttribute("full-path");
438
249
  if (!fullPath)
439
250
  throw new Error("Failed to parse EPUB container.xml: Found no rootfile element");
440
251
  this.rootfile = await this.resolveEntry("", fullPath);
@@ -448,12 +259,15 @@ export class Epub {
448
259
  return packageDocumentString;
449
260
  }
450
261
  async getPackageDocument() {
451
- const packageDocument = Epub.xmlParser.parse(await this.getPackageDocumentSource());
452
- return normalizeOpfNamespacePrefixes(packageDocument);
262
+ const packageDocument = parse(await this.getPackageDocumentSource(), "text/xml");
263
+ ensureOpfNamespace(packageDocument);
264
+ return packageDocument;
453
265
  }
454
266
  async getPackageElement() {
455
267
  const packageDocument = await this.getPackageDocument();
456
- const packageElement = Epub.findXmlChildByName("package", packageDocument);
268
+ const packageElement = packageDocument.documentElement?.localName === "package"
269
+ ? packageDocument.documentElement
270
+ : null;
457
271
  if (!packageElement) {
458
272
  throw new Error("Failed to parse EPUB: Found no package element in package document");
459
273
  }
@@ -475,12 +289,14 @@ export class Epub {
475
289
  this.assertWritable();
476
290
  await this.packageMutex.runExclusive(async () => {
477
291
  const packageDocument = await this.getPackageDocument();
478
- const packageElement = Epub.findXmlChildByName("package", packageDocument);
292
+ const packageElement = packageDocument.documentElement?.localName === "package"
293
+ ? packageDocument.documentElement
294
+ : null;
479
295
  if (!packageElement) {
480
296
  throw new Error("Failed to parse EPUB: Found no package element in package document");
481
297
  }
482
- const produced = (await producer(packageElement));
483
- const updatedPackageDocument = Epub.xmlBuilder.build(produced ?? packageDocument);
298
+ const produced = await producer(packageElement);
299
+ const updatedPackageDocument = serialize(produced ?? packageDocument);
484
300
  const rootfile = await this.getRootfile();
485
301
  await this.writeEntryContents(rootfile, updatedPackageDocument, "utf-8");
486
302
  });
@@ -497,24 +313,25 @@ export class Epub {
497
313
  if (this.manifest !== null)
498
314
  return this.manifest;
499
315
  const packageElement = await this.getPackageElement();
500
- const manifest = Epub.findXmlChildByName("manifest", Epub.getXmlChildren(packageElement));
501
- if (!manifest)
316
+ const manifest = findXmlChildByNameNS(NS.opf, "manifest", packageElement);
317
+ if (!manifest) {
502
318
  throw new Error("Failed to parse EPUB: Found no manifest element in package document");
503
- this.manifest = Epub.getXmlChildren(manifest).reduce((acc, item) => {
504
- if (Epub.isXmlTextNode(item))
505
- return acc;
506
- if (!item[":@"]?.["@_id"] || !item[":@"]["@_href"]) {
319
+ }
320
+ this.manifest = Array.from(manifest.children).reduce((acc, item) => {
321
+ const id = item.getAttribute("id");
322
+ const href = item.getAttribute("href");
323
+ if (!id || !href) {
507
324
  return acc;
508
325
  }
509
326
  return {
510
327
  ...acc,
511
- [item[":@"]["@_id"]]: {
512
- id: item[":@"]["@_id"],
513
- href: item[":@"]["@_href"],
514
- mediaType: item[":@"]["@_media-type"],
515
- mediaOverlay: item[":@"]["@_media-overlay"],
516
- fallback: item[":@"]["@_fallback"],
517
- properties: item[":@"]["@_properties"]?.split(" "),
328
+ [id]: {
329
+ id,
330
+ href,
331
+ mediaType: item.getAttribute("media-type") ?? undefined,
332
+ mediaOverlay: item.getAttribute("media-overlay") ?? undefined,
333
+ fallback: item.getAttribute("fallback") ?? undefined,
334
+ properties: item.getAttribute("properties")?.split(" ") ?? undefined,
518
335
  },
519
336
  };
520
337
  }, {});
@@ -531,13 +348,11 @@ export class Epub {
531
348
  * array, this method is only meant to be used internally.
532
349
  */
533
350
  findMetadataIndex(packageElement, predicate) {
534
- const metadataElement = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
351
+ const metadataElement = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
535
352
  if (!metadataElement)
536
353
  throw new Error("Failed to parse EPUB: Found no metadata element in package document");
537
- return metadataElement.metadata.findIndex((node) => {
354
+ return Array.from(metadataElement.children).findIndex((node) => {
538
355
  const item = Epub.parseMetadataItem(node);
539
- if (!item)
540
- return false;
541
356
  return predicate(item);
542
357
  });
543
358
  }
@@ -555,36 +370,26 @@ export class Epub {
555
370
  */
556
371
  async findAllMetadataItems(predicate) {
557
372
  const packageElement = await this.getPackageElement();
558
- const metadataElement = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
373
+ const metadataElement = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
559
374
  if (!metadataElement)
560
375
  throw new Error("Failed to parse EPUB: Found no metadata element in package document");
561
- const elements = metadataElement.metadata.filter((node) => {
376
+ const elements = metadataElement.children.filter((node) => {
562
377
  const item = Epub.parseMetadataItem(node);
563
- if (!item)
564
- return false;
565
378
  return predicate(item);
566
379
  });
567
- return elements
568
- .map((element) => Epub.parseMetadataItem(element))
569
- .filter((item) => !!item);
380
+ return elements.map((element) => Epub.parseMetadataItem(element));
570
381
  }
571
- static parseMetadataItem(node) {
572
- if (Epub.isXmlTextNode(node))
573
- return null;
574
- const elementName = Epub.getXmlElementName(node);
575
- const textNode = Epub.getXmlChildren(node)[0];
382
+ static parseMetadataItem(element) {
383
+ const elementName = element.nodeName;
384
+ const textNode = element.firstChild;
576
385
  // https://www.w3.org/TR/epub-33/#sec-metadata-values
577
386
  // Whitespace within these element values is not significant.
578
387
  // Sequences of one or more whitespace characters are collapsed
579
388
  // to a single space [infra] during processing .
580
- const value = !textNode || !Epub.isXmlTextNode(textNode)
389
+ const value = !textNode || !isText(textNode)
581
390
  ? undefined
582
- : textNode["#text"].replaceAll(/\s+/g, " ");
583
- const attributes = node[":@"] ?? {};
584
- const { id, ...properties } = Object.fromEntries(Object.entries(attributes).map(([attrName, value]) => [
585
- attrName.slice(2),
586
- value,
587
- ]));
391
+ : textNode.data.replaceAll(/\s+/g, " ").trim();
392
+ const { id, ...properties } = Object.fromEntries(Array.from(element.attributes).map((attr) => [attr.name, attr.value]));
588
393
  return {
589
394
  id,
590
395
  type: elementName,
@@ -605,12 +410,10 @@ export class Epub {
605
410
  */
606
411
  async getMetadata() {
607
412
  const packageElement = await this.getPackageElement();
608
- const metadataElement = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
413
+ const metadataElement = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
609
414
  if (!metadataElement)
610
415
  throw new Error("Failed to parse EPUB: Found no metadata element in package document");
611
- const metadata = metadataElement.metadata
612
- .map((node) => Epub.parseMetadataItem(node))
613
- .filter((node) => !!node);
416
+ const metadata = Array.from(metadataElement.children).map((node) => Epub.parseMetadataItem(node));
614
417
  return metadata;
615
418
  }
616
419
  /**
@@ -675,31 +478,23 @@ export class Epub {
675
478
  */
676
479
  async setUniqueIdentifier(identifier) {
677
480
  await this.withPackage(async (packageElement) => {
678
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
481
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
679
482
  if (!metadata) {
680
483
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
681
484
  }
682
485
  let uniqueId = await this.getUniqueIdentifierId();
683
486
  if (!uniqueId) {
684
487
  // Create a new unique identifier id
685
- const newUniqueId = nanoid();
686
- packageElement[":@"] = {
687
- ...packageElement[":@"],
688
- "@_unique-identifier": newUniqueId,
689
- };
488
+ const newUniqueId = xmlId();
489
+ packageElement.setAttribute("unique-identifier", newUniqueId);
690
490
  uniqueId = newUniqueId;
691
491
  }
692
- const children = Epub.getXmlChildren(metadata);
693
- const entry = Epub.findXmlChildByName("dc:identifier", children, (node) => node[":@"]?.["@_id"] === uniqueId);
492
+ const entry = findXmlChildByNameNS(NS.dc, "identifier", metadata, (node) => node.getAttribute("id") === uniqueId);
694
493
  if (entry) {
695
- children.splice(children.indexOf(entry), 1, Epub.createXmlElement("dc:identifier", { id: uniqueId }, [
696
- Epub.createXmlTextNode(identifier),
697
- ]));
494
+ replace(metadata, _jsx("dc:identifier", { id: uniqueId, children: identifier }), entry);
698
495
  return;
699
496
  }
700
- children.push(Epub.createXmlElement("dc:identifier", { id: uniqueId }, [
701
- Epub.createXmlTextNode(identifier),
702
- ]));
497
+ insert(metadata, _jsx("dc:identifier", { id: uniqueId, children: identifier }));
703
498
  });
704
499
  }
705
500
  /**
@@ -710,7 +505,7 @@ export class Epub {
710
505
  */
711
506
  async getUniqueIdentifierId() {
712
507
  const packageElement = await this.getPackageElement();
713
- return packageElement[":@"]?.["@_unique-identifier"] ?? null;
508
+ return packageElement.getAttribute("unique-identifier") ?? null;
714
509
  }
715
510
  /**
716
511
  * Collect `dc:identifier` or `dc:source` entries, attaching the value and
@@ -855,47 +650,37 @@ export class Epub {
855
650
  async setIdentifiers(identifiers) {
856
651
  const uniqueId = await this.getUniqueIdentifierId();
857
652
  await this.withPackage((packageElement) => {
858
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
859
- if (!metadata)
653
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
654
+ if (!metadata) {
860
655
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
861
- const children = Epub.getXmlChildren(metadata);
656
+ }
862
657
  const removedIds = new Set();
863
- for (let i = children.length - 1; i >= 0; i--) {
864
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
865
- const node = children[i];
866
- if (Epub.isXmlTextNode(node) ||
867
- Epub.getXmlElementName(node) !== "dc:identifier") {
658
+ for (const child of Array.from(metadata.children)) {
659
+ if (child.nodeName !== "dc:identifier") {
868
660
  continue;
869
661
  }
870
- const id = node[":@"]?.["@_id"];
662
+ const id = child.getAttribute("id");
871
663
  if (id && id === uniqueId) {
872
664
  continue;
873
665
  }
874
666
  if (id) {
875
667
  removedIds.add(id);
876
668
  }
877
- children.splice(i, 1);
669
+ metadata.removeChild(child);
878
670
  }
879
- Epub.removeRefiningMetas(children, removedIds, ["identifier-type"]);
671
+ Epub.removeRefiningMetas(metadata, removedIds, ["identifier-type"]);
880
672
  for (const identifier of identifiers) {
881
673
  // we don't allow you to set the unique identifier, you need to use setUniqueIdentifier
882
674
  if (identifier.id && identifier.id === uniqueId)
883
675
  continue;
884
676
  const id = identifier.id ??
885
- (identifier.identifierType !== undefined ? nanoid() : undefined);
886
- children.push(Epub.createXmlElement("dc:identifier", {
887
- ...(id && { id }),
888
- ...(identifier.scheme &&
889
- identifier.identifierType === undefined && {
677
+ (identifier.identifierType !== undefined ? xmlId() : undefined);
678
+ insert(metadata, _jsx("dc:identifier", { ...(id && { id }), ...(identifier.scheme &&
679
+ identifier.id === undefined && {
890
680
  "opf:scheme": identifier.scheme,
891
- }),
892
- }, [Epub.createXmlTextNode(identifier.value)]));
681
+ }), children: identifier.value }));
893
682
  if (identifier.identifierType !== undefined && id) {
894
- children.push(Epub.createXmlElement("meta", {
895
- refines: `#${id}`,
896
- property: "identifier-type",
897
- ...(identifier.scheme && { scheme: identifier.scheme }),
898
- }, [Epub.createXmlTextNode(identifier.identifierType)]));
683
+ insert(metadata, _jsx("meta", { refines: `#${id}`, property: "identifier-type", ...(identifier.scheme && { scheme: identifier.scheme }), children: identifier.identifierType }));
899
684
  }
900
685
  }
901
686
  });
@@ -903,21 +688,19 @@ export class Epub {
903
688
  /**
904
689
  * Remove `meta` refinements pointing at any of the given ids,
905
690
  * restricted to the given `property` values as a cleanup step
906
- * Mutates `children` in place.
691
+ * Mutates `metadata` in place.
907
692
  */
908
- static removeRefiningMetas(children, ids, properties) {
909
- for (let i = children.length - 1; i >= 0; i--) {
910
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
911
- const node = children[i];
912
- if (Epub.isXmlTextNode(node) || Epub.getXmlElementName(node) !== "meta") {
693
+ static removeRefiningMetas(metadata, ids, properties) {
694
+ for (const child of Array.from(metadata.children)) {
695
+ if (child.nodeName !== "meta") {
913
696
  continue;
914
697
  }
915
- const property = node[":@"]?.["@_property"];
916
- if (!property || !properties.includes(property))
698
+ const property = child.getAttribute("property");
699
+ if (properties && (!property || !properties.includes(property)))
917
700
  continue;
918
- const refines = node[":@"]?.["@_refines"]?.slice(1);
701
+ const refines = child.getAttribute("refines")?.slice(1);
919
702
  if (refines && ids.has(refines)) {
920
- children.splice(i, 1);
703
+ metadata.removeChild(child);
921
704
  }
922
705
  }
923
706
  }
@@ -937,47 +720,37 @@ export class Epub {
937
720
  */
938
721
  async setSources(sources) {
939
722
  await this.withPackage((packageElement) => {
940
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
941
- if (!metadata)
723
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
724
+ if (!metadata) {
942
725
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
943
- const children = Epub.getXmlChildren(metadata);
726
+ }
944
727
  const removedIds = new Set();
945
- for (let i = children.length - 1; i >= 0; i--) {
946
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
947
- const node = children[i];
948
- if (Epub.isXmlTextNode(node) ||
949
- Epub.getXmlElementName(node) !== "dc:source") {
728
+ for (const child of Array.from(metadata.children)) {
729
+ if (child.nodeName !== "dc:source") {
950
730
  continue;
951
731
  }
952
- const id = node[":@"]?.["@_id"];
732
+ const id = child.getAttribute("id");
953
733
  if (id) {
954
734
  removedIds.add(id);
955
735
  }
956
- children.splice(i, 1);
736
+ metadata.removeChild(child);
957
737
  }
958
- Epub.removeRefiningMetas(children, removedIds, [
738
+ Epub.removeRefiningMetas(metadata, removedIds, [
959
739
  "identifier-type",
960
740
  "source-of",
961
741
  ]);
962
742
  for (const source of sources) {
963
743
  const needsId = source.identifierType !== undefined || source.isPageBreakSource;
964
- const id = source.id ?? (needsId ? nanoid() : undefined);
965
- children.push(Epub.createXmlElement("dc:source", {
966
- ...(id && { id }),
967
- ...(source.scheme &&
744
+ const id = source.id ?? (needsId ? xmlId() : undefined);
745
+ insert(metadata, _jsx("dc:source", { ...(id && { id }), ...(source.scheme &&
968
746
  source.identifierType === undefined && {
969
747
  "opf:scheme": source.scheme,
970
- }),
971
- }, [Epub.createXmlTextNode(source.value)]));
748
+ }), children: source.value }));
972
749
  if (source.identifierType !== undefined && id) {
973
- children.push(Epub.createXmlElement("meta", {
974
- refines: `#${id}`,
975
- property: "identifier-type",
976
- ...(source.scheme && { scheme: source.scheme }),
977
- }, [Epub.createXmlTextNode(source.identifierType)]));
750
+ insert(metadata, _jsx("meta", { refines: `#${id}`, property: "identifier-type", ...(source.scheme && { scheme: source.scheme }), children: source.identifierType }));
978
751
  }
979
752
  if (source.isPageBreakSource && id) {
980
- children.push(Epub.createXmlElement("meta", { refines: `#${id}`, property: "source-of" }, [Epub.createXmlTextNode("pagination")]));
753
+ insert(metadata, _jsx("meta", { refines: `#${id}`, property: "source-of", children: "pagination" }));
981
754
  }
982
755
  }
983
756
  });
@@ -991,11 +764,11 @@ export class Epub {
991
764
  */
992
765
  async getEpub2CoverImage() {
993
766
  const packageElement = await this.getPackageElement();
994
- const metadataElement = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
767
+ const metadataElement = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
995
768
  if (!metadataElement)
996
769
  throw new Error("Failed to parse EPUB: Found no metadata element in package document");
997
- const coverImageElement = Epub.getXmlChildren(metadataElement).find((node) => !Epub.isXmlTextNode(node) && node[":@"]?.["@_name"] === "cover");
998
- const manifestItemId = coverImageElement?.[":@"]?.["@_content"];
770
+ const coverImageElement = Array.from(metadataElement.children).find((element) => element.getAttribute("name") === "cover");
771
+ const manifestItemId = coverImageElement?.getAttribute("content");
999
772
  if (!manifestItemId)
1000
773
  return null;
1001
774
  const manifest = await this.getManifest();
@@ -1121,7 +894,7 @@ export class Epub {
1121
894
  */
1122
895
  async getBaseDirection() {
1123
896
  const packageEl = await this.getPackageElement();
1124
- const dir = packageEl[":@"]?.["@_dir"];
897
+ const dir = packageEl.getAttribute("dir");
1125
898
  if (dir !== "ltr" && dir !== "rtl" && dir !== "auto") {
1126
899
  return "auto";
1127
900
  }
@@ -1168,7 +941,7 @@ export class Epub {
1168
941
  value: subject,
1169
942
  }
1170
943
  : subject;
1171
- const subjectId = nanoid();
944
+ const subjectId = xmlId();
1172
945
  await this.addMetadata({
1173
946
  id: subjectId,
1174
947
  type: "dc:subject",
@@ -1198,24 +971,14 @@ export class Epub {
1198
971
  */
1199
972
  async removeSubject(index) {
1200
973
  await this.withPackage((packageElement) => {
1201
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
974
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
1202
975
  if (!metadata)
1203
976
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
1204
- let subjectCount = null;
1205
- let metadataIndex = null;
1206
- for (const meta of Epub.getXmlChildren(metadata)) {
1207
- if (subjectCount === index)
1208
- break;
1209
- metadataIndex = metadataIndex === null ? 0 : metadataIndex + 1;
1210
- if (Epub.isXmlTextNode(meta))
1211
- continue;
1212
- if (Epub.getXmlElementName(meta) !== "dc:subject")
1213
- continue;
1214
- subjectCount = subjectCount === null ? 0 : subjectCount + 1;
1215
- }
1216
- if (subjectCount === null || metadataIndex === null)
977
+ const subjects = metadata.children.filter((child) => child.nodeName === "dc:subject");
978
+ const subject = subjects[index];
979
+ if (!subject)
1217
980
  return;
1218
- Epub.getXmlChildren(metadata).splice(metadataIndex, 1);
981
+ metadata.removeChild(subject);
1219
982
  });
1220
983
  }
1221
984
  /**
@@ -1412,7 +1175,7 @@ export class Epub {
1412
1175
  */
1413
1176
  async getPackageVocabularyPrefixes() {
1414
1177
  const packageElement = await this.getPackageElement();
1415
- const prefixValue = packageElement[":@"]?.["@_prefix"];
1178
+ const prefixValue = packageElement.getAttribute("prefix");
1416
1179
  if (!prefixValue)
1417
1180
  return {};
1418
1181
  const matches = prefixValue.matchAll(/(?:([a-z]+): +(\S+)\s*)/gs);
@@ -1427,10 +1190,9 @@ export class Epub {
1427
1190
  await this.withPackage(async (packageElement) => {
1428
1191
  const prefixes = await this.getPackageVocabularyPrefixes();
1429
1192
  prefixes[prefix] = uri;
1430
- packageElement[":@"] ??= {};
1431
- packageElement[":@"]["@_prefix"] = Object.entries(prefixes)
1193
+ packageElement.setAttribute("prefix", Object.entries(prefixes)
1432
1194
  .map(([p, u]) => `${p}: ${u}`)
1433
- .join("\n ");
1195
+ .join("\n "));
1434
1196
  });
1435
1197
  }
1436
1198
  /**
@@ -1447,49 +1209,44 @@ export class Epub {
1447
1209
  // rather than a single string, to support expanded titles.
1448
1210
  async setTitle(title) {
1449
1211
  await this.withPackage((packageElement) => {
1450
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
1451
- if (!metadata)
1212
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
1213
+ if (!metadata) {
1452
1214
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
1453
- const titleElement = Epub.findXmlChildByName("dc:title", metadata.metadata);
1215
+ }
1216
+ const titleElement = findXmlChildByNameNS(NS.dc, "title", metadata);
1454
1217
  if (!titleElement) {
1455
- Epub.getXmlChildren(metadata).push(Epub.createXmlElement("dc:title", {}, [
1456
- Epub.createXmlTextNode(title),
1457
- ]));
1218
+ insert(metadata, _jsx("dc:title", { children: title }));
1458
1219
  }
1459
1220
  else {
1460
- titleElement["dc:title"] = [Epub.createXmlTextNode(title)];
1221
+ while (titleElement.firstChild) {
1222
+ titleElement.removeChild(titleElement.firstChild);
1223
+ }
1224
+ insert(titleElement, text(title));
1461
1225
  }
1462
1226
  });
1463
1227
  }
1464
1228
  async setTitles(entries) {
1465
1229
  await this.withPackage((packageElement) => {
1466
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
1230
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
1467
1231
  if (!metadata) {
1468
1232
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
1469
1233
  }
1470
- const metadataEntries = Epub.getXmlChildren(metadata);
1471
- for (let i = metadataEntries.length - 1; i >= 0; i--) {
1472
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1473
- const meta = metadataEntries[i];
1474
- if (Epub.isXmlTextNode(meta))
1475
- continue;
1476
- if (Epub.getXmlElementName(meta) === "dc:title" ||
1477
- meta[":@"]?.["@_property"] === "title-type" ||
1478
- meta[":@"]?.["@_property"] === "display-seq") {
1479
- metadataEntries.splice(i, 1);
1234
+ for (const meta of Array.from(metadata.children)) {
1235
+ if (meta.nodeName === "dc:title" ||
1236
+ meta.getAttribute("property") === "title-type" ||
1237
+ meta.getAttribute("property") === "display-seq") {
1238
+ metadata.removeChild(meta);
1480
1239
  }
1481
1240
  }
1482
1241
  for (let i = 0; i < entries.length; i++) {
1483
1242
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1484
1243
  const entry = entries[i];
1485
- const id = nanoid();
1486
- metadataEntries.push(Epub.createXmlElement("dc:title", { id }, [
1487
- Epub.createXmlTextNode(entry.title),
1488
- ]));
1244
+ const id = xmlId();
1245
+ insert(metadata, _jsx("dc:title", { id: id, children: entry.title }));
1489
1246
  if (entry.type) {
1490
- metadataEntries.push(Epub.createXmlElement("meta", { refines: `#${id}`, property: "title-type" }, [Epub.createXmlTextNode(entry.type)]));
1247
+ insert(metadata, _jsx("meta", { refines: `#${id}`, property: "title-type", children: entry.type }));
1491
1248
  }
1492
- metadataEntries.push(Epub.createXmlElement("meta", { refines: `#${id}`, property: "display-seq" }, [Epub.createXmlTextNode((i + 1).toString())]));
1249
+ insert(metadata, _jsx("meta", { refines: `#${id}`, property: "display-seq", children: (i + 1).toString() }));
1493
1250
  }
1494
1251
  });
1495
1252
  }
@@ -1523,31 +1280,31 @@ export class Epub {
1523
1280
  * will be added to the end of the list.
1524
1281
  */
1525
1282
  async addCollection(collection, index) {
1526
- const collectionId = nanoid();
1283
+ const collectionId = xmlId();
1527
1284
  // Order matters for creators and contributors,
1528
1285
  // so we can't just append these to the end of the
1529
1286
  // metadata element's children using `addMetadata`.
1530
1287
  // We have to manually find the correct insertion point
1531
1288
  // based on the provided index
1532
1289
  await this.withPackage((packageElement) => {
1533
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
1290
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
1534
1291
  if (!metadata)
1535
1292
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
1536
1293
  let collectionCount = 0;
1537
1294
  let metadataIndex = 0;
1538
- for (const meta of Epub.getXmlChildren(metadata)) {
1295
+ for (const meta of metadata.childNodes) {
1539
1296
  if (collectionCount === index)
1540
1297
  break;
1541
1298
  metadataIndex++;
1542
- if (Epub.isXmlTextNode(meta))
1299
+ if (!isElement(meta))
1543
1300
  continue;
1544
- if (Epub.getXmlElementName(meta) !== "meta")
1301
+ if (meta.nodeName !== "meta")
1545
1302
  continue;
1546
- if (meta[":@"]?.["@_property"] !== "belongs-to-collection")
1303
+ if (meta.getAttribute("property") !== "belongs-to-collection")
1547
1304
  continue;
1548
1305
  collectionCount++;
1549
1306
  }
1550
- Epub.getXmlChildren(metadata).splice(metadataIndex, 0, Epub.createXmlElement("meta", { id: collectionId, property: "belongs-to-collection" }, [Epub.createXmlTextNode(collection.name)]));
1307
+ insert(metadata, _jsx("meta", { id: collectionId, property: "belongs-to-collection", children: collection.name }), metadata.childNodes.item(metadataIndex));
1551
1308
  });
1552
1309
  // These can all just go at the end; order is only
1553
1310
  // important for the `dc:creator`/`dc:contributor`
@@ -1578,38 +1335,20 @@ export class Epub {
1578
1335
  */
1579
1336
  async removeCollection(index) {
1580
1337
  await this.withPackage((packageElement) => {
1581
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
1582
- if (!metadata)
1338
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
1339
+ if (!metadata) {
1583
1340
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
1584
- let collectionCount = null;
1585
- let metadataIndex = null;
1586
- for (const meta of Epub.getXmlChildren(metadata)) {
1587
- if (collectionCount === index)
1588
- break;
1589
- metadataIndex = metadataIndex === null ? 0 : metadataIndex + 1;
1590
- if (Epub.isXmlTextNode(meta))
1591
- continue;
1592
- if (Epub.getXmlElementName(meta) !== "meta")
1593
- continue;
1594
- if (meta[":@"]?.["@_property"] !== "belongs-to-collection")
1595
- continue;
1596
- collectionCount = collectionCount === null ? 0 : collectionCount + 1;
1597
1341
  }
1598
- if (collectionCount === null || metadataIndex === null)
1342
+ // The index refers to what `getCollections()` returns, so filter the
1343
+ // same way before indexing
1344
+ const removed = Array.from(metadata.children).filter((child) => child.getAttribute("property") === "belongs-to-collection" &&
1345
+ !!child.textContent?.trim())[index];
1346
+ if (!removed)
1599
1347
  return;
1600
- const [removed] = Epub.getXmlChildren(metadata).splice(metadataIndex, 1);
1601
- if (removed && !Epub.isXmlTextNode(removed) && removed[":@"]?.["@_id"]) {
1602
- const id = removed[":@"]["@_id"];
1603
- const newChildren = Epub.getXmlChildren(metadata).filter((node) => {
1604
- if (Epub.isXmlTextNode(node))
1605
- return true;
1606
- if (Epub.getXmlElementName(node) !== "meta")
1607
- return true;
1608
- if (node[":@"]?.["@_refines"] !== `#${id}`)
1609
- return true;
1610
- return false;
1611
- });
1612
- Epub.replaceXmlChildren(metadata, newChildren);
1348
+ const removedId = removed.getAttribute("id");
1349
+ metadata.removeChild(removed);
1350
+ if (removedId) {
1351
+ Epub.removeRefiningMetas(metadata, new Set([removedId]));
1613
1352
  }
1614
1353
  });
1615
1354
  }
@@ -1685,31 +1424,31 @@ export class Epub {
1685
1424
  * @link https://www.w3.org/TR/epub-33/#sec-opf-dccreator
1686
1425
  */
1687
1426
  async addCreator(creator, index, type = "creator") {
1688
- const creatorId = nanoid();
1427
+ const creatorId = xmlId();
1689
1428
  // Order matters for creators and contributors,
1690
1429
  // so we can't just append these to the end of the
1691
1430
  // metadata element's children using `addMetadata`.
1692
1431
  // We have to manually find the correct insertion point
1693
1432
  // based on the provided index
1694
1433
  await this.withPackage((packageElement) => {
1695
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
1434
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
1696
1435
  if (!metadata)
1697
1436
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
1698
1437
  let creatorCount = 0;
1699
1438
  let metadataIndex = 0;
1700
- for (const meta of Epub.getXmlChildren(metadata)) {
1439
+ for (const meta of metadata.childNodes) {
1701
1440
  if (creatorCount === index)
1702
1441
  break;
1703
1442
  metadataIndex++;
1704
- if (Epub.isXmlTextNode(meta))
1443
+ if (!isElement(meta))
1444
+ continue;
1445
+ if (meta.nodeName !== `dc:${type}`)
1705
1446
  continue;
1706
- if (Epub.getXmlElementName(meta) !== `dc:${type}`)
1447
+ if (meta.getAttribute("property") !== "belongs-to-collection")
1707
1448
  continue;
1708
1449
  creatorCount++;
1709
1450
  }
1710
- Epub.getXmlChildren(metadata).splice(metadataIndex, 0, Epub.createXmlElement(`dc:${type}`, { id: creatorId }, [
1711
- Epub.createXmlTextNode(creator.name),
1712
- ]));
1451
+ insert(metadata, x(`dc:${type}`, { id: creatorId }, creator.name), metadata.childNodes.item(metadataIndex));
1713
1452
  });
1714
1453
  // These can all just go at the end; order is only
1715
1454
  // important for the `dc:creator`/`dc:contributor`
@@ -1756,36 +1495,16 @@ export class Epub {
1756
1495
  */
1757
1496
  async removeCreator(index, type = "creator") {
1758
1497
  await this.withPackage((packageElement) => {
1759
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
1498
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
1760
1499
  if (!metadata)
1761
1500
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
1762
- let creatorCount = null;
1763
- let metadataIndex = null;
1764
- for (const meta of Epub.getXmlChildren(metadata)) {
1765
- if (creatorCount === index)
1766
- break;
1767
- metadataIndex = metadataIndex === null ? 0 : metadataIndex + 1;
1768
- if (Epub.isXmlTextNode(meta))
1769
- continue;
1770
- if (Epub.getXmlElementName(meta) !== `dc:${type}`)
1771
- continue;
1772
- creatorCount = creatorCount === null ? 0 : creatorCount + 1;
1773
- }
1774
- if (creatorCount === null || metadataIndex === null)
1501
+ const toRemove = Array.from(metadata.children).filter((child) => child.nodeName === `dc:${type}`)[index];
1502
+ if (!toRemove)
1775
1503
  return;
1776
- const [removed] = Epub.getXmlChildren(metadata).splice(metadataIndex, 1);
1777
- if (removed && !Epub.isXmlTextNode(removed) && removed[":@"]?.["@_id"]) {
1778
- const id = removed[":@"]["@_id"];
1779
- const newChildren = Epub.getXmlChildren(metadata).filter((node) => {
1780
- if (Epub.isXmlTextNode(node))
1781
- return true;
1782
- if (Epub.getXmlElementName(node) !== "meta")
1783
- return true;
1784
- if (node[":@"]?.["@_refines"] !== `#${id}`)
1785
- return true;
1786
- return false;
1787
- });
1788
- Epub.replaceXmlChildren(metadata, newChildren);
1504
+ const id = toRemove.getAttribute("id");
1505
+ metadata.removeChild(toRemove);
1506
+ if (id) {
1507
+ Epub.removeRefiningMetas(metadata, new Set([id]));
1789
1508
  }
1790
1509
  });
1791
1510
  }
@@ -1822,17 +1541,14 @@ export class Epub {
1822
1541
  if (this.spine !== null)
1823
1542
  return this.spine;
1824
1543
  const packageElement = await this.getPackageElement();
1825
- const spine = Epub.findXmlChildByName("spine", Epub.getXmlChildren(packageElement));
1544
+ const spine = findXmlChildByNameNS(NS.opf, "spine", packageElement);
1826
1545
  if (!spine)
1827
1546
  throw new Error("Failed to parse EPUB: Found no spine element in package document");
1828
- this.spine = spine["spine"]
1829
- .filter((node) => !Epub.isXmlTextNode(node))
1547
+ this.spine = Array.from(spine.children)
1830
1548
  .map((itemref) => {
1831
- const idRef = itemref[":@"]?.["@_idref"];
1832
- const linear = itemref[":@"]?.["@_linear"];
1833
1549
  return {
1834
- itemRef: idRef,
1835
- linear: linear === "no" ? false : true,
1550
+ itemRef: itemref.getAttribute("idref"),
1551
+ linear: itemref.getAttribute("linear") !== "no",
1836
1552
  };
1837
1553
  })
1838
1554
  .filter((itemref) => !!itemref.itemRef);
@@ -1871,21 +1587,24 @@ export class Epub {
1871
1587
  * @link https://www.w3.org/TR/epub-33/#sec-spine-elem
1872
1588
  */
1873
1589
  async addSpineItem(manifestId, index) {
1874
- const item = Epub.createXmlElement("itemref", { idref: manifestId });
1590
+ const item = _jsx("itemref", { idref: manifestId });
1875
1591
  const manifest = await this.getManifest();
1876
1592
  const manifestItem = manifest[manifestId];
1877
1593
  if (!manifestItem)
1878
1594
  throw new Error(`Manifest item not found with id "${manifestId}"`);
1595
+ if ((await this.getSpine()).some(({ itemRef }) => itemRef === manifestId)) {
1596
+ throw new Error(`Manifest item "${manifestId}" is already referenced in the spine`);
1597
+ }
1598
+ if (!spineReferenceable(manifest, manifestItem)) {
1599
+ throw new Error(`Manifest item "${manifestId}" is not an EPUB content document, so it cannot be added to the spine`);
1600
+ }
1879
1601
  await this.withPackage((packageElement) => {
1880
- const spine = Epub.findXmlChildByName("spine", Epub.getXmlChildren(packageElement));
1602
+ const spine = findXmlChildByNameNS(NS.opf, "spine", packageElement);
1881
1603
  if (!spine)
1882
1604
  throw new Error("Failed to parse EPUB: Found no spine element in package document");
1883
- if (index === undefined) {
1884
- Epub.getXmlChildren(spine).push(item);
1885
- }
1886
- else {
1887
- Epub.getXmlChildren(spine).splice(index, 0, item);
1888
- }
1605
+ insert(spine, item, index === undefined
1606
+ ? null
1607
+ : (Array.from(spine.children)[index] ?? null));
1889
1608
  });
1890
1609
  // Reset the spine cache
1891
1610
  this.spine = null;
@@ -1897,41 +1616,42 @@ export class Epub {
1897
1616
  */
1898
1617
  async removeSpineItem(index) {
1899
1618
  await this.withPackage((packageElement) => {
1900
- const spine = Epub.findXmlChildByName("spine", Epub.getXmlChildren(packageElement));
1619
+ const spine = findXmlChildByNameNS(NS.opf, "spine", packageElement);
1901
1620
  if (!spine)
1902
1621
  throw new Error("Failed to parse EPUB: Found no spine element in package document");
1903
- Epub.getXmlChildren(spine).splice(index, 1);
1622
+ const itemref = Array.from(spine.children)[index];
1623
+ if (itemref)
1624
+ spine.removeChild(itemref);
1904
1625
  });
1905
1626
  // Reset the spine cache
1906
1627
  this.spine = null;
1907
1628
  }
1908
1629
  async getNavigationChildren(ol, navHref, { resolveToRoot } = {}) {
1909
1630
  const children = [];
1910
- const childrenElements = Epub.getXmlChildren(ol).filter((node) => !Epub.isXmlTextNode(node) && Epub.getXmlElementName(node) === "li");
1911
- for (const childEl of childrenElements) {
1912
- const [firstChild, secondChild] = Epub.getXmlChildren(childEl).filter((node) => !Epub.isXmlTextNode(node) &&
1913
- ["a", "span", "ol"].includes(Epub.getXmlElementName(node)));
1631
+ const listItems = Array.from(ol.children).filter((element) => element.nodeName === "li");
1632
+ for (const childEl of listItems) {
1633
+ const [firstChild, secondChild] = Array.from(childEl.children).filter((element) => ["a", "span", "ol"].includes(element.nodeName));
1914
1634
  if (!firstChild)
1915
1635
  continue;
1916
- if (!["a", "span"].includes(Epub.getXmlElementName(firstChild))) {
1636
+ const name = firstChild.nodeName;
1637
+ if (name !== "a" && name !== "span")
1917
1638
  continue;
1918
- }
1919
- if (Epub.getXmlElementName(firstChild) === "span" &&
1920
- (!secondChild || Epub.getXmlElementName(secondChild) !== "ol")) {
1639
+ if (name === "span" && (!secondChild || secondChild.nodeName !== "ol")) {
1921
1640
  continue;
1922
1641
  }
1923
- const role = firstChild[":@"]?.["@_epub:type"]?.trim();
1642
+ const role = firstChild.getAttribute("epub:type")?.trim();
1643
+ const href = firstChild.getAttribute("href");
1924
1644
  children.push({
1925
- title: Epub.getXhtmlTextContent(Epub.getXmlChildren(firstChild)),
1926
- ...(Epub.getXmlElementName(firstChild) === "a" &&
1927
- firstChild[":@"]?.["@_href"] && {
1928
- href: await this.resolveHref(firstChild[":@"]["@_href"], navHref, {
1645
+ title: firstChild.textContent ?? "",
1646
+ ...(name === "a" &&
1647
+ href && {
1648
+ href: await this.resolveHref(href, navHref, {
1929
1649
  toRoot: resolveToRoot,
1930
1650
  }),
1931
1651
  }),
1932
1652
  ...(role && { role }),
1933
1653
  ...(secondChild &&
1934
- Epub.getXmlElementName(secondChild) === "ol" && {
1654
+ secondChild.nodeName === "ol" && {
1935
1655
  children: await this.getNavigationChildren(secondChild, navHref, {
1936
1656
  resolveToRoot,
1937
1657
  }),
@@ -1946,19 +1666,21 @@ export class Epub {
1946
1666
  if (!navItem)
1947
1667
  return null;
1948
1668
  const navContents = await this.readXhtmlItemContents(navItem.id);
1949
- const navEl = Epub.findXmlDescendantByName("nav", navContents, (node) => Epub.getXmlAttributes(node)["epub:type"] === role);
1669
+ const html = navContents.documentElement;
1670
+ if (!html)
1671
+ return null;
1672
+ const navEl = findXmlDescendantByName("nav", html, (node) => node.getAttribute("epub:type") === role);
1950
1673
  if (!navEl)
1951
1674
  return null;
1952
- const [firstChild, secondChild] = Epub.getXmlChildren(navEl).filter((node) => !!(!Epub.isXmlTextNode(node) &&
1953
- Epub.getXmlElementName(node).match(/(?:h[1-6]|ol)/)));
1675
+ const [firstChild, secondChild] = Array.from(navEl.children).filter((element) => /h[1-6]|ol/.test(element.nodeName));
1954
1676
  if (!firstChild)
1955
1677
  return null;
1956
- const title = Epub.getXmlElementName(firstChild).match(/h[1-6]/)
1957
- ? Epub.getXhtmlTextContent(Epub.getXmlChildren(firstChild))
1678
+ const title = /h[1-6]/.test(firstChild.nodeName)
1679
+ ? (firstChild.textContent ?? "")
1958
1680
  : null;
1959
- const list = Epub.getXmlElementName(firstChild) === "ol"
1681
+ const list = firstChild.nodeName === "ol"
1960
1682
  ? firstChild
1961
- : secondChild && Epub.getXmlElementName(secondChild) === "ol"
1683
+ : secondChild && secondChild.nodeName === "ol"
1962
1684
  ? secondChild
1963
1685
  : null;
1964
1686
  if (!list)
@@ -2103,6 +1825,26 @@ export class Epub {
2103
1825
  : await this.adapter.read(entry);
2104
1826
  return itemEntry;
2105
1827
  }
1828
+ /**
1829
+ * Create a new XML document with the given root node.
1830
+ *
1831
+ * @param root The XML node to use as the document element
1832
+ * @param language Optional - defaults to the EPUB's language
1833
+ */
1834
+ createXmlDocument(root) {
1835
+ const document = createDocument(root.namespaceURI ?? root.getAttribute("xmlns"), root.nodeName, null);
1836
+ const docRoot = document.documentElement;
1837
+ if (!docRoot)
1838
+ throw new Error("Failed to create XML document");
1839
+ for (const attr of root.attributes) {
1840
+ docRoot.setAttributeNode(attr.cloneNode());
1841
+ }
1842
+ for (const child of Array.from(root.childNodes)) {
1843
+ insert(docRoot, child);
1844
+ }
1845
+ document.insertBefore(document.createProcessingInstruction("xml", 'version="1.0" encoding="UTF-8"'), docRoot);
1846
+ return document;
1847
+ }
2106
1848
  /**
2107
1849
  * Create a new XHTML document with the given body
2108
1850
  * and head.
@@ -2113,27 +1855,32 @@ export class Epub {
2113
1855
  */
2114
1856
  async createXhtmlDocument(body, head, language) {
2115
1857
  const lang = language ?? (await this.getLanguage());
2116
- return [
2117
- Epub.createXmlElement("?xml", { version: "1.0", encoding: "UTF-8" }, [
2118
- { "#text": "" },
2119
- ]),
2120
- Epub.createXmlElement("html", {
2121
- xmlns: "http://www.w3.org/1999/xhtml",
2122
- "xmlns:epub": "http://www.idpf.org/2007/ops",
2123
- ...(lang && { "xml:lang": lang.toString(), lang: lang.toString() }),
2124
- }, [
2125
- Epub.createXmlElement("head", {}, head),
2126
- Epub.createXmlElement("body", {}, body),
2127
- ]),
2128
- ];
1858
+ const document = createDocument(NS.xhtml, "html", createDocumentType("html", "", ""));
1859
+ const html = document.documentElement;
1860
+ if (!html)
1861
+ throw new Error("Failed to create XHTML document");
1862
+ html.setAttributeNS(NS.xmlns, "xmlns:epub", NS.epub);
1863
+ if (lang) {
1864
+ html.setAttributeNS(NS.xml, "xml:lang", lang.toString());
1865
+ html.setAttribute("lang", lang.toString());
1866
+ }
1867
+ const headElement = document.createElement("head");
1868
+ for (const node of head ?? [])
1869
+ insert(headElement, node);
1870
+ insert(html, headElement);
1871
+ const bodyElement = document.createElement("body");
1872
+ for (const node of body)
1873
+ insert(bodyElement, node);
1874
+ insert(html, bodyElement);
1875
+ document.insertBefore(document.createProcessingInstruction("xml", 'version="1.0" encoding="UTF-8"'), document.doctype);
1876
+ return document;
2129
1877
  }
2130
1878
  async readXhtmlItemContents(id, as = "xhtml") {
2131
1879
  const contents = await this.readItemContents(id, "utf-8");
2132
- const xml = Epub.xhtmlParser.parse(contents);
1880
+ const document = parse(contents, "application/xhtml+xml");
2133
1881
  if (as === "xhtml")
2134
- return xml;
2135
- const body = Epub.getXhtmlBody(xml);
2136
- return Epub.getXhtmlTextContent(body);
1882
+ return document;
1883
+ return getXhtmlBody(document).textContent ?? "";
2137
1884
  }
2138
1885
  async writeEntryContents(path, contents, encoding) {
2139
1886
  this.assertWritable();
@@ -2177,42 +1924,52 @@ export class Epub {
2177
1924
  * @link https://www.w3.org/TR/epub-33/#sec-xhtml
2178
1925
  */
2179
1926
  async writeXhtmlItemContents(id, contents) {
2180
- await this.writeItemContents(id, Epub.xhtmlBuilder.build(contents), "utf-8");
1927
+ await this.writeItemContents(id, serialize(contents), "utf-8");
2181
1928
  }
2182
1929
  async removeManifestItem(id) {
2183
1930
  await this.withPackage(async (packageElement) => {
2184
- const manifest = Epub.findXmlChildByName("manifest", Epub.getXmlChildren(packageElement));
1931
+ const manifest = findXmlChildByNameNS(NS.opf, "manifest", packageElement);
2185
1932
  if (!manifest)
2186
1933
  throw new Error("Failed to parse EPUB: Found no manifest element in package document");
2187
- const itemIndex = Epub.getXmlChildren(manifest).findIndex((node) => !Epub.isXmlTextNode(node) && node[":@"]?.["@_id"] === id);
2188
- if (itemIndex === -1)
1934
+ const item = Array.from(manifest.children).find((element) => element.getAttribute("id") === id);
1935
+ if (!item)
2189
1936
  return;
2190
- const [item] = Epub.getXmlChildren(manifest).splice(itemIndex, 1);
2191
- if (!item || Epub.isXmlTextNode(item) || !item[":@"]?.["@_href"])
1937
+ const href = item.getAttribute("href");
1938
+ manifest.removeChild(item);
1939
+ const spine = findXmlChildByNameNS(NS.opf, "spine", packageElement);
1940
+ if (spine) {
1941
+ for (const itemref of Array.from(spine.children)) {
1942
+ if (itemref.getAttribute("idref") === id)
1943
+ spine.removeChild(itemref);
1944
+ }
1945
+ }
1946
+ if (!href)
2192
1947
  return;
2193
- await this.removeEntry(item[":@"]["@_href"]);
1948
+ await this.removeEntry(href);
2194
1949
  });
2195
- // Reset the cached manifest, so that it will be read from
1950
+ // Reset the cached manifest and spine, so that they will be read from
2196
1951
  // the updated XML next time
2197
1952
  this.manifest = null;
1953
+ this.spine = null;
2198
1954
  }
2199
1955
  async addManifestItem(item, contents, encoding) {
1956
+ const existingItems = Object.values(await this.getManifest());
1957
+ if (existingItems.some((existing) => existing.id === item.id)) {
1958
+ throw new Error(`Cannot add manifest item: an item with the id "${item.id}" already exists`);
1959
+ }
1960
+ // 4.2.1: hrefs that normalize and case-fold to the same path would
1961
+ // overwrite each other on a normalizing filesystem.
1962
+ const foldedHref = foldPathName(item.href);
1963
+ for (const existing of existingItems) {
1964
+ if (foldPathName(existing.href) === foldedHref) {
1965
+ throw new Error(`Cannot add manifest item: "${item.href}" collides with the existing item "${existing.href}"`);
1966
+ }
1967
+ }
2200
1968
  await this.withPackage((packageElement) => {
2201
- const manifest = Epub.findXmlChildByName("manifest", Epub.getXmlChildren(packageElement));
1969
+ const manifest = findXmlChildByNameNS(NS.opf, "manifest", packageElement);
2202
1970
  if (!manifest)
2203
1971
  throw new Error("Failed to parse EPUB: Found no manifest element in package document");
2204
- // TODO: Should we ensure that there isn't already a manifest
2205
- // item with this id first?
2206
- Epub.getXmlChildren(manifest).push(Epub.createXmlElement("item", {
2207
- id: item.id,
2208
- href: item.href,
2209
- ...(item.mediaType && { "media-type": item.mediaType }),
2210
- ...(item.fallback && { fallback: item.fallback }),
2211
- ...(item.mediaOverlay && { "media-overlay": item.mediaOverlay }),
2212
- ...(item.properties && {
2213
- properties: item.properties.join(" "),
2214
- }),
2215
- }));
1972
+ insert(manifest, _jsx("item", { id: item.id, href: item.href, ...(item.mediaType && { "media-type": item.mediaType }), ...(item.fallback && { fallback: item.fallback }), ...(item.mediaOverlay && { "media-overlay": item.mediaOverlay }), ...(item.properties && { properties: item.properties.join(" ") }) }));
2216
1973
  });
2217
1974
  // Reset the cached manifest, so that it will be read from
2218
1975
  // the updated XML next time
@@ -2223,7 +1980,7 @@ export class Epub {
2223
1980
  const data = encoding === "utf-8" || encoding === "xml"
2224
1981
  ? new TextEncoder().encode(encoding === "utf-8"
2225
1982
  ? contents
2226
- : Epub.xmlBuilder.build(contents))
1983
+ : serialize(contents))
2227
1984
  : contents;
2228
1985
  /* oxlint-enable typescript/no-unnecessary-type-assertion */
2229
1986
  await this.writeEntryContents(filename, data);
@@ -2238,22 +1995,17 @@ export class Epub {
2238
1995
  */
2239
1996
  async updateManifestItem(id, newItem) {
2240
1997
  await this.withPackage((packageElement) => {
2241
- const manifest = Epub.findXmlChildByName("manifest", Epub.getXmlChildren(packageElement));
1998
+ const manifest = findXmlChildByNameNS(NS.opf, "manifest", packageElement);
2242
1999
  if (!manifest)
2243
2000
  throw new Error("Failed to parse EPUB: Found no manifest element in package document");
2244
- const itemIndex = manifest["manifest"].findIndex((item) => !Epub.isXmlTextNode(item) && item[":@"]?.["@_id"] === id);
2245
- Epub.getXmlChildren(manifest).splice(itemIndex, 1, Epub.createXmlElement("item", {
2246
- id: id,
2247
- href: newItem.href,
2248
- ...(newItem.mediaType && { "media-type": newItem.mediaType }),
2249
- ...(newItem.fallback && { fallback: newItem.fallback }),
2250
- ...(newItem.mediaOverlay && {
2001
+ const existing = Array.from(manifest.children).find((element) => element.getAttribute("id") === id);
2002
+ if (!existing)
2003
+ return;
2004
+ replace(manifest, _jsx("item", { id: id, href: newItem.href, ...(newItem.mediaType && { "media-type": newItem.mediaType }), ...(newItem.fallback && { fallback: newItem.fallback }), ...(newItem.mediaOverlay && {
2251
2005
  "media-overlay": newItem.mediaOverlay,
2252
- }),
2253
- ...(newItem.properties && {
2006
+ }), ...(newItem.properties && {
2254
2007
  properties: newItem.properties.join(" "),
2255
- }),
2256
- }));
2008
+ }) }), existing);
2257
2009
  });
2258
2010
  // Reset the cached manifest, so that it will be read from
2259
2011
  // the updated XML next time
@@ -2271,15 +2023,13 @@ export class Epub {
2271
2023
  */
2272
2024
  async addMetadata(entry) {
2273
2025
  await this.withPackage((packageElement) => {
2274
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
2026
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
2275
2027
  if (!metadata)
2276
2028
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
2277
- Epub.getXmlChildren(metadata).push(Epub.createXmlElement(entry.type, {
2029
+ insert(metadata, x(entry.type, {
2278
2030
  ...(entry.id && { id: entry.id }),
2279
2031
  ...entry.properties,
2280
- }, entry.value !== undefined
2281
- ? [Epub.createXmlTextNode(entry.value)]
2282
- : []));
2032
+ }, entry.value));
2283
2033
  });
2284
2034
  }
2285
2035
  /**
@@ -2297,19 +2047,22 @@ export class Epub {
2297
2047
  */
2298
2048
  async replaceMetadata(predicate, entry) {
2299
2049
  await this.withPackage((packageElement) => {
2300
- const metadataElement = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
2050
+ const metadataElement = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
2301
2051
  if (!metadataElement)
2302
2052
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
2303
2053
  const oldEntryIndex = this.findMetadataIndex(packageElement, predicate);
2304
- const newElement = Epub.createXmlElement(entry.type, {
2054
+ const newElement = x(entry.type, {
2305
2055
  ...(entry.id && { id: entry.id }),
2306
2056
  ...entry.properties,
2307
- }, entry.value !== undefined ? [Epub.createXmlTextNode(entry.value)] : []);
2308
- if (oldEntryIndex === -1) {
2309
- metadataElement.metadata.push(newElement);
2057
+ }, entry.value);
2058
+ const existing = oldEntryIndex === -1
2059
+ ? null
2060
+ : (Array.from(metadataElement.children)[oldEntryIndex] ?? null);
2061
+ if (existing) {
2062
+ replace(metadataElement, newElement, existing);
2310
2063
  }
2311
2064
  else {
2312
- metadataElement.metadata.splice(oldEntryIndex, 1, newElement);
2065
+ insert(metadataElement, newElement);
2313
2066
  }
2314
2067
  });
2315
2068
  }
@@ -2327,19 +2080,14 @@ export class Epub {
2327
2080
  */
2328
2081
  async removeMetadata(predicate) {
2329
2082
  await this.withPackage((packageElement) => {
2330
- const metadataElement = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
2083
+ const metadataElement = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
2331
2084
  if (!metadataElement) {
2332
2085
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
2333
2086
  }
2334
- const metadataEntries = Epub.getXmlChildren(metadataElement);
2335
- for (let i = metadataEntries.length - 1; i >= 0; i--) {
2336
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2337
- const meta = metadataEntries[i];
2087
+ for (const meta of Array.from(metadataElement.children)) {
2338
2088
  const item = Epub.parseMetadataItem(meta);
2339
- if (!item)
2340
- continue;
2341
2089
  if (predicate(item)) {
2342
- metadataEntries.splice(i, 1);
2090
+ metadataElement.removeChild(meta);
2343
2091
  }
2344
2092
  }
2345
2093
  });
@@ -2349,7 +2097,7 @@ export class Epub {
2349
2097
  */
2350
2098
  async getVersion() {
2351
2099
  const packageElement = await this.getPackageElement();
2352
- return packageElement[":@"]?.["@_version"] ?? "2.0";
2100
+ return packageElement.getAttribute("version") ?? "2.0";
2353
2101
  }
2354
2102
  /**
2355
2103
  * Parse the NCX table of contents, if one exists, and return
@@ -2364,49 +2112,42 @@ export class Epub {
2364
2112
  this.getManifest(),
2365
2113
  this.getPackageElement(),
2366
2114
  ]);
2367
- const spine = Epub.findXmlChildByName("spine", Epub.getXmlChildren(packageElement));
2368
- const spineTocId = spine?.[":@"]?.["@_toc"];
2115
+ const spine = findXmlChildByNameNS(NS.opf, "spine", packageElement);
2116
+ const spineTocId = spine?.getAttribute("toc");
2369
2117
  const ncxItem = spineTocId
2370
2118
  ? manifest[spineTocId]
2371
2119
  : Object.values(manifest).find((item) => MediaType.fromMime(item.mediaType ?? "") === MediaType.NCX);
2372
2120
  if (!ncxItem)
2373
2121
  return [];
2374
2122
  const ncxContent = await this.readItemContents(ncxItem.id, "utf-8");
2375
- const ncxXml = Epub.xmlParser.parse(ncxContent);
2376
- const ncxElement = Epub.findXmlChildByName("ncx", ncxXml);
2377
- if (!ncxElement)
2123
+ const ncxElement = parse(ncxContent, "text/xml").documentElement;
2124
+ if (!ncxElement || ncxElement.localName !== "ncx")
2378
2125
  return [];
2379
- const ncxChildren = Epub.getXmlChildren(ncxElement);
2380
- const navMap = Epub.findXmlChildByName("navMap", ncxChildren) ??
2381
- Epub.findXmlChildByName("navmap", ncxChildren);
2126
+ const navMap = findXmlChildByName("navMap", ncxElement) ??
2127
+ findXmlChildByName("navmap", ncxElement);
2382
2128
  if (!navMap)
2383
2129
  return [];
2384
- return this.parseNavPoints(Epub.getXmlChildren(navMap), ncxItem.href);
2130
+ return this.parseNavPoints(navMap, ncxItem.href);
2385
2131
  }
2386
- async parseNavPoints(nodes, ncxHref) {
2132
+ async parseNavPoints(parent, ncxHref) {
2387
2133
  const entries = [];
2388
- for (const node of nodes) {
2389
- if (Epub.isXmlTextNode(node))
2390
- continue;
2391
- const name = Epub.getXmlElementName(node);
2392
- const isNavPoint = name === "navPoint" || name === "navpoint";
2393
- if (!isNavPoint)
2134
+ for (const node of Array.from(parent.children)) {
2135
+ if (node.nodeName !== "navPoint" && node.nodeName !== "navpoint") {
2394
2136
  continue;
2395
- const children = Epub.getXmlChildren(node);
2396
- const navLabel = Epub.findXmlChildByName("navLabel", children) ??
2397
- Epub.findXmlChildByName("navlabel", children);
2137
+ }
2138
+ const navLabel = findXmlChildByName("navLabel", node) ??
2139
+ findXmlChildByName("navlabel", node);
2398
2140
  let title = null;
2399
2141
  if (navLabel) {
2400
- const textEl = Epub.findXmlChildByName("text", Epub.getXmlChildren(navLabel));
2142
+ const textEl = findXmlChildByName("text", navLabel);
2401
2143
  if (textEl) {
2402
- title =
2403
- Epub.getXhtmlTextContent(Epub.getXmlChildren(textEl)).trim() || null;
2144
+ title = textEl.textContent?.trim() || null;
2404
2145
  }
2405
2146
  }
2406
- const contentEl = Epub.findXmlChildByName("content", children);
2407
- const src = contentEl?.[":@"]?.["@_src"];
2147
+ const contentEl = findXmlChildByName("content", node);
2148
+ const src = contentEl?.getAttribute("src");
2408
2149
  const href = src ? await this.resolveHref(src, ncxHref) : null;
2409
- const childEntries = await this.parseNavPoints(children, ncxHref);
2150
+ const childEntries = await this.parseNavPoints(node, ncxHref);
2410
2151
  entries.push({
2411
2152
  title: title ?? `${entries.length}`,
2412
2153
  ...(href && { href }),
@@ -2423,15 +2164,15 @@ export class Epub {
2423
2164
  */
2424
2165
  async getGuideEntries() {
2425
2166
  const packageElement = await this.getPackageElement();
2426
- const guide = Epub.findXmlChildByName("guide", Epub.getXmlChildren(packageElement));
2167
+ const guide = findXmlChildByNameNS(NS.opf, "guide", packageElement);
2427
2168
  if (!guide)
2428
2169
  return [];
2429
- return Epub.getXmlChildren(guide)
2430
- .filter((node) => !Epub.isXmlTextNode(node) && "reference" in node)
2431
- .map((ref) => ({
2432
- href: ref[":@"]?.["@_href"] ?? "",
2433
- title: ref[":@"]?.["@_title"] ?? "",
2434
- type: (ref[":@"]?.["@_type"] ?? "").toLowerCase(),
2170
+ return Array.from(guide.children)
2171
+ .filter((reference) => reference.nodeName === "reference")
2172
+ .map((reference) => ({
2173
+ href: reference.getAttribute("href") ?? "",
2174
+ title: reference.getAttribute("title") ?? "",
2175
+ type: (reference.getAttribute("type") ?? "").toLowerCase(),
2435
2176
  }))
2436
2177
  .filter((entry) => entry.href);
2437
2178
  }
@@ -2542,27 +2283,14 @@ export class EpubFactory {
2542
2283
  // unreachable: a writable adapter must implement write
2543
2284
  throw new Error(`adapter ${this.adapterClass.kind} declared writable but did not implement write()`);
2544
2285
  }
2545
- const encoder = new TextEncoder();
2546
- const container = encoder.encode(`<?xml version="1.0"?>
2547
- <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
2548
- <rootfiles>
2549
- <rootfile media-type="${MediaType.OPF.mime}" full-path="OEBPS/content.opf"/>
2550
- </rootfiles>
2551
- </container>
2552
- `);
2553
- await adapter.write("META-INF/container.xml", container);
2554
- const packageDocument = encoder.encode(`<?xml version="1.0"?>
2555
- <package unique-identifier="pub-id" dir="${language.textInfo.direction}" xml:lang="${language.toString()}" version="3.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
2556
- <metadata>
2557
- </metadata>
2558
- <manifest>
2559
- </manifest>
2560
- <spine>
2561
- </spine>
2562
- </package>
2563
- `);
2564
- await adapter.write("OEBPS/content.opf", packageDocument);
2286
+ const textInfo = typeof language.getTextInfo === "function"
2287
+ ? language.getTextInfo()
2288
+ : language.textInfo;
2565
2289
  const epub = new Epub(this.adapterClass, adapter, path);
2290
+ const container = epub.createXmlDocument(_jsx("container", { version: "1.0", xmlns: "urn:oasis:names:tc:opendocument:xmlns:container", children: _jsx("rootfiles", { children: _jsx("rootfile", { "media-type": MediaType.OPF.mime, "full-path": "OEBPS/content.opf" }) }) }));
2291
+ await adapter.write("META-INF/container.xml", serialize(container), "utf-8");
2292
+ const packageDocument = epub.createXmlDocument(_jsxs("package", { xmlns: NS.opf, "unique-identifier": "pub-id", dir: textInfo?.direction ?? "ltr", "xml:lang": language.toString(), version: "3.0", "xmlns:dc": "http://purl.org/dc/elements/1.1/", children: [_jsx("metadata", {}), _jsx("manifest", {}), _jsx("spine", {})] }));
2293
+ await adapter.write("OEBPS/content.opf", serialize(packageDocument), "utf-8");
2566
2294
  const metadata = [
2567
2295
  {
2568
2296
  id: "pub-id",
@@ -2586,7 +2314,7 @@ export class EpubFactory {
2586
2314
  await Promise.all(creators.map((creator) => epub.addCreator(creator)));
2587
2315
  }
2588
2316
  if (contributors) {
2589
- await Promise.all(contributors.map((contributor) => epub.addCreator(contributor)));
2317
+ await Promise.all(contributors.map((contributor) => epub.addContributor(contributor)));
2590
2318
  }
2591
2319
  return epub; // oxlint-disable-line typescript/no-unnecessary-type-assertion
2592
2320
  }
@@ -2614,10 +2342,8 @@ export class EpubFactory {
2614
2342
  throw new EpubReadOnlyError(`adapter ${this.adapterClass.kind} is read-only; cannot upgrade`);
2615
2343
  }
2616
2344
  const { removeNcx = false, outputPath } = options;
2617
- if (outputPath) {
2618
- await mkdir(dirname(outputPath), { recursive: true });
2619
- await cp(path, outputPath, { force: true });
2620
- }
2345
+ if (outputPath)
2346
+ await copyFile(path, outputPath);
2621
2347
  const source = outputPath ?? path;
2622
2348
  const adapter = await this.adapterClass.init(source, // oxlint-disable-line typescript/no-unnecessary-type-assertion
2623
2349
  options);
@@ -2662,14 +2388,14 @@ export class EpubFactory {
2662
2388
  mediaType: MediaType.XHTML.mime,
2663
2389
  properties: ["nav"],
2664
2390
  }, navContent, "utf-8");
2665
- // go over each xhtml item and replace the outdated
2666
- // <!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.1//EN' 'http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd'>
2667
- // with <!DOCTYPE html>
2391
+ // XHTML 1.x doctypes are not valid HTML5, so rewrite them to
2392
+ // <!DOCTYPE html> as part of the upgrade.
2668
2393
  const manifest = await epub.getManifest();
2669
2394
  for (const item of Object.values(manifest)) {
2670
2395
  if (MediaType.fromMime(item.mediaType ?? "") !== MediaType.XHTML)
2671
2396
  continue;
2672
2397
  const contents = await epub.readXhtmlItemContents(item.id);
2398
+ Upgrade.normalizeDoctype(contents);
2673
2399
  await epub.writeXhtmlItemContents(item.id, contents);
2674
2400
  }
2675
2401
  // oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- tsgolint treats the cast as unnecessary but it still narrows the type