@storyteller-platform/epub 0.8.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,20 +1,26 @@
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
4
  import { nanoid } from "nanoid";
7
5
  import { MediaType } from "@storyteller-platform/media-types";
8
- import { dirname } from "@storyteller-platform/path";
9
- import { TmpFsAdapter } from "./adapters/tmpfs.js";
6
+ import { copyFile } from "#files";
7
+ import { TmpFsAdapter } from "#tmpfs";
10
8
  import * as Upgrade from "./upgrade.js";
9
+ import { createDocument, createDocumentType, declareDefaultNamespace, findXmlChildByName, findXmlChildByNameNS, findXmlDescendantByName, getXhtmlBody, insert, isElement, isText, NS, parse, replace, serialize, text, x, } from "./xml.js";
10
+ export { TmpFsAdapter } from "#tmpfs";
11
11
  export { MemoryAdapter } from "./adapters/memory.js";
12
- export { TmpFsAdapter } from "./adapters/tmpfs.js";
12
+ /**
13
+ * The XML surface. Publication-level operations live on {@link Epub}; for
14
+ * building and inspecting XML trees directly, use these.
15
+ *
16
+ * The DOM node types are re-exported so that callers do not need to depend on
17
+ * `@xmldom/xmldom` themselves.
18
+ */
19
+ 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
20
  export class EpubVersionError extends Error {
14
21
  }
15
22
  export class EpubReadOnlyError extends Error {
16
23
  }
17
- const OPF_NAMESPACE = "http://www.idpf.org/2007/opf";
18
24
  /**
19
25
  * makes it easy to distinguish normal hrefs from external/invalid ones
20
26
  */
@@ -34,52 +40,48 @@ function resolveEntryUrl(from, href) {
34
40
  const url = new URL(href, base);
35
41
  return url.href.startsWith(ENTRY_ORIGIN) ? url : null;
36
42
  }
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
- }
43
+ function ensureOpfNamespace(doc) {
44
+ const { documentElement } = doc;
45
+ if (!documentElement)
46
+ return;
47
+ if (!documentElement.hasAttribute("xmlns")) {
48
+ declareDefaultNamespace(documentElement, NS.opf);
49
49
  }
50
- return renamed;
51
50
  }
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;
51
+ /**
52
+ * Names within a directory must stay distinct after Unicode normalization and
53
+ * full case folding, since names that collapse onto one another overwrite each
54
+ * other on any filesystem that normalizes.
55
+ *
56
+ * @link https://www.w3.org/TR/epub-33/#sec-names-paths
57
+ */
58
+ function foldPathName(path) {
59
+ return path.normalize("NFKC").toLowerCase();
66
60
  }
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;
61
+ /** The media types that make a manifest item an EPUB content document. */
62
+ const CONTENT_DOCUMENT_MEDIA_TYPES = new Set([
63
+ MediaType.XHTML.mime,
64
+ MediaType.SVG.mime,
65
+ ]);
66
+ /**
67
+ * Whether a manifest item may be referenced from the spine: it must be an EPUB
68
+ * content document, or a foreign content document whose manifest fallback chain
69
+ * reaches one.
70
+ *
71
+ * @link https://www.w3.org/TR/epub-33/#sec-item-elem
72
+ */
73
+ function spineReferenceable(manifest, item) {
74
+ const visited = new Set();
75
+ let current = item;
76
+ while (current && !visited.has(current.id)) {
77
+ visited.add(current.id);
78
+ if (current.mediaType &&
79
+ CONTENT_DOCUMENT_MEDIA_TYPES.has(current.mediaType)) {
80
+ return true;
81
+ }
82
+ current = current.fallback ? manifest[current.fallback] : undefined;
83
+ }
84
+ return false;
83
85
  }
84
86
  /**
85
87
  * A single EPUB instance.
@@ -98,7 +100,7 @@ function normalizeOpfNamespacePrefixes(doc) {
98
100
  * const chptOneXml = await epub.readXhtmlItemContents(chptOne.id);
99
101
  *
100
102
  * const body = getBody(chptOneXml);
101
- * const h1 = Epub.findXmlChildByName('h1', body);
103
+ * const h1 = findXmlChildByName('h1', body);
102
104
  * const headingText = textContent(h1);
103
105
  *
104
106
  * await epub.setTitle(headingText);
@@ -113,65 +115,6 @@ export class Epub {
113
115
  adapter;
114
116
  inputPath;
115
117
  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
118
  /**
176
119
  * Format a duration, provided as a number of seconds, as
177
120
  * a SMIL clock value, to be used for Media Overlays.
@@ -188,135 +131,6 @@ export class Epub {
188
131
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
189
132
  return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.padStart(2, "0")}.${millis ?? "0"}`;
190
133
  }
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
134
  rootfile = null;
321
135
  manifest = null;
322
136
  spine = null;
@@ -425,16 +239,15 @@ export class Epub {
425
239
  const containerString = await this.adapter.read("META-INF/container.xml", "utf-8");
426
240
  if (!containerString)
427
241
  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);
242
+ const containerDocument = parse(containerString, "text/xml");
243
+ const container = containerDocument.documentElement;
430
244
  if (!container)
431
245
  throw new Error("Failed to parse EPUB container.xml: Found no container element");
432
- const rootfiles = Epub.findXmlChildByName("rootfiles", Epub.getXmlChildren(container));
246
+ const rootfiles = findXmlChildByName("rootfiles", container);
433
247
  if (!rootfiles)
434
248
  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"];
249
+ const rootfile = findXmlChildByName("rootfile", rootfiles, (element) => element.getAttribute("media-type") === MediaType.OPF.mime);
250
+ const fullPath = rootfile?.getAttribute("full-path");
438
251
  if (!fullPath)
439
252
  throw new Error("Failed to parse EPUB container.xml: Found no rootfile element");
440
253
  this.rootfile = await this.resolveEntry("", fullPath);
@@ -448,12 +261,15 @@ export class Epub {
448
261
  return packageDocumentString;
449
262
  }
450
263
  async getPackageDocument() {
451
- const packageDocument = Epub.xmlParser.parse(await this.getPackageDocumentSource());
452
- return normalizeOpfNamespacePrefixes(packageDocument);
264
+ const packageDocument = parse(await this.getPackageDocumentSource(), "text/xml");
265
+ ensureOpfNamespace(packageDocument);
266
+ return packageDocument;
453
267
  }
454
268
  async getPackageElement() {
455
269
  const packageDocument = await this.getPackageDocument();
456
- const packageElement = Epub.findXmlChildByName("package", packageDocument);
270
+ const packageElement = packageDocument.documentElement?.localName === "package"
271
+ ? packageDocument.documentElement
272
+ : null;
457
273
  if (!packageElement) {
458
274
  throw new Error("Failed to parse EPUB: Found no package element in package document");
459
275
  }
@@ -475,12 +291,14 @@ export class Epub {
475
291
  this.assertWritable();
476
292
  await this.packageMutex.runExclusive(async () => {
477
293
  const packageDocument = await this.getPackageDocument();
478
- const packageElement = Epub.findXmlChildByName("package", packageDocument);
294
+ const packageElement = packageDocument.documentElement?.localName === "package"
295
+ ? packageDocument.documentElement
296
+ : null;
479
297
  if (!packageElement) {
480
298
  throw new Error("Failed to parse EPUB: Found no package element in package document");
481
299
  }
482
- const produced = (await producer(packageElement));
483
- const updatedPackageDocument = Epub.xmlBuilder.build(produced ?? packageDocument);
300
+ const produced = await producer(packageElement);
301
+ const updatedPackageDocument = serialize(produced ?? packageDocument);
484
302
  const rootfile = await this.getRootfile();
485
303
  await this.writeEntryContents(rootfile, updatedPackageDocument, "utf-8");
486
304
  });
@@ -497,24 +315,25 @@ export class Epub {
497
315
  if (this.manifest !== null)
498
316
  return this.manifest;
499
317
  const packageElement = await this.getPackageElement();
500
- const manifest = Epub.findXmlChildByName("manifest", Epub.getXmlChildren(packageElement));
501
- if (!manifest)
318
+ const manifest = findXmlChildByNameNS(NS.opf, "manifest", packageElement);
319
+ if (!manifest) {
502
320
  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"]) {
321
+ }
322
+ this.manifest = Array.from(manifest.children).reduce((acc, item) => {
323
+ const id = item.getAttribute("id");
324
+ const href = item.getAttribute("href");
325
+ if (!id || !href) {
507
326
  return acc;
508
327
  }
509
328
  return {
510
329
  ...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(" "),
330
+ [id]: {
331
+ id,
332
+ href,
333
+ mediaType: item.getAttribute("media-type") ?? undefined,
334
+ mediaOverlay: item.getAttribute("media-overlay") ?? undefined,
335
+ fallback: item.getAttribute("fallback") ?? undefined,
336
+ properties: item.getAttribute("properties")?.split(" ") ?? undefined,
518
337
  },
519
338
  };
520
339
  }, {});
@@ -531,13 +350,11 @@ export class Epub {
531
350
  * array, this method is only meant to be used internally.
532
351
  */
533
352
  findMetadataIndex(packageElement, predicate) {
534
- const metadataElement = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
353
+ const metadataElement = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
535
354
  if (!metadataElement)
536
355
  throw new Error("Failed to parse EPUB: Found no metadata element in package document");
537
- return metadataElement.metadata.findIndex((node) => {
356
+ return Array.from(metadataElement.children).findIndex((node) => {
538
357
  const item = Epub.parseMetadataItem(node);
539
- if (!item)
540
- return false;
541
358
  return predicate(item);
542
359
  });
543
360
  }
@@ -555,36 +372,26 @@ export class Epub {
555
372
  */
556
373
  async findAllMetadataItems(predicate) {
557
374
  const packageElement = await this.getPackageElement();
558
- const metadataElement = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
375
+ const metadataElement = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
559
376
  if (!metadataElement)
560
377
  throw new Error("Failed to parse EPUB: Found no metadata element in package document");
561
- const elements = metadataElement.metadata.filter((node) => {
378
+ const elements = metadataElement.children.filter((node) => {
562
379
  const item = Epub.parseMetadataItem(node);
563
- if (!item)
564
- return false;
565
380
  return predicate(item);
566
381
  });
567
- return elements
568
- .map((element) => Epub.parseMetadataItem(element))
569
- .filter((item) => !!item);
382
+ return elements.map((element) => Epub.parseMetadataItem(element));
570
383
  }
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];
384
+ static parseMetadataItem(element) {
385
+ const elementName = element.nodeName;
386
+ const textNode = element.firstChild;
576
387
  // https://www.w3.org/TR/epub-33/#sec-metadata-values
577
388
  // Whitespace within these element values is not significant.
578
389
  // Sequences of one or more whitespace characters are collapsed
579
390
  // to a single space [infra] during processing .
580
- const value = !textNode || !Epub.isXmlTextNode(textNode)
391
+ const value = !textNode || !isText(textNode)
581
392
  ? 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
- ]));
393
+ : textNode.data.replaceAll(/\s+/g, " ").trim();
394
+ const { id, ...properties } = Object.fromEntries(Array.from(element.attributes).map((attr) => [attr.name, attr.value]));
588
395
  return {
589
396
  id,
590
397
  type: elementName,
@@ -605,12 +412,10 @@ export class Epub {
605
412
  */
606
413
  async getMetadata() {
607
414
  const packageElement = await this.getPackageElement();
608
- const metadataElement = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
415
+ const metadataElement = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
609
416
  if (!metadataElement)
610
417
  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);
418
+ const metadata = Array.from(metadataElement.children).map((node) => Epub.parseMetadataItem(node));
614
419
  return metadata;
615
420
  }
616
421
  /**
@@ -675,7 +480,7 @@ export class Epub {
675
480
  */
676
481
  async setUniqueIdentifier(identifier) {
677
482
  await this.withPackage(async (packageElement) => {
678
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
483
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
679
484
  if (!metadata) {
680
485
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
681
486
  }
@@ -683,23 +488,15 @@ export class Epub {
683
488
  if (!uniqueId) {
684
489
  // Create a new unique identifier id
685
490
  const newUniqueId = nanoid();
686
- packageElement[":@"] = {
687
- ...packageElement[":@"],
688
- "@_unique-identifier": newUniqueId,
689
- };
491
+ packageElement.setAttribute("unique-identifier", newUniqueId);
690
492
  uniqueId = newUniqueId;
691
493
  }
692
- const children = Epub.getXmlChildren(metadata);
693
- const entry = Epub.findXmlChildByName("dc:identifier", children, (node) => node[":@"]?.["@_id"] === uniqueId);
494
+ const entry = findXmlChildByNameNS(NS.dc, "identifier", metadata, (node) => node.getAttribute("id") === uniqueId);
694
495
  if (entry) {
695
- children.splice(children.indexOf(entry), 1, Epub.createXmlElement("dc:identifier", { id: uniqueId }, [
696
- Epub.createXmlTextNode(identifier),
697
- ]));
496
+ replace(metadata, _jsx("dc:identifier", { id: uniqueId, children: identifier }), entry);
698
497
  return;
699
498
  }
700
- children.push(Epub.createXmlElement("dc:identifier", { id: uniqueId }, [
701
- Epub.createXmlTextNode(identifier),
702
- ]));
499
+ insert(metadata, _jsx("dc:identifier", { id: uniqueId, children: identifier }));
703
500
  });
704
501
  }
705
502
  /**
@@ -710,7 +507,7 @@ export class Epub {
710
507
  */
711
508
  async getUniqueIdentifierId() {
712
509
  const packageElement = await this.getPackageElement();
713
- return packageElement[":@"]?.["@_unique-identifier"] ?? null;
510
+ return packageElement.getAttribute("unique-identifier") ?? null;
714
511
  }
715
512
  /**
716
513
  * Collect `dc:identifier` or `dc:source` entries, attaching the value and
@@ -855,47 +652,37 @@ export class Epub {
855
652
  async setIdentifiers(identifiers) {
856
653
  const uniqueId = await this.getUniqueIdentifierId();
857
654
  await this.withPackage((packageElement) => {
858
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
859
- if (!metadata)
655
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
656
+ if (!metadata) {
860
657
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
861
- const children = Epub.getXmlChildren(metadata);
658
+ }
862
659
  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") {
660
+ for (const child of Array.from(metadata.children)) {
661
+ if (child.nodeName !== "dc:identifier") {
868
662
  continue;
869
663
  }
870
- const id = node[":@"]?.["@_id"];
664
+ const id = child.getAttribute("id");
871
665
  if (id && id === uniqueId) {
872
666
  continue;
873
667
  }
874
668
  if (id) {
875
669
  removedIds.add(id);
876
670
  }
877
- children.splice(i, 1);
671
+ metadata.removeChild(child);
878
672
  }
879
- Epub.removeRefiningMetas(children, removedIds, ["identifier-type"]);
673
+ Epub.removeRefiningMetas(metadata, removedIds, ["identifier-type"]);
880
674
  for (const identifier of identifiers) {
881
675
  // we don't allow you to set the unique identifier, you need to use setUniqueIdentifier
882
676
  if (identifier.id && identifier.id === uniqueId)
883
677
  continue;
884
678
  const id = identifier.id ??
885
679
  (identifier.identifierType !== undefined ? nanoid() : undefined);
886
- children.push(Epub.createXmlElement("dc:identifier", {
887
- ...(id && { id }),
888
- ...(identifier.scheme &&
889
- identifier.identifierType === undefined && {
680
+ insert(metadata, _jsx("dc:identifier", { ...(id && { id }), ...(identifier.scheme &&
681
+ identifier.id === undefined && {
890
682
  "opf:scheme": identifier.scheme,
891
- }),
892
- }, [Epub.createXmlTextNode(identifier.value)]));
683
+ }), children: identifier.value }));
893
684
  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)]));
685
+ insert(metadata, _jsx("meta", { refines: `#${id}`, property: "identifier-type", ...(identifier.scheme && { scheme: identifier.scheme }), children: identifier.identifierType }));
899
686
  }
900
687
  }
901
688
  });
@@ -903,21 +690,19 @@ export class Epub {
903
690
  /**
904
691
  * Remove `meta` refinements pointing at any of the given ids,
905
692
  * restricted to the given `property` values as a cleanup step
906
- * Mutates `children` in place.
693
+ * Mutates `metadata` in place.
907
694
  */
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") {
695
+ static removeRefiningMetas(metadata, ids, properties) {
696
+ for (const child of Array.from(metadata.children)) {
697
+ if (child.nodeName !== "meta") {
913
698
  continue;
914
699
  }
915
- const property = node[":@"]?.["@_property"];
916
- if (!property || !properties.includes(property))
700
+ const property = child.getAttribute("property");
701
+ if (properties && (!property || !properties.includes(property)))
917
702
  continue;
918
- const refines = node[":@"]?.["@_refines"]?.slice(1);
703
+ const refines = child.getAttribute("refines")?.slice(1);
919
704
  if (refines && ids.has(refines)) {
920
- children.splice(i, 1);
705
+ metadata.removeChild(child);
921
706
  }
922
707
  }
923
708
  }
@@ -937,47 +722,37 @@ export class Epub {
937
722
  */
938
723
  async setSources(sources) {
939
724
  await this.withPackage((packageElement) => {
940
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
941
- if (!metadata)
725
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
726
+ if (!metadata) {
942
727
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
943
- const children = Epub.getXmlChildren(metadata);
728
+ }
944
729
  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") {
730
+ for (const child of Array.from(metadata.children)) {
731
+ if (child.nodeName !== "dc:source") {
950
732
  continue;
951
733
  }
952
- const id = node[":@"]?.["@_id"];
734
+ const id = child.getAttribute("id");
953
735
  if (id) {
954
736
  removedIds.add(id);
955
737
  }
956
- children.splice(i, 1);
738
+ metadata.removeChild(child);
957
739
  }
958
- Epub.removeRefiningMetas(children, removedIds, [
740
+ Epub.removeRefiningMetas(metadata, removedIds, [
959
741
  "identifier-type",
960
742
  "source-of",
961
743
  ]);
962
744
  for (const source of sources) {
963
745
  const needsId = source.identifierType !== undefined || source.isPageBreakSource;
964
746
  const id = source.id ?? (needsId ? nanoid() : undefined);
965
- children.push(Epub.createXmlElement("dc:source", {
966
- ...(id && { id }),
967
- ...(source.scheme &&
747
+ insert(metadata, _jsx("dc:source", { ...(id && { id }), ...(source.scheme &&
968
748
  source.identifierType === undefined && {
969
749
  "opf:scheme": source.scheme,
970
- }),
971
- }, [Epub.createXmlTextNode(source.value)]));
750
+ }), children: source.value }));
972
751
  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)]));
752
+ insert(metadata, _jsx("meta", { refines: `#${id}`, property: "identifier-type", ...(source.scheme && { scheme: source.scheme }), children: source.identifierType }));
978
753
  }
979
754
  if (source.isPageBreakSource && id) {
980
- children.push(Epub.createXmlElement("meta", { refines: `#${id}`, property: "source-of" }, [Epub.createXmlTextNode("pagination")]));
755
+ insert(metadata, _jsx("meta", { refines: `#${id}`, property: "source-of", children: "pagination" }));
981
756
  }
982
757
  }
983
758
  });
@@ -991,11 +766,11 @@ export class Epub {
991
766
  */
992
767
  async getEpub2CoverImage() {
993
768
  const packageElement = await this.getPackageElement();
994
- const metadataElement = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
769
+ const metadataElement = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
995
770
  if (!metadataElement)
996
771
  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"];
772
+ const coverImageElement = Array.from(metadataElement.children).find((element) => element.getAttribute("name") === "cover");
773
+ const manifestItemId = coverImageElement?.getAttribute("content");
999
774
  if (!manifestItemId)
1000
775
  return null;
1001
776
  const manifest = await this.getManifest();
@@ -1121,7 +896,7 @@ export class Epub {
1121
896
  */
1122
897
  async getBaseDirection() {
1123
898
  const packageEl = await this.getPackageElement();
1124
- const dir = packageEl[":@"]?.["@_dir"];
899
+ const dir = packageEl.getAttribute("dir");
1125
900
  if (dir !== "ltr" && dir !== "rtl" && dir !== "auto") {
1126
901
  return "auto";
1127
902
  }
@@ -1198,24 +973,14 @@ export class Epub {
1198
973
  */
1199
974
  async removeSubject(index) {
1200
975
  await this.withPackage((packageElement) => {
1201
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
976
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
1202
977
  if (!metadata)
1203
978
  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)
979
+ const subjects = metadata.children.filter((child) => child.nodeName === "dc:subject");
980
+ const subject = subjects[index];
981
+ if (!subject)
1217
982
  return;
1218
- Epub.getXmlChildren(metadata).splice(metadataIndex, 1);
983
+ metadata.removeChild(subject);
1219
984
  });
1220
985
  }
1221
986
  /**
@@ -1412,7 +1177,7 @@ export class Epub {
1412
1177
  */
1413
1178
  async getPackageVocabularyPrefixes() {
1414
1179
  const packageElement = await this.getPackageElement();
1415
- const prefixValue = packageElement[":@"]?.["@_prefix"];
1180
+ const prefixValue = packageElement.getAttribute("prefix");
1416
1181
  if (!prefixValue)
1417
1182
  return {};
1418
1183
  const matches = prefixValue.matchAll(/(?:([a-z]+): +(\S+)\s*)/gs);
@@ -1427,10 +1192,9 @@ export class Epub {
1427
1192
  await this.withPackage(async (packageElement) => {
1428
1193
  const prefixes = await this.getPackageVocabularyPrefixes();
1429
1194
  prefixes[prefix] = uri;
1430
- packageElement[":@"] ??= {};
1431
- packageElement[":@"]["@_prefix"] = Object.entries(prefixes)
1195
+ packageElement.setAttribute("prefix", Object.entries(prefixes)
1432
1196
  .map(([p, u]) => `${p}: ${u}`)
1433
- .join("\n ");
1197
+ .join("\n "));
1434
1198
  });
1435
1199
  }
1436
1200
  /**
@@ -1447,49 +1211,44 @@ export class Epub {
1447
1211
  // rather than a single string, to support expanded titles.
1448
1212
  async setTitle(title) {
1449
1213
  await this.withPackage((packageElement) => {
1450
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
1451
- if (!metadata)
1214
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
1215
+ if (!metadata) {
1452
1216
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
1453
- const titleElement = Epub.findXmlChildByName("dc:title", metadata.metadata);
1217
+ }
1218
+ const titleElement = findXmlChildByNameNS(NS.dc, "title", metadata);
1454
1219
  if (!titleElement) {
1455
- Epub.getXmlChildren(metadata).push(Epub.createXmlElement("dc:title", {}, [
1456
- Epub.createXmlTextNode(title),
1457
- ]));
1220
+ insert(metadata, _jsx("dc:title", { children: title }));
1458
1221
  }
1459
1222
  else {
1460
- titleElement["dc:title"] = [Epub.createXmlTextNode(title)];
1223
+ while (titleElement.firstChild) {
1224
+ titleElement.removeChild(titleElement.firstChild);
1225
+ }
1226
+ insert(titleElement, text(title));
1461
1227
  }
1462
1228
  });
1463
1229
  }
1464
1230
  async setTitles(entries) {
1465
1231
  await this.withPackage((packageElement) => {
1466
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
1232
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
1467
1233
  if (!metadata) {
1468
1234
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
1469
1235
  }
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);
1236
+ for (const meta of Array.from(metadata.children)) {
1237
+ if (meta.nodeName === "dc:title" ||
1238
+ meta.getAttribute("property") === "title-type" ||
1239
+ meta.getAttribute("property") === "display-seq") {
1240
+ metadata.removeChild(meta);
1480
1241
  }
1481
1242
  }
1482
1243
  for (let i = 0; i < entries.length; i++) {
1483
1244
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1484
1245
  const entry = entries[i];
1485
1246
  const id = nanoid();
1486
- metadataEntries.push(Epub.createXmlElement("dc:title", { id }, [
1487
- Epub.createXmlTextNode(entry.title),
1488
- ]));
1247
+ insert(metadata, _jsx("dc:title", { id: id, children: entry.title }));
1489
1248
  if (entry.type) {
1490
- metadataEntries.push(Epub.createXmlElement("meta", { refines: `#${id}`, property: "title-type" }, [Epub.createXmlTextNode(entry.type)]));
1249
+ insert(metadata, _jsx("meta", { refines: `#${id}`, property: "title-type", children: entry.type }));
1491
1250
  }
1492
- metadataEntries.push(Epub.createXmlElement("meta", { refines: `#${id}`, property: "display-seq" }, [Epub.createXmlTextNode((i + 1).toString())]));
1251
+ insert(metadata, _jsx("meta", { refines: `#${id}`, property: "display-seq", children: (i + 1).toString() }));
1493
1252
  }
1494
1253
  });
1495
1254
  }
@@ -1530,24 +1289,24 @@ export class Epub {
1530
1289
  // We have to manually find the correct insertion point
1531
1290
  // based on the provided index
1532
1291
  await this.withPackage((packageElement) => {
1533
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
1292
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
1534
1293
  if (!metadata)
1535
1294
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
1536
1295
  let collectionCount = 0;
1537
1296
  let metadataIndex = 0;
1538
- for (const meta of Epub.getXmlChildren(metadata)) {
1297
+ for (const meta of metadata.childNodes) {
1539
1298
  if (collectionCount === index)
1540
1299
  break;
1541
1300
  metadataIndex++;
1542
- if (Epub.isXmlTextNode(meta))
1301
+ if (!isElement(meta))
1543
1302
  continue;
1544
- if (Epub.getXmlElementName(meta) !== "meta")
1303
+ if (meta.nodeName !== "meta")
1545
1304
  continue;
1546
- if (meta[":@"]?.["@_property"] !== "belongs-to-collection")
1305
+ if (meta.getAttribute("property") !== "belongs-to-collection")
1547
1306
  continue;
1548
1307
  collectionCount++;
1549
1308
  }
1550
- Epub.getXmlChildren(metadata).splice(metadataIndex, 0, Epub.createXmlElement("meta", { id: collectionId, property: "belongs-to-collection" }, [Epub.createXmlTextNode(collection.name)]));
1309
+ insert(metadata, _jsx("meta", { id: collectionId, property: "belongs-to-collection", children: collection.name }), metadata.childNodes.item(metadataIndex));
1551
1310
  });
1552
1311
  // These can all just go at the end; order is only
1553
1312
  // important for the `dc:creator`/`dc:contributor`
@@ -1578,38 +1337,20 @@ export class Epub {
1578
1337
  */
1579
1338
  async removeCollection(index) {
1580
1339
  await this.withPackage((packageElement) => {
1581
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
1582
- if (!metadata)
1340
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
1341
+ if (!metadata) {
1583
1342
  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
1343
  }
1598
- if (collectionCount === null || metadataIndex === null)
1344
+ // The index refers to what `getCollections()` returns, so filter the
1345
+ // same way before indexing
1346
+ const removed = Array.from(metadata.children).filter((child) => child.getAttribute("property") === "belongs-to-collection" &&
1347
+ !!child.textContent?.trim())[index];
1348
+ if (!removed)
1599
1349
  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);
1350
+ const removedId = removed.getAttribute("id");
1351
+ metadata.removeChild(removed);
1352
+ if (removedId) {
1353
+ Epub.removeRefiningMetas(metadata, new Set([removedId]));
1613
1354
  }
1614
1355
  });
1615
1356
  }
@@ -1692,24 +1433,24 @@ export class Epub {
1692
1433
  // We have to manually find the correct insertion point
1693
1434
  // based on the provided index
1694
1435
  await this.withPackage((packageElement) => {
1695
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
1436
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
1696
1437
  if (!metadata)
1697
1438
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
1698
1439
  let creatorCount = 0;
1699
1440
  let metadataIndex = 0;
1700
- for (const meta of Epub.getXmlChildren(metadata)) {
1441
+ for (const meta of metadata.childNodes) {
1701
1442
  if (creatorCount === index)
1702
1443
  break;
1703
1444
  metadataIndex++;
1704
- if (Epub.isXmlTextNode(meta))
1445
+ if (!isElement(meta))
1446
+ continue;
1447
+ if (meta.nodeName !== `dc:${type}`)
1705
1448
  continue;
1706
- if (Epub.getXmlElementName(meta) !== `dc:${type}`)
1449
+ if (meta.getAttribute("property") !== "belongs-to-collection")
1707
1450
  continue;
1708
1451
  creatorCount++;
1709
1452
  }
1710
- Epub.getXmlChildren(metadata).splice(metadataIndex, 0, Epub.createXmlElement(`dc:${type}`, { id: creatorId }, [
1711
- Epub.createXmlTextNode(creator.name),
1712
- ]));
1453
+ insert(metadata, x(`dc:${type}`, { id: creatorId }, creator.name), metadata.childNodes.item(metadataIndex));
1713
1454
  });
1714
1455
  // These can all just go at the end; order is only
1715
1456
  // important for the `dc:creator`/`dc:contributor`
@@ -1756,36 +1497,16 @@ export class Epub {
1756
1497
  */
1757
1498
  async removeCreator(index, type = "creator") {
1758
1499
  await this.withPackage((packageElement) => {
1759
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
1500
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
1760
1501
  if (!metadata)
1761
1502
  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)
1503
+ const toRemove = Array.from(metadata.children).filter((child) => child.nodeName === `dc:${type}`)[index];
1504
+ if (!toRemove)
1775
1505
  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);
1506
+ const id = toRemove.getAttribute("id");
1507
+ metadata.removeChild(toRemove);
1508
+ if (id) {
1509
+ Epub.removeRefiningMetas(metadata, new Set([id]));
1789
1510
  }
1790
1511
  });
1791
1512
  }
@@ -1822,17 +1543,14 @@ export class Epub {
1822
1543
  if (this.spine !== null)
1823
1544
  return this.spine;
1824
1545
  const packageElement = await this.getPackageElement();
1825
- const spine = Epub.findXmlChildByName("spine", Epub.getXmlChildren(packageElement));
1546
+ const spine = findXmlChildByNameNS(NS.opf, "spine", packageElement);
1826
1547
  if (!spine)
1827
1548
  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))
1549
+ this.spine = Array.from(spine.children)
1830
1550
  .map((itemref) => {
1831
- const idRef = itemref[":@"]?.["@_idref"];
1832
- const linear = itemref[":@"]?.["@_linear"];
1833
1551
  return {
1834
- itemRef: idRef,
1835
- linear: linear === "no" ? false : true,
1552
+ itemRef: itemref.getAttribute("idref"),
1553
+ linear: itemref.getAttribute("linear") !== "no",
1836
1554
  };
1837
1555
  })
1838
1556
  .filter((itemref) => !!itemref.itemRef);
@@ -1871,21 +1589,24 @@ export class Epub {
1871
1589
  * @link https://www.w3.org/TR/epub-33/#sec-spine-elem
1872
1590
  */
1873
1591
  async addSpineItem(manifestId, index) {
1874
- const item = Epub.createXmlElement("itemref", { idref: manifestId });
1592
+ const item = _jsx("itemref", { idref: manifestId });
1875
1593
  const manifest = await this.getManifest();
1876
1594
  const manifestItem = manifest[manifestId];
1877
1595
  if (!manifestItem)
1878
1596
  throw new Error(`Manifest item not found with id "${manifestId}"`);
1597
+ if ((await this.getSpine()).some(({ itemRef }) => itemRef === manifestId)) {
1598
+ throw new Error(`Manifest item "${manifestId}" is already referenced in the spine`);
1599
+ }
1600
+ if (!spineReferenceable(manifest, manifestItem)) {
1601
+ throw new Error(`Manifest item "${manifestId}" is not an EPUB content document, so it cannot be added to the spine`);
1602
+ }
1879
1603
  await this.withPackage((packageElement) => {
1880
- const spine = Epub.findXmlChildByName("spine", Epub.getXmlChildren(packageElement));
1604
+ const spine = findXmlChildByNameNS(NS.opf, "spine", packageElement);
1881
1605
  if (!spine)
1882
1606
  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
- }
1607
+ insert(spine, item, index === undefined
1608
+ ? null
1609
+ : (Array.from(spine.children)[index] ?? null));
1889
1610
  });
1890
1611
  // Reset the spine cache
1891
1612
  this.spine = null;
@@ -1897,41 +1618,42 @@ export class Epub {
1897
1618
  */
1898
1619
  async removeSpineItem(index) {
1899
1620
  await this.withPackage((packageElement) => {
1900
- const spine = Epub.findXmlChildByName("spine", Epub.getXmlChildren(packageElement));
1621
+ const spine = findXmlChildByNameNS(NS.opf, "spine", packageElement);
1901
1622
  if (!spine)
1902
1623
  throw new Error("Failed to parse EPUB: Found no spine element in package document");
1903
- Epub.getXmlChildren(spine).splice(index, 1);
1624
+ const itemref = Array.from(spine.children)[index];
1625
+ if (itemref)
1626
+ spine.removeChild(itemref);
1904
1627
  });
1905
1628
  // Reset the spine cache
1906
1629
  this.spine = null;
1907
1630
  }
1908
1631
  async getNavigationChildren(ol, navHref, { resolveToRoot } = {}) {
1909
1632
  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)));
1633
+ const listItems = Array.from(ol.children).filter((element) => element.nodeName === "li");
1634
+ for (const childEl of listItems) {
1635
+ const [firstChild, secondChild] = Array.from(childEl.children).filter((element) => ["a", "span", "ol"].includes(element.nodeName));
1914
1636
  if (!firstChild)
1915
1637
  continue;
1916
- if (!["a", "span"].includes(Epub.getXmlElementName(firstChild))) {
1638
+ const name = firstChild.nodeName;
1639
+ if (name !== "a" && name !== "span")
1917
1640
  continue;
1918
- }
1919
- if (Epub.getXmlElementName(firstChild) === "span" &&
1920
- (!secondChild || Epub.getXmlElementName(secondChild) !== "ol")) {
1641
+ if (name === "span" && (!secondChild || secondChild.nodeName !== "ol")) {
1921
1642
  continue;
1922
1643
  }
1923
- const role = firstChild[":@"]?.["@_epub:type"]?.trim();
1644
+ const role = firstChild.getAttribute("epub:type")?.trim();
1645
+ const href = firstChild.getAttribute("href");
1924
1646
  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, {
1647
+ title: firstChild.textContent ?? "",
1648
+ ...(name === "a" &&
1649
+ href && {
1650
+ href: await this.resolveHref(href, navHref, {
1929
1651
  toRoot: resolveToRoot,
1930
1652
  }),
1931
1653
  }),
1932
1654
  ...(role && { role }),
1933
1655
  ...(secondChild &&
1934
- Epub.getXmlElementName(secondChild) === "ol" && {
1656
+ secondChild.nodeName === "ol" && {
1935
1657
  children: await this.getNavigationChildren(secondChild, navHref, {
1936
1658
  resolveToRoot,
1937
1659
  }),
@@ -1946,19 +1668,21 @@ export class Epub {
1946
1668
  if (!navItem)
1947
1669
  return null;
1948
1670
  const navContents = await this.readXhtmlItemContents(navItem.id);
1949
- const navEl = Epub.findXmlDescendantByName("nav", navContents, (node) => Epub.getXmlAttributes(node)["epub:type"] === role);
1671
+ const html = navContents.documentElement;
1672
+ if (!html)
1673
+ return null;
1674
+ const navEl = findXmlDescendantByName("nav", html, (node) => node.getAttribute("epub:type") === role);
1950
1675
  if (!navEl)
1951
1676
  return null;
1952
- const [firstChild, secondChild] = Epub.getXmlChildren(navEl).filter((node) => !!(!Epub.isXmlTextNode(node) &&
1953
- Epub.getXmlElementName(node).match(/(?:h[1-6]|ol)/)));
1677
+ const [firstChild, secondChild] = Array.from(navEl.children).filter((element) => /h[1-6]|ol/.test(element.nodeName));
1954
1678
  if (!firstChild)
1955
1679
  return null;
1956
- const title = Epub.getXmlElementName(firstChild).match(/h[1-6]/)
1957
- ? Epub.getXhtmlTextContent(Epub.getXmlChildren(firstChild))
1680
+ const title = /h[1-6]/.test(firstChild.nodeName)
1681
+ ? (firstChild.textContent ?? "")
1958
1682
  : null;
1959
- const list = Epub.getXmlElementName(firstChild) === "ol"
1683
+ const list = firstChild.nodeName === "ol"
1960
1684
  ? firstChild
1961
- : secondChild && Epub.getXmlElementName(secondChild) === "ol"
1685
+ : secondChild && secondChild.nodeName === "ol"
1962
1686
  ? secondChild
1963
1687
  : null;
1964
1688
  if (!list)
@@ -2103,6 +1827,26 @@ export class Epub {
2103
1827
  : await this.adapter.read(entry);
2104
1828
  return itemEntry;
2105
1829
  }
1830
+ /**
1831
+ * Create a new XML document with the given root node.
1832
+ *
1833
+ * @param root The XML node to use as the document element
1834
+ * @param language Optional - defaults to the EPUB's language
1835
+ */
1836
+ createXmlDocument(root) {
1837
+ const document = createDocument(root.namespaceURI ?? root.getAttribute("xmlns"), root.nodeName, null);
1838
+ const docRoot = document.documentElement;
1839
+ if (!docRoot)
1840
+ throw new Error("Failed to create XML document");
1841
+ for (const attr of root.attributes) {
1842
+ docRoot.setAttributeNode(attr.cloneNode());
1843
+ }
1844
+ for (const child of Array.from(root.childNodes)) {
1845
+ insert(docRoot, child);
1846
+ }
1847
+ document.insertBefore(document.createProcessingInstruction("xml", 'version="1.0" encoding="UTF-8"'), docRoot);
1848
+ return document;
1849
+ }
2106
1850
  /**
2107
1851
  * Create a new XHTML document with the given body
2108
1852
  * and head.
@@ -2113,27 +1857,32 @@ export class Epub {
2113
1857
  */
2114
1858
  async createXhtmlDocument(body, head, language) {
2115
1859
  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
- ];
1860
+ const document = createDocument(NS.xhtml, "html", createDocumentType("html", "", ""));
1861
+ const html = document.documentElement;
1862
+ if (!html)
1863
+ throw new Error("Failed to create XHTML document");
1864
+ html.setAttributeNS(NS.xmlns, "xmlns:epub", NS.epub);
1865
+ if (lang) {
1866
+ html.setAttributeNS(NS.xml, "xml:lang", lang.toString());
1867
+ html.setAttribute("lang", lang.toString());
1868
+ }
1869
+ const headElement = document.createElement("head");
1870
+ for (const node of head ?? [])
1871
+ insert(headElement, node);
1872
+ insert(html, headElement);
1873
+ const bodyElement = document.createElement("body");
1874
+ for (const node of body)
1875
+ insert(bodyElement, node);
1876
+ insert(html, bodyElement);
1877
+ document.insertBefore(document.createProcessingInstruction("xml", 'version="1.0" encoding="UTF-8"'), document.doctype);
1878
+ return document;
2129
1879
  }
2130
1880
  async readXhtmlItemContents(id, as = "xhtml") {
2131
1881
  const contents = await this.readItemContents(id, "utf-8");
2132
- const xml = Epub.xhtmlParser.parse(contents);
1882
+ const document = parse(contents, "application/xhtml+xml");
2133
1883
  if (as === "xhtml")
2134
- return xml;
2135
- const body = Epub.getXhtmlBody(xml);
2136
- return Epub.getXhtmlTextContent(body);
1884
+ return document;
1885
+ return getXhtmlBody(document).textContent ?? "";
2137
1886
  }
2138
1887
  async writeEntryContents(path, contents, encoding) {
2139
1888
  this.assertWritable();
@@ -2177,42 +1926,52 @@ export class Epub {
2177
1926
  * @link https://www.w3.org/TR/epub-33/#sec-xhtml
2178
1927
  */
2179
1928
  async writeXhtmlItemContents(id, contents) {
2180
- await this.writeItemContents(id, Epub.xhtmlBuilder.build(contents), "utf-8");
1929
+ await this.writeItemContents(id, serialize(contents), "utf-8");
2181
1930
  }
2182
1931
  async removeManifestItem(id) {
2183
1932
  await this.withPackage(async (packageElement) => {
2184
- const manifest = Epub.findXmlChildByName("manifest", Epub.getXmlChildren(packageElement));
1933
+ const manifest = findXmlChildByNameNS(NS.opf, "manifest", packageElement);
2185
1934
  if (!manifest)
2186
1935
  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)
1936
+ const item = Array.from(manifest.children).find((element) => element.getAttribute("id") === id);
1937
+ if (!item)
2189
1938
  return;
2190
- const [item] = Epub.getXmlChildren(manifest).splice(itemIndex, 1);
2191
- if (!item || Epub.isXmlTextNode(item) || !item[":@"]?.["@_href"])
1939
+ const href = item.getAttribute("href");
1940
+ manifest.removeChild(item);
1941
+ const spine = findXmlChildByNameNS(NS.opf, "spine", packageElement);
1942
+ if (spine) {
1943
+ for (const itemref of Array.from(spine.children)) {
1944
+ if (itemref.getAttribute("idref") === id)
1945
+ spine.removeChild(itemref);
1946
+ }
1947
+ }
1948
+ if (!href)
2192
1949
  return;
2193
- await this.removeEntry(item[":@"]["@_href"]);
1950
+ await this.removeEntry(href);
2194
1951
  });
2195
- // Reset the cached manifest, so that it will be read from
1952
+ // Reset the cached manifest and spine, so that they will be read from
2196
1953
  // the updated XML next time
2197
1954
  this.manifest = null;
1955
+ this.spine = null;
2198
1956
  }
2199
1957
  async addManifestItem(item, contents, encoding) {
1958
+ const existingItems = Object.values(await this.getManifest());
1959
+ if (existingItems.some((existing) => existing.id === item.id)) {
1960
+ throw new Error(`Cannot add manifest item: an item with the id "${item.id}" already exists`);
1961
+ }
1962
+ // 4.2.1: hrefs that normalize and case-fold to the same path would
1963
+ // overwrite each other on a normalizing filesystem.
1964
+ const foldedHref = foldPathName(item.href);
1965
+ for (const existing of existingItems) {
1966
+ if (foldPathName(existing.href) === foldedHref) {
1967
+ throw new Error(`Cannot add manifest item: "${item.href}" collides with the existing item "${existing.href}"`);
1968
+ }
1969
+ }
2200
1970
  await this.withPackage((packageElement) => {
2201
- const manifest = Epub.findXmlChildByName("manifest", Epub.getXmlChildren(packageElement));
1971
+ const manifest = findXmlChildByNameNS(NS.opf, "manifest", packageElement);
2202
1972
  if (!manifest)
2203
1973
  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
- }));
1974
+ 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
1975
  });
2217
1976
  // Reset the cached manifest, so that it will be read from
2218
1977
  // the updated XML next time
@@ -2223,7 +1982,7 @@ export class Epub {
2223
1982
  const data = encoding === "utf-8" || encoding === "xml"
2224
1983
  ? new TextEncoder().encode(encoding === "utf-8"
2225
1984
  ? contents
2226
- : Epub.xmlBuilder.build(contents))
1985
+ : serialize(contents))
2227
1986
  : contents;
2228
1987
  /* oxlint-enable typescript/no-unnecessary-type-assertion */
2229
1988
  await this.writeEntryContents(filename, data);
@@ -2238,22 +1997,17 @@ export class Epub {
2238
1997
  */
2239
1998
  async updateManifestItem(id, newItem) {
2240
1999
  await this.withPackage((packageElement) => {
2241
- const manifest = Epub.findXmlChildByName("manifest", Epub.getXmlChildren(packageElement));
2000
+ const manifest = findXmlChildByNameNS(NS.opf, "manifest", packageElement);
2242
2001
  if (!manifest)
2243
2002
  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 && {
2003
+ const existing = Array.from(manifest.children).find((element) => element.getAttribute("id") === id);
2004
+ if (!existing)
2005
+ return;
2006
+ replace(manifest, _jsx("item", { id: id, href: newItem.href, ...(newItem.mediaType && { "media-type": newItem.mediaType }), ...(newItem.fallback && { fallback: newItem.fallback }), ...(newItem.mediaOverlay && {
2251
2007
  "media-overlay": newItem.mediaOverlay,
2252
- }),
2253
- ...(newItem.properties && {
2008
+ }), ...(newItem.properties && {
2254
2009
  properties: newItem.properties.join(" "),
2255
- }),
2256
- }));
2010
+ }) }), existing);
2257
2011
  });
2258
2012
  // Reset the cached manifest, so that it will be read from
2259
2013
  // the updated XML next time
@@ -2271,15 +2025,13 @@ export class Epub {
2271
2025
  */
2272
2026
  async addMetadata(entry) {
2273
2027
  await this.withPackage((packageElement) => {
2274
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
2028
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
2275
2029
  if (!metadata)
2276
2030
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
2277
- Epub.getXmlChildren(metadata).push(Epub.createXmlElement(entry.type, {
2031
+ insert(metadata, x(entry.type, {
2278
2032
  ...(entry.id && { id: entry.id }),
2279
2033
  ...entry.properties,
2280
- }, entry.value !== undefined
2281
- ? [Epub.createXmlTextNode(entry.value)]
2282
- : []));
2034
+ }, entry.value));
2283
2035
  });
2284
2036
  }
2285
2037
  /**
@@ -2297,19 +2049,22 @@ export class Epub {
2297
2049
  */
2298
2050
  async replaceMetadata(predicate, entry) {
2299
2051
  await this.withPackage((packageElement) => {
2300
- const metadataElement = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
2052
+ const metadataElement = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
2301
2053
  if (!metadataElement)
2302
2054
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
2303
2055
  const oldEntryIndex = this.findMetadataIndex(packageElement, predicate);
2304
- const newElement = Epub.createXmlElement(entry.type, {
2056
+ const newElement = x(entry.type, {
2305
2057
  ...(entry.id && { id: entry.id }),
2306
2058
  ...entry.properties,
2307
- }, entry.value !== undefined ? [Epub.createXmlTextNode(entry.value)] : []);
2308
- if (oldEntryIndex === -1) {
2309
- metadataElement.metadata.push(newElement);
2059
+ }, entry.value);
2060
+ const existing = oldEntryIndex === -1
2061
+ ? null
2062
+ : (Array.from(metadataElement.children)[oldEntryIndex] ?? null);
2063
+ if (existing) {
2064
+ replace(metadataElement, newElement, existing);
2310
2065
  }
2311
2066
  else {
2312
- metadataElement.metadata.splice(oldEntryIndex, 1, newElement);
2067
+ insert(metadataElement, newElement);
2313
2068
  }
2314
2069
  });
2315
2070
  }
@@ -2327,19 +2082,14 @@ export class Epub {
2327
2082
  */
2328
2083
  async removeMetadata(predicate) {
2329
2084
  await this.withPackage((packageElement) => {
2330
- const metadataElement = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
2085
+ const metadataElement = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
2331
2086
  if (!metadataElement) {
2332
2087
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
2333
2088
  }
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];
2089
+ for (const meta of Array.from(metadataElement.children)) {
2338
2090
  const item = Epub.parseMetadataItem(meta);
2339
- if (!item)
2340
- continue;
2341
2091
  if (predicate(item)) {
2342
- metadataEntries.splice(i, 1);
2092
+ metadataElement.removeChild(meta);
2343
2093
  }
2344
2094
  }
2345
2095
  });
@@ -2349,7 +2099,7 @@ export class Epub {
2349
2099
  */
2350
2100
  async getVersion() {
2351
2101
  const packageElement = await this.getPackageElement();
2352
- return packageElement[":@"]?.["@_version"] ?? "2.0";
2102
+ return packageElement.getAttribute("version") ?? "2.0";
2353
2103
  }
2354
2104
  /**
2355
2105
  * Parse the NCX table of contents, if one exists, and return
@@ -2364,49 +2114,42 @@ export class Epub {
2364
2114
  this.getManifest(),
2365
2115
  this.getPackageElement(),
2366
2116
  ]);
2367
- const spine = Epub.findXmlChildByName("spine", Epub.getXmlChildren(packageElement));
2368
- const spineTocId = spine?.[":@"]?.["@_toc"];
2117
+ const spine = findXmlChildByNameNS(NS.opf, "spine", packageElement);
2118
+ const spineTocId = spine?.getAttribute("toc");
2369
2119
  const ncxItem = spineTocId
2370
2120
  ? manifest[spineTocId]
2371
2121
  : Object.values(manifest).find((item) => MediaType.fromMime(item.mediaType ?? "") === MediaType.NCX);
2372
2122
  if (!ncxItem)
2373
2123
  return [];
2374
2124
  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)
2125
+ const ncxElement = parse(ncxContent, "text/xml").documentElement;
2126
+ if (!ncxElement || ncxElement.localName !== "ncx")
2378
2127
  return [];
2379
- const ncxChildren = Epub.getXmlChildren(ncxElement);
2380
- const navMap = Epub.findXmlChildByName("navMap", ncxChildren) ??
2381
- Epub.findXmlChildByName("navmap", ncxChildren);
2128
+ const navMap = findXmlChildByName("navMap", ncxElement) ??
2129
+ findXmlChildByName("navmap", ncxElement);
2382
2130
  if (!navMap)
2383
2131
  return [];
2384
- return this.parseNavPoints(Epub.getXmlChildren(navMap), ncxItem.href);
2132
+ return this.parseNavPoints(navMap, ncxItem.href);
2385
2133
  }
2386
- async parseNavPoints(nodes, ncxHref) {
2134
+ async parseNavPoints(parent, ncxHref) {
2387
2135
  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)
2136
+ for (const node of Array.from(parent.children)) {
2137
+ if (node.nodeName !== "navPoint" && node.nodeName !== "navpoint") {
2394
2138
  continue;
2395
- const children = Epub.getXmlChildren(node);
2396
- const navLabel = Epub.findXmlChildByName("navLabel", children) ??
2397
- Epub.findXmlChildByName("navlabel", children);
2139
+ }
2140
+ const navLabel = findXmlChildByName("navLabel", node) ??
2141
+ findXmlChildByName("navlabel", node);
2398
2142
  let title = null;
2399
2143
  if (navLabel) {
2400
- const textEl = Epub.findXmlChildByName("text", Epub.getXmlChildren(navLabel));
2144
+ const textEl = findXmlChildByName("text", navLabel);
2401
2145
  if (textEl) {
2402
- title =
2403
- Epub.getXhtmlTextContent(Epub.getXmlChildren(textEl)).trim() || null;
2146
+ title = textEl.textContent?.trim() || null;
2404
2147
  }
2405
2148
  }
2406
- const contentEl = Epub.findXmlChildByName("content", children);
2407
- const src = contentEl?.[":@"]?.["@_src"];
2149
+ const contentEl = findXmlChildByName("content", node);
2150
+ const src = contentEl?.getAttribute("src");
2408
2151
  const href = src ? await this.resolveHref(src, ncxHref) : null;
2409
- const childEntries = await this.parseNavPoints(children, ncxHref);
2152
+ const childEntries = await this.parseNavPoints(node, ncxHref);
2410
2153
  entries.push({
2411
2154
  title: title ?? `${entries.length}`,
2412
2155
  ...(href && { href }),
@@ -2423,15 +2166,15 @@ export class Epub {
2423
2166
  */
2424
2167
  async getGuideEntries() {
2425
2168
  const packageElement = await this.getPackageElement();
2426
- const guide = Epub.findXmlChildByName("guide", Epub.getXmlChildren(packageElement));
2169
+ const guide = findXmlChildByNameNS(NS.opf, "guide", packageElement);
2427
2170
  if (!guide)
2428
2171
  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(),
2172
+ return Array.from(guide.children)
2173
+ .filter((reference) => reference.nodeName === "reference")
2174
+ .map((reference) => ({
2175
+ href: reference.getAttribute("href") ?? "",
2176
+ title: reference.getAttribute("title") ?? "",
2177
+ type: (reference.getAttribute("type") ?? "").toLowerCase(),
2435
2178
  }))
2436
2179
  .filter((entry) => entry.href);
2437
2180
  }
@@ -2542,27 +2285,14 @@ export class EpubFactory {
2542
2285
  // unreachable: a writable adapter must implement write
2543
2286
  throw new Error(`adapter ${this.adapterClass.kind} declared writable but did not implement write()`);
2544
2287
  }
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);
2288
+ const textInfo = typeof language.getTextInfo === "function"
2289
+ ? language.getTextInfo()
2290
+ : language.textInfo;
2565
2291
  const epub = new Epub(this.adapterClass, adapter, path);
2292
+ 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" }) }) }));
2293
+ await adapter.write("META-INF/container.xml", serialize(container), "utf-8");
2294
+ 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", {})] }));
2295
+ await adapter.write("OEBPS/content.opf", serialize(packageDocument), "utf-8");
2566
2296
  const metadata = [
2567
2297
  {
2568
2298
  id: "pub-id",
@@ -2586,7 +2316,7 @@ export class EpubFactory {
2586
2316
  await Promise.all(creators.map((creator) => epub.addCreator(creator)));
2587
2317
  }
2588
2318
  if (contributors) {
2589
- await Promise.all(contributors.map((contributor) => epub.addCreator(contributor)));
2319
+ await Promise.all(contributors.map((contributor) => epub.addContributor(contributor)));
2590
2320
  }
2591
2321
  return epub; // oxlint-disable-line typescript/no-unnecessary-type-assertion
2592
2322
  }
@@ -2614,10 +2344,8 @@ export class EpubFactory {
2614
2344
  throw new EpubReadOnlyError(`adapter ${this.adapterClass.kind} is read-only; cannot upgrade`);
2615
2345
  }
2616
2346
  const { removeNcx = false, outputPath } = options;
2617
- if (outputPath) {
2618
- await mkdir(dirname(outputPath), { recursive: true });
2619
- await cp(path, outputPath, { force: true });
2620
- }
2347
+ if (outputPath)
2348
+ await copyFile(path, outputPath);
2621
2349
  const source = outputPath ?? path;
2622
2350
  const adapter = await this.adapterClass.init(source, // oxlint-disable-line typescript/no-unnecessary-type-assertion
2623
2351
  options);
@@ -2662,14 +2390,14 @@ export class EpubFactory {
2662
2390
  mediaType: MediaType.XHTML.mime,
2663
2391
  properties: ["nav"],
2664
2392
  }, 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>
2393
+ // XHTML 1.x doctypes are not valid HTML5, so rewrite them to
2394
+ // <!DOCTYPE html> as part of the upgrade.
2668
2395
  const manifest = await epub.getManifest();
2669
2396
  for (const item of Object.values(manifest)) {
2670
2397
  if (MediaType.fromMime(item.mediaType ?? "") !== MediaType.XHTML)
2671
2398
  continue;
2672
2399
  const contents = await epub.readXhtmlItemContents(item.id);
2400
+ Upgrade.normalizeDoctype(contents);
2673
2401
  await epub.writeXhtmlItemContents(item.id, contents);
2674
2402
  }
2675
2403
  // oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- tsgolint treats the cast as unnecessary but it still narrows the type