@storyteller-platform/epub 0.8.1 → 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,32 +239,37 @@ 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);
441
254
  return this.rootfile;
442
255
  }
443
- async getPackageDocument() {
256
+ async getPackageDocumentSource() {
444
257
  const rootfile = await this.getRootfile();
445
258
  const packageDocumentString = await this.adapter.read(rootfile, "utf-8");
446
259
  if (!packageDocumentString)
447
260
  throw new Error(`Failed to parse EPUB: could not find package document at ${rootfile}`);
448
- const packageDocument = Epub.xmlParser.parse(packageDocumentString);
449
- return normalizeOpfNamespacePrefixes(packageDocument);
261
+ return packageDocumentString;
262
+ }
263
+ async getPackageDocument() {
264
+ const packageDocument = parse(await this.getPackageDocumentSource(), "text/xml");
265
+ ensureOpfNamespace(packageDocument);
266
+ return packageDocument;
450
267
  }
451
268
  async getPackageElement() {
452
269
  const packageDocument = await this.getPackageDocument();
453
- const packageElement = Epub.findXmlChildByName("package", packageDocument);
270
+ const packageElement = packageDocument.documentElement?.localName === "package"
271
+ ? packageDocument.documentElement
272
+ : null;
454
273
  if (!packageElement) {
455
274
  throw new Error("Failed to parse EPUB: Found no package element in package document");
456
275
  }
@@ -472,12 +291,14 @@ export class Epub {
472
291
  this.assertWritable();
473
292
  await this.packageMutex.runExclusive(async () => {
474
293
  const packageDocument = await this.getPackageDocument();
475
- const packageElement = Epub.findXmlChildByName("package", packageDocument);
294
+ const packageElement = packageDocument.documentElement?.localName === "package"
295
+ ? packageDocument.documentElement
296
+ : null;
476
297
  if (!packageElement) {
477
298
  throw new Error("Failed to parse EPUB: Found no package element in package document");
478
299
  }
479
- const produced = (await producer(packageElement));
480
- const updatedPackageDocument = Epub.xmlBuilder.build(produced ?? packageDocument);
300
+ const produced = await producer(packageElement);
301
+ const updatedPackageDocument = serialize(produced ?? packageDocument);
481
302
  const rootfile = await this.getRootfile();
482
303
  await this.writeEntryContents(rootfile, updatedPackageDocument, "utf-8");
483
304
  });
@@ -494,24 +315,25 @@ export class Epub {
494
315
  if (this.manifest !== null)
495
316
  return this.manifest;
496
317
  const packageElement = await this.getPackageElement();
497
- const manifest = Epub.findXmlChildByName("manifest", Epub.getXmlChildren(packageElement));
498
- if (!manifest)
318
+ const manifest = findXmlChildByNameNS(NS.opf, "manifest", packageElement);
319
+ if (!manifest) {
499
320
  throw new Error("Failed to parse EPUB: Found no manifest element in package document");
500
- this.manifest = Epub.getXmlChildren(manifest).reduce((acc, item) => {
501
- if (Epub.isXmlTextNode(item))
502
- return acc;
503
- 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) {
504
326
  return acc;
505
327
  }
506
328
  return {
507
329
  ...acc,
508
- [item[":@"]["@_id"]]: {
509
- id: item[":@"]["@_id"],
510
- href: item[":@"]["@_href"],
511
- mediaType: item[":@"]["@_media-type"],
512
- mediaOverlay: item[":@"]["@_media-overlay"],
513
- fallback: item[":@"]["@_fallback"],
514
- 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,
515
337
  },
516
338
  };
517
339
  }, {});
@@ -528,13 +350,11 @@ export class Epub {
528
350
  * array, this method is only meant to be used internally.
529
351
  */
530
352
  findMetadataIndex(packageElement, predicate) {
531
- const metadataElement = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
353
+ const metadataElement = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
532
354
  if (!metadataElement)
533
355
  throw new Error("Failed to parse EPUB: Found no metadata element in package document");
534
- return metadataElement.metadata.findIndex((node) => {
356
+ return Array.from(metadataElement.children).findIndex((node) => {
535
357
  const item = Epub.parseMetadataItem(node);
536
- if (!item)
537
- return false;
538
358
  return predicate(item);
539
359
  });
540
360
  }
@@ -552,36 +372,26 @@ export class Epub {
552
372
  */
553
373
  async findAllMetadataItems(predicate) {
554
374
  const packageElement = await this.getPackageElement();
555
- const metadataElement = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
375
+ const metadataElement = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
556
376
  if (!metadataElement)
557
377
  throw new Error("Failed to parse EPUB: Found no metadata element in package document");
558
- const elements = metadataElement.metadata.filter((node) => {
378
+ const elements = metadataElement.children.filter((node) => {
559
379
  const item = Epub.parseMetadataItem(node);
560
- if (!item)
561
- return false;
562
380
  return predicate(item);
563
381
  });
564
- return elements
565
- .map((element) => Epub.parseMetadataItem(element))
566
- .filter((item) => !!item);
382
+ return elements.map((element) => Epub.parseMetadataItem(element));
567
383
  }
568
- static parseMetadataItem(node) {
569
- if (Epub.isXmlTextNode(node))
570
- return null;
571
- const elementName = Epub.getXmlElementName(node);
572
- const textNode = Epub.getXmlChildren(node)[0];
384
+ static parseMetadataItem(element) {
385
+ const elementName = element.nodeName;
386
+ const textNode = element.firstChild;
573
387
  // https://www.w3.org/TR/epub-33/#sec-metadata-values
574
388
  // Whitespace within these element values is not significant.
575
389
  // Sequences of one or more whitespace characters are collapsed
576
390
  // to a single space [infra] during processing .
577
- const value = !textNode || !Epub.isXmlTextNode(textNode)
391
+ const value = !textNode || !isText(textNode)
578
392
  ? undefined
579
- : textNode["#text"].replaceAll(/\s+/g, " ");
580
- const attributes = node[":@"] ?? {};
581
- const { id, ...properties } = Object.fromEntries(Object.entries(attributes).map(([attrName, value]) => [
582
- attrName.slice(2),
583
- value,
584
- ]));
393
+ : textNode.data.replaceAll(/\s+/g, " ").trim();
394
+ const { id, ...properties } = Object.fromEntries(Array.from(element.attributes).map((attr) => [attr.name, attr.value]));
585
395
  return {
586
396
  id,
587
397
  type: elementName,
@@ -602,12 +412,10 @@ export class Epub {
602
412
  */
603
413
  async getMetadata() {
604
414
  const packageElement = await this.getPackageElement();
605
- const metadataElement = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
415
+ const metadataElement = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
606
416
  if (!metadataElement)
607
417
  throw new Error("Failed to parse EPUB: Found no metadata element in package document");
608
- const metadata = metadataElement.metadata
609
- .map((node) => Epub.parseMetadataItem(node))
610
- .filter((node) => !!node);
418
+ const metadata = Array.from(metadataElement.children).map((node) => Epub.parseMetadataItem(node));
611
419
  return metadata;
612
420
  }
613
421
  /**
@@ -672,7 +480,7 @@ export class Epub {
672
480
  */
673
481
  async setUniqueIdentifier(identifier) {
674
482
  await this.withPackage(async (packageElement) => {
675
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
483
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
676
484
  if (!metadata) {
677
485
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
678
486
  }
@@ -680,23 +488,15 @@ export class Epub {
680
488
  if (!uniqueId) {
681
489
  // Create a new unique identifier id
682
490
  const newUniqueId = nanoid();
683
- packageElement[":@"] = {
684
- ...packageElement[":@"],
685
- "@_unique-identifier": newUniqueId,
686
- };
491
+ packageElement.setAttribute("unique-identifier", newUniqueId);
687
492
  uniqueId = newUniqueId;
688
493
  }
689
- const children = Epub.getXmlChildren(metadata);
690
- const entry = Epub.findXmlChildByName("dc:identifier", children, (node) => node[":@"]?.["@_id"] === uniqueId);
494
+ const entry = findXmlChildByNameNS(NS.dc, "identifier", metadata, (node) => node.getAttribute("id") === uniqueId);
691
495
  if (entry) {
692
- children.splice(children.indexOf(entry), 1, Epub.createXmlElement("dc:identifier", { id: uniqueId }, [
693
- Epub.createXmlTextNode(identifier),
694
- ]));
496
+ replace(metadata, _jsx("dc:identifier", { id: uniqueId, children: identifier }), entry);
695
497
  return;
696
498
  }
697
- children.push(Epub.createXmlElement("dc:identifier", { id: uniqueId }, [
698
- Epub.createXmlTextNode(identifier),
699
- ]));
499
+ insert(metadata, _jsx("dc:identifier", { id: uniqueId, children: identifier }));
700
500
  });
701
501
  }
702
502
  /**
@@ -707,7 +507,7 @@ export class Epub {
707
507
  */
708
508
  async getUniqueIdentifierId() {
709
509
  const packageElement = await this.getPackageElement();
710
- return packageElement[":@"]?.["@_unique-identifier"] ?? null;
510
+ return packageElement.getAttribute("unique-identifier") ?? null;
711
511
  }
712
512
  /**
713
513
  * Collect `dc:identifier` or `dc:source` entries, attaching the value and
@@ -852,47 +652,37 @@ export class Epub {
852
652
  async setIdentifiers(identifiers) {
853
653
  const uniqueId = await this.getUniqueIdentifierId();
854
654
  await this.withPackage((packageElement) => {
855
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
856
- if (!metadata)
655
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
656
+ if (!metadata) {
857
657
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
858
- const children = Epub.getXmlChildren(metadata);
658
+ }
859
659
  const removedIds = new Set();
860
- for (let i = children.length - 1; i >= 0; i--) {
861
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
862
- const node = children[i];
863
- if (Epub.isXmlTextNode(node) ||
864
- Epub.getXmlElementName(node) !== "dc:identifier") {
660
+ for (const child of Array.from(metadata.children)) {
661
+ if (child.nodeName !== "dc:identifier") {
865
662
  continue;
866
663
  }
867
- const id = node[":@"]?.["@_id"];
664
+ const id = child.getAttribute("id");
868
665
  if (id && id === uniqueId) {
869
666
  continue;
870
667
  }
871
668
  if (id) {
872
669
  removedIds.add(id);
873
670
  }
874
- children.splice(i, 1);
671
+ metadata.removeChild(child);
875
672
  }
876
- Epub.removeRefiningMetas(children, removedIds, ["identifier-type"]);
673
+ Epub.removeRefiningMetas(metadata, removedIds, ["identifier-type"]);
877
674
  for (const identifier of identifiers) {
878
675
  // we don't allow you to set the unique identifier, you need to use setUniqueIdentifier
879
676
  if (identifier.id && identifier.id === uniqueId)
880
677
  continue;
881
678
  const id = identifier.id ??
882
679
  (identifier.identifierType !== undefined ? nanoid() : undefined);
883
- children.push(Epub.createXmlElement("dc:identifier", {
884
- ...(id && { id }),
885
- ...(identifier.scheme &&
886
- identifier.identifierType === undefined && {
680
+ insert(metadata, _jsx("dc:identifier", { ...(id && { id }), ...(identifier.scheme &&
681
+ identifier.id === undefined && {
887
682
  "opf:scheme": identifier.scheme,
888
- }),
889
- }, [Epub.createXmlTextNode(identifier.value)]));
683
+ }), children: identifier.value }));
890
684
  if (identifier.identifierType !== undefined && id) {
891
- children.push(Epub.createXmlElement("meta", {
892
- refines: `#${id}`,
893
- property: "identifier-type",
894
- ...(identifier.scheme && { scheme: identifier.scheme }),
895
- }, [Epub.createXmlTextNode(identifier.identifierType)]));
685
+ insert(metadata, _jsx("meta", { refines: `#${id}`, property: "identifier-type", ...(identifier.scheme && { scheme: identifier.scheme }), children: identifier.identifierType }));
896
686
  }
897
687
  }
898
688
  });
@@ -900,21 +690,19 @@ export class Epub {
900
690
  /**
901
691
  * Remove `meta` refinements pointing at any of the given ids,
902
692
  * restricted to the given `property` values as a cleanup step
903
- * Mutates `children` in place.
693
+ * Mutates `metadata` in place.
904
694
  */
905
- static removeRefiningMetas(children, ids, properties) {
906
- for (let i = children.length - 1; i >= 0; i--) {
907
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
908
- const node = children[i];
909
- 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") {
910
698
  continue;
911
699
  }
912
- const property = node[":@"]?.["@_property"];
913
- if (!property || !properties.includes(property))
700
+ const property = child.getAttribute("property");
701
+ if (properties && (!property || !properties.includes(property)))
914
702
  continue;
915
- const refines = node[":@"]?.["@_refines"]?.slice(1);
703
+ const refines = child.getAttribute("refines")?.slice(1);
916
704
  if (refines && ids.has(refines)) {
917
- children.splice(i, 1);
705
+ metadata.removeChild(child);
918
706
  }
919
707
  }
920
708
  }
@@ -934,47 +722,37 @@ export class Epub {
934
722
  */
935
723
  async setSources(sources) {
936
724
  await this.withPackage((packageElement) => {
937
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
938
- if (!metadata)
725
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
726
+ if (!metadata) {
939
727
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
940
- const children = Epub.getXmlChildren(metadata);
728
+ }
941
729
  const removedIds = new Set();
942
- for (let i = children.length - 1; i >= 0; i--) {
943
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
944
- const node = children[i];
945
- if (Epub.isXmlTextNode(node) ||
946
- Epub.getXmlElementName(node) !== "dc:source") {
730
+ for (const child of Array.from(metadata.children)) {
731
+ if (child.nodeName !== "dc:source") {
947
732
  continue;
948
733
  }
949
- const id = node[":@"]?.["@_id"];
734
+ const id = child.getAttribute("id");
950
735
  if (id) {
951
736
  removedIds.add(id);
952
737
  }
953
- children.splice(i, 1);
738
+ metadata.removeChild(child);
954
739
  }
955
- Epub.removeRefiningMetas(children, removedIds, [
740
+ Epub.removeRefiningMetas(metadata, removedIds, [
956
741
  "identifier-type",
957
742
  "source-of",
958
743
  ]);
959
744
  for (const source of sources) {
960
745
  const needsId = source.identifierType !== undefined || source.isPageBreakSource;
961
746
  const id = source.id ?? (needsId ? nanoid() : undefined);
962
- children.push(Epub.createXmlElement("dc:source", {
963
- ...(id && { id }),
964
- ...(source.scheme &&
747
+ insert(metadata, _jsx("dc:source", { ...(id && { id }), ...(source.scheme &&
965
748
  source.identifierType === undefined && {
966
749
  "opf:scheme": source.scheme,
967
- }),
968
- }, [Epub.createXmlTextNode(source.value)]));
750
+ }), children: source.value }));
969
751
  if (source.identifierType !== undefined && id) {
970
- children.push(Epub.createXmlElement("meta", {
971
- refines: `#${id}`,
972
- property: "identifier-type",
973
- ...(source.scheme && { scheme: source.scheme }),
974
- }, [Epub.createXmlTextNode(source.identifierType)]));
752
+ insert(metadata, _jsx("meta", { refines: `#${id}`, property: "identifier-type", ...(source.scheme && { scheme: source.scheme }), children: source.identifierType }));
975
753
  }
976
754
  if (source.isPageBreakSource && id) {
977
- 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" }));
978
756
  }
979
757
  }
980
758
  });
@@ -988,11 +766,11 @@ export class Epub {
988
766
  */
989
767
  async getEpub2CoverImage() {
990
768
  const packageElement = await this.getPackageElement();
991
- const metadataElement = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
769
+ const metadataElement = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
992
770
  if (!metadataElement)
993
771
  throw new Error("Failed to parse EPUB: Found no metadata element in package document");
994
- const coverImageElement = Epub.getXmlChildren(metadataElement).find((node) => !Epub.isXmlTextNode(node) && node[":@"]?.["@_name"] === "cover");
995
- const manifestItemId = coverImageElement?.[":@"]?.["@_content"];
772
+ const coverImageElement = Array.from(metadataElement.children).find((element) => element.getAttribute("name") === "cover");
773
+ const manifestItemId = coverImageElement?.getAttribute("content");
996
774
  if (!manifestItemId)
997
775
  return null;
998
776
  const manifest = await this.getManifest();
@@ -1118,7 +896,7 @@ export class Epub {
1118
896
  */
1119
897
  async getBaseDirection() {
1120
898
  const packageEl = await this.getPackageElement();
1121
- const dir = packageEl[":@"]?.["@_dir"];
899
+ const dir = packageEl.getAttribute("dir");
1122
900
  if (dir !== "ltr" && dir !== "rtl" && dir !== "auto") {
1123
901
  return "auto";
1124
902
  }
@@ -1195,24 +973,14 @@ export class Epub {
1195
973
  */
1196
974
  async removeSubject(index) {
1197
975
  await this.withPackage((packageElement) => {
1198
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
976
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
1199
977
  if (!metadata)
1200
978
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
1201
- let subjectCount = null;
1202
- let metadataIndex = null;
1203
- for (const meta of Epub.getXmlChildren(metadata)) {
1204
- if (subjectCount === index)
1205
- break;
1206
- metadataIndex = metadataIndex === null ? 0 : metadataIndex + 1;
1207
- if (Epub.isXmlTextNode(meta))
1208
- continue;
1209
- if (Epub.getXmlElementName(meta) !== "dc:subject")
1210
- continue;
1211
- subjectCount = subjectCount === null ? 0 : subjectCount + 1;
1212
- }
1213
- if (subjectCount === null || metadataIndex === null)
979
+ const subjects = metadata.children.filter((child) => child.nodeName === "dc:subject");
980
+ const subject = subjects[index];
981
+ if (!subject)
1214
982
  return;
1215
- Epub.getXmlChildren(metadata).splice(metadataIndex, 1);
983
+ metadata.removeChild(subject);
1216
984
  });
1217
985
  }
1218
986
  /**
@@ -1409,7 +1177,7 @@ export class Epub {
1409
1177
  */
1410
1178
  async getPackageVocabularyPrefixes() {
1411
1179
  const packageElement = await this.getPackageElement();
1412
- const prefixValue = packageElement[":@"]?.["@_prefix"];
1180
+ const prefixValue = packageElement.getAttribute("prefix");
1413
1181
  if (!prefixValue)
1414
1182
  return {};
1415
1183
  const matches = prefixValue.matchAll(/(?:([a-z]+): +(\S+)\s*)/gs);
@@ -1424,10 +1192,9 @@ export class Epub {
1424
1192
  await this.withPackage(async (packageElement) => {
1425
1193
  const prefixes = await this.getPackageVocabularyPrefixes();
1426
1194
  prefixes[prefix] = uri;
1427
- packageElement[":@"] ??= {};
1428
- packageElement[":@"]["@_prefix"] = Object.entries(prefixes)
1195
+ packageElement.setAttribute("prefix", Object.entries(prefixes)
1429
1196
  .map(([p, u]) => `${p}: ${u}`)
1430
- .join("\n ");
1197
+ .join("\n "));
1431
1198
  });
1432
1199
  }
1433
1200
  /**
@@ -1444,49 +1211,44 @@ export class Epub {
1444
1211
  // rather than a single string, to support expanded titles.
1445
1212
  async setTitle(title) {
1446
1213
  await this.withPackage((packageElement) => {
1447
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
1448
- if (!metadata)
1214
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
1215
+ if (!metadata) {
1449
1216
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
1450
- const titleElement = Epub.findXmlChildByName("dc:title", metadata.metadata);
1217
+ }
1218
+ const titleElement = findXmlChildByNameNS(NS.dc, "title", metadata);
1451
1219
  if (!titleElement) {
1452
- Epub.getXmlChildren(metadata).push(Epub.createXmlElement("dc:title", {}, [
1453
- Epub.createXmlTextNode(title),
1454
- ]));
1220
+ insert(metadata, _jsx("dc:title", { children: title }));
1455
1221
  }
1456
1222
  else {
1457
- titleElement["dc:title"] = [Epub.createXmlTextNode(title)];
1223
+ while (titleElement.firstChild) {
1224
+ titleElement.removeChild(titleElement.firstChild);
1225
+ }
1226
+ insert(titleElement, text(title));
1458
1227
  }
1459
1228
  });
1460
1229
  }
1461
1230
  async setTitles(entries) {
1462
1231
  await this.withPackage((packageElement) => {
1463
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
1232
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
1464
1233
  if (!metadata) {
1465
1234
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
1466
1235
  }
1467
- const metadataEntries = Epub.getXmlChildren(metadata);
1468
- for (let i = metadataEntries.length - 1; i >= 0; i--) {
1469
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1470
- const meta = metadataEntries[i];
1471
- if (Epub.isXmlTextNode(meta))
1472
- continue;
1473
- if (Epub.getXmlElementName(meta) === "dc:title" ||
1474
- meta[":@"]?.["@_property"] === "title-type" ||
1475
- meta[":@"]?.["@_property"] === "display-seq") {
1476
- 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);
1477
1241
  }
1478
1242
  }
1479
1243
  for (let i = 0; i < entries.length; i++) {
1480
1244
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1481
1245
  const entry = entries[i];
1482
1246
  const id = nanoid();
1483
- metadataEntries.push(Epub.createXmlElement("dc:title", { id }, [
1484
- Epub.createXmlTextNode(entry.title),
1485
- ]));
1247
+ insert(metadata, _jsx("dc:title", { id: id, children: entry.title }));
1486
1248
  if (entry.type) {
1487
- 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 }));
1488
1250
  }
1489
- 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() }));
1490
1252
  }
1491
1253
  });
1492
1254
  }
@@ -1527,24 +1289,24 @@ export class Epub {
1527
1289
  // We have to manually find the correct insertion point
1528
1290
  // based on the provided index
1529
1291
  await this.withPackage((packageElement) => {
1530
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
1292
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
1531
1293
  if (!metadata)
1532
1294
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
1533
1295
  let collectionCount = 0;
1534
1296
  let metadataIndex = 0;
1535
- for (const meta of Epub.getXmlChildren(metadata)) {
1297
+ for (const meta of metadata.childNodes) {
1536
1298
  if (collectionCount === index)
1537
1299
  break;
1538
1300
  metadataIndex++;
1539
- if (Epub.isXmlTextNode(meta))
1301
+ if (!isElement(meta))
1540
1302
  continue;
1541
- if (Epub.getXmlElementName(meta) !== "meta")
1303
+ if (meta.nodeName !== "meta")
1542
1304
  continue;
1543
- if (meta[":@"]?.["@_property"] !== "belongs-to-collection")
1305
+ if (meta.getAttribute("property") !== "belongs-to-collection")
1544
1306
  continue;
1545
1307
  collectionCount++;
1546
1308
  }
1547
- 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));
1548
1310
  });
1549
1311
  // These can all just go at the end; order is only
1550
1312
  // important for the `dc:creator`/`dc:contributor`
@@ -1575,38 +1337,20 @@ export class Epub {
1575
1337
  */
1576
1338
  async removeCollection(index) {
1577
1339
  await this.withPackage((packageElement) => {
1578
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
1579
- if (!metadata)
1340
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
1341
+ if (!metadata) {
1580
1342
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
1581
- let collectionCount = null;
1582
- let metadataIndex = null;
1583
- for (const meta of Epub.getXmlChildren(metadata)) {
1584
- if (collectionCount === index)
1585
- break;
1586
- metadataIndex = metadataIndex === null ? 0 : metadataIndex + 1;
1587
- if (Epub.isXmlTextNode(meta))
1588
- continue;
1589
- if (Epub.getXmlElementName(meta) !== "meta")
1590
- continue;
1591
- if (meta[":@"]?.["@_property"] !== "belongs-to-collection")
1592
- continue;
1593
- collectionCount = collectionCount === null ? 0 : collectionCount + 1;
1594
1343
  }
1595
- 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)
1596
1349
  return;
1597
- const [removed] = Epub.getXmlChildren(metadata).splice(metadataIndex, 1);
1598
- if (removed && !Epub.isXmlTextNode(removed) && removed[":@"]?.["@_id"]) {
1599
- const id = removed[":@"]["@_id"];
1600
- const newChildren = Epub.getXmlChildren(metadata).filter((node) => {
1601
- if (Epub.isXmlTextNode(node))
1602
- return true;
1603
- if (Epub.getXmlElementName(node) !== "meta")
1604
- return true;
1605
- if (node[":@"]?.["@_refines"] !== `#${id}`)
1606
- return true;
1607
- return false;
1608
- });
1609
- Epub.replaceXmlChildren(metadata, newChildren);
1350
+ const removedId = removed.getAttribute("id");
1351
+ metadata.removeChild(removed);
1352
+ if (removedId) {
1353
+ Epub.removeRefiningMetas(metadata, new Set([removedId]));
1610
1354
  }
1611
1355
  });
1612
1356
  }
@@ -1689,24 +1433,24 @@ export class Epub {
1689
1433
  // We have to manually find the correct insertion point
1690
1434
  // based on the provided index
1691
1435
  await this.withPackage((packageElement) => {
1692
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
1436
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
1693
1437
  if (!metadata)
1694
1438
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
1695
1439
  let creatorCount = 0;
1696
1440
  let metadataIndex = 0;
1697
- for (const meta of Epub.getXmlChildren(metadata)) {
1441
+ for (const meta of metadata.childNodes) {
1698
1442
  if (creatorCount === index)
1699
1443
  break;
1700
1444
  metadataIndex++;
1701
- if (Epub.isXmlTextNode(meta))
1445
+ if (!isElement(meta))
1446
+ continue;
1447
+ if (meta.nodeName !== `dc:${type}`)
1702
1448
  continue;
1703
- if (Epub.getXmlElementName(meta) !== `dc:${type}`)
1449
+ if (meta.getAttribute("property") !== "belongs-to-collection")
1704
1450
  continue;
1705
1451
  creatorCount++;
1706
1452
  }
1707
- Epub.getXmlChildren(metadata).splice(metadataIndex, 0, Epub.createXmlElement(`dc:${type}`, { id: creatorId }, [
1708
- Epub.createXmlTextNode(creator.name),
1709
- ]));
1453
+ insert(metadata, x(`dc:${type}`, { id: creatorId }, creator.name), metadata.childNodes.item(metadataIndex));
1710
1454
  });
1711
1455
  // These can all just go at the end; order is only
1712
1456
  // important for the `dc:creator`/`dc:contributor`
@@ -1753,36 +1497,16 @@ export class Epub {
1753
1497
  */
1754
1498
  async removeCreator(index, type = "creator") {
1755
1499
  await this.withPackage((packageElement) => {
1756
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
1500
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
1757
1501
  if (!metadata)
1758
1502
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
1759
- let creatorCount = null;
1760
- let metadataIndex = null;
1761
- for (const meta of Epub.getXmlChildren(metadata)) {
1762
- if (creatorCount === index)
1763
- break;
1764
- metadataIndex = metadataIndex === null ? 0 : metadataIndex + 1;
1765
- if (Epub.isXmlTextNode(meta))
1766
- continue;
1767
- if (Epub.getXmlElementName(meta) !== `dc:${type}`)
1768
- continue;
1769
- creatorCount = creatorCount === null ? 0 : creatorCount + 1;
1770
- }
1771
- if (creatorCount === null || metadataIndex === null)
1503
+ const toRemove = Array.from(metadata.children).filter((child) => child.nodeName === `dc:${type}`)[index];
1504
+ if (!toRemove)
1772
1505
  return;
1773
- const [removed] = Epub.getXmlChildren(metadata).splice(metadataIndex, 1);
1774
- if (removed && !Epub.isXmlTextNode(removed) && removed[":@"]?.["@_id"]) {
1775
- const id = removed[":@"]["@_id"];
1776
- const newChildren = Epub.getXmlChildren(metadata).filter((node) => {
1777
- if (Epub.isXmlTextNode(node))
1778
- return true;
1779
- if (Epub.getXmlElementName(node) !== "meta")
1780
- return true;
1781
- if (node[":@"]?.["@_refines"] !== `#${id}`)
1782
- return true;
1783
- return false;
1784
- });
1785
- Epub.replaceXmlChildren(metadata, newChildren);
1506
+ const id = toRemove.getAttribute("id");
1507
+ metadata.removeChild(toRemove);
1508
+ if (id) {
1509
+ Epub.removeRefiningMetas(metadata, new Set([id]));
1786
1510
  }
1787
1511
  });
1788
1512
  }
@@ -1819,13 +1543,17 @@ export class Epub {
1819
1543
  if (this.spine !== null)
1820
1544
  return this.spine;
1821
1545
  const packageElement = await this.getPackageElement();
1822
- const spine = Epub.findXmlChildByName("spine", Epub.getXmlChildren(packageElement));
1546
+ const spine = findXmlChildByNameNS(NS.opf, "spine", packageElement);
1823
1547
  if (!spine)
1824
1548
  throw new Error("Failed to parse EPUB: Found no spine element in package document");
1825
- this.spine = spine["spine"]
1826
- .filter((node) => !Epub.isXmlTextNode(node))
1827
- .map((itemref) => itemref[":@"]?.["@_idref"])
1828
- .filter((idref) => !!idref);
1549
+ this.spine = Array.from(spine.children)
1550
+ .map((itemref) => {
1551
+ return {
1552
+ itemRef: itemref.getAttribute("idref"),
1553
+ linear: itemref.getAttribute("linear") !== "no",
1554
+ };
1555
+ })
1556
+ .filter((itemref) => !!itemref.itemRef);
1829
1557
  return this.spine;
1830
1558
  }
1831
1559
  /**
@@ -1839,7 +1567,14 @@ export class Epub {
1839
1567
  async getSpineItems() {
1840
1568
  const spine = await this.getSpine();
1841
1569
  const manifest = await this.getManifest();
1842
- return spine.map((itemref) => manifest[itemref]).filter((entry) => !!entry);
1570
+ const spineItems = [];
1571
+ for (const itemref of spine) {
1572
+ const item = manifest[itemref.itemRef];
1573
+ if (!item)
1574
+ continue;
1575
+ spineItems.push({ ...item, linear: itemref.linear });
1576
+ }
1577
+ return spineItems;
1843
1578
  }
1844
1579
  /**
1845
1580
  * Add an item to the spine of the EPUB.
@@ -1854,21 +1589,24 @@ export class Epub {
1854
1589
  * @link https://www.w3.org/TR/epub-33/#sec-spine-elem
1855
1590
  */
1856
1591
  async addSpineItem(manifestId, index) {
1857
- const item = Epub.createXmlElement("itemref", { idref: manifestId });
1592
+ const item = _jsx("itemref", { idref: manifestId });
1858
1593
  const manifest = await this.getManifest();
1859
1594
  const manifestItem = manifest[manifestId];
1860
1595
  if (!manifestItem)
1861
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
+ }
1862
1603
  await this.withPackage((packageElement) => {
1863
- const spine = Epub.findXmlChildByName("spine", Epub.getXmlChildren(packageElement));
1604
+ const spine = findXmlChildByNameNS(NS.opf, "spine", packageElement);
1864
1605
  if (!spine)
1865
1606
  throw new Error("Failed to parse EPUB: Found no spine element in package document");
1866
- if (index === undefined) {
1867
- Epub.getXmlChildren(spine).push(item);
1868
- }
1869
- else {
1870
- Epub.getXmlChildren(spine).splice(index, 0, item);
1871
- }
1607
+ insert(spine, item, index === undefined
1608
+ ? null
1609
+ : (Array.from(spine.children)[index] ?? null));
1872
1610
  });
1873
1611
  // Reset the spine cache
1874
1612
  this.spine = null;
@@ -1880,41 +1618,42 @@ export class Epub {
1880
1618
  */
1881
1619
  async removeSpineItem(index) {
1882
1620
  await this.withPackage((packageElement) => {
1883
- const spine = Epub.findXmlChildByName("spine", Epub.getXmlChildren(packageElement));
1621
+ const spine = findXmlChildByNameNS(NS.opf, "spine", packageElement);
1884
1622
  if (!spine)
1885
1623
  throw new Error("Failed to parse EPUB: Found no spine element in package document");
1886
- Epub.getXmlChildren(spine).splice(index, 1);
1624
+ const itemref = Array.from(spine.children)[index];
1625
+ if (itemref)
1626
+ spine.removeChild(itemref);
1887
1627
  });
1888
1628
  // Reset the spine cache
1889
1629
  this.spine = null;
1890
1630
  }
1891
1631
  async getNavigationChildren(ol, navHref, { resolveToRoot } = {}) {
1892
1632
  const children = [];
1893
- const childrenElements = Epub.getXmlChildren(ol).filter((node) => !Epub.isXmlTextNode(node) && Epub.getXmlElementName(node) === "li");
1894
- for (const childEl of childrenElements) {
1895
- const [firstChild, secondChild] = Epub.getXmlChildren(childEl).filter((node) => !Epub.isXmlTextNode(node) &&
1896
- ["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));
1897
1636
  if (!firstChild)
1898
1637
  continue;
1899
- if (!["a", "span"].includes(Epub.getXmlElementName(firstChild))) {
1638
+ const name = firstChild.nodeName;
1639
+ if (name !== "a" && name !== "span")
1900
1640
  continue;
1901
- }
1902
- if (Epub.getXmlElementName(firstChild) === "span" &&
1903
- (!secondChild || Epub.getXmlElementName(secondChild) !== "ol")) {
1641
+ if (name === "span" && (!secondChild || secondChild.nodeName !== "ol")) {
1904
1642
  continue;
1905
1643
  }
1644
+ const role = firstChild.getAttribute("epub:type")?.trim();
1645
+ const href = firstChild.getAttribute("href");
1906
1646
  children.push({
1907
- title: Epub.getXhtmlTextContent(Epub.getXmlChildren(firstChild)),
1908
- ...(Epub.getXmlElementName(firstChild) === "a" &&
1909
- firstChild[":@"]?.["@_href"] && {
1910
- href: await this.resolveHref(firstChild[":@"]["@_href"], undefined, { toRoot: resolveToRoot }),
1911
- }),
1912
- ...(Epub.getXmlElementName(firstChild) === "a" &&
1913
- firstChild[":@"]?.["@_epub:type"] && {
1914
- role: await this.resolveHref(firstChild[":@"]["@_epub:type"], undefined, { toRoot: resolveToRoot }),
1647
+ title: firstChild.textContent ?? "",
1648
+ ...(name === "a" &&
1649
+ href && {
1650
+ href: await this.resolveHref(href, navHref, {
1651
+ toRoot: resolveToRoot,
1652
+ }),
1915
1653
  }),
1654
+ ...(role && { role }),
1916
1655
  ...(secondChild &&
1917
- Epub.getXmlElementName(secondChild) === "ol" && {
1656
+ secondChild.nodeName === "ol" && {
1918
1657
  children: await this.getNavigationChildren(secondChild, navHref, {
1919
1658
  resolveToRoot,
1920
1659
  }),
@@ -1929,19 +1668,21 @@ export class Epub {
1929
1668
  if (!navItem)
1930
1669
  return null;
1931
1670
  const navContents = await this.readXhtmlItemContents(navItem.id);
1932
- 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);
1933
1675
  if (!navEl)
1934
1676
  return null;
1935
- const [firstChild, secondChild] = Epub.getXmlChildren(navEl).filter((node) => !!(!Epub.isXmlTextNode(node) &&
1936
- 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));
1937
1678
  if (!firstChild)
1938
1679
  return null;
1939
- const title = Epub.getXmlElementName(firstChild).match(/h[1-6]/)
1940
- ? Epub.getXhtmlTextContent(Epub.getXmlChildren(firstChild))
1680
+ const title = /h[1-6]/.test(firstChild.nodeName)
1681
+ ? (firstChild.textContent ?? "")
1941
1682
  : null;
1942
- const list = Epub.getXmlElementName(firstChild) === "ol"
1683
+ const list = firstChild.nodeName === "ol"
1943
1684
  ? firstChild
1944
- : secondChild && Epub.getXmlElementName(secondChild) === "ol"
1685
+ : secondChild && secondChild.nodeName === "ol"
1945
1686
  ? secondChild
1946
1687
  : null;
1947
1688
  if (!list)
@@ -2086,6 +1827,26 @@ export class Epub {
2086
1827
  : await this.adapter.read(entry);
2087
1828
  return itemEntry;
2088
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
+ }
2089
1850
  /**
2090
1851
  * Create a new XHTML document with the given body
2091
1852
  * and head.
@@ -2096,27 +1857,32 @@ export class Epub {
2096
1857
  */
2097
1858
  async createXhtmlDocument(body, head, language) {
2098
1859
  const lang = language ?? (await this.getLanguage());
2099
- return [
2100
- Epub.createXmlElement("?xml", { version: "1.0", encoding: "UTF-8" }, [
2101
- { "#text": "" },
2102
- ]),
2103
- Epub.createXmlElement("html", {
2104
- xmlns: "http://www.w3.org/1999/xhtml",
2105
- "xmlns:epub": "http://www.idpf.org/2007/ops",
2106
- ...(lang && { "xml:lang": lang.toString(), lang: lang.toString() }),
2107
- }, [
2108
- Epub.createXmlElement("head", {}, head),
2109
- Epub.createXmlElement("body", {}, body),
2110
- ]),
2111
- ];
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;
2112
1879
  }
2113
1880
  async readXhtmlItemContents(id, as = "xhtml") {
2114
1881
  const contents = await this.readItemContents(id, "utf-8");
2115
- const xml = Epub.xhtmlParser.parse(contents);
1882
+ const document = parse(contents, "application/xhtml+xml");
2116
1883
  if (as === "xhtml")
2117
- return xml;
2118
- const body = Epub.getXhtmlBody(xml);
2119
- return Epub.getXhtmlTextContent(body);
1884
+ return document;
1885
+ return getXhtmlBody(document).textContent ?? "";
2120
1886
  }
2121
1887
  async writeEntryContents(path, contents, encoding) {
2122
1888
  this.assertWritable();
@@ -2160,42 +1926,52 @@ export class Epub {
2160
1926
  * @link https://www.w3.org/TR/epub-33/#sec-xhtml
2161
1927
  */
2162
1928
  async writeXhtmlItemContents(id, contents) {
2163
- await this.writeItemContents(id, Epub.xhtmlBuilder.build(contents), "utf-8");
1929
+ await this.writeItemContents(id, serialize(contents), "utf-8");
2164
1930
  }
2165
1931
  async removeManifestItem(id) {
2166
1932
  await this.withPackage(async (packageElement) => {
2167
- const manifest = Epub.findXmlChildByName("manifest", Epub.getXmlChildren(packageElement));
1933
+ const manifest = findXmlChildByNameNS(NS.opf, "manifest", packageElement);
2168
1934
  if (!manifest)
2169
1935
  throw new Error("Failed to parse EPUB: Found no manifest element in package document");
2170
- const itemIndex = Epub.getXmlChildren(manifest).findIndex((node) => !Epub.isXmlTextNode(node) && node[":@"]?.["@_id"] === id);
2171
- if (itemIndex === -1)
1936
+ const item = Array.from(manifest.children).find((element) => element.getAttribute("id") === id);
1937
+ if (!item)
2172
1938
  return;
2173
- const [item] = Epub.getXmlChildren(manifest).splice(itemIndex, 1);
2174
- 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)
2175
1949
  return;
2176
- await this.removeEntry(item[":@"]["@_href"]);
1950
+ await this.removeEntry(href);
2177
1951
  });
2178
- // 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
2179
1953
  // the updated XML next time
2180
1954
  this.manifest = null;
1955
+ this.spine = null;
2181
1956
  }
2182
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
+ }
2183
1970
  await this.withPackage((packageElement) => {
2184
- const manifest = Epub.findXmlChildByName("manifest", Epub.getXmlChildren(packageElement));
1971
+ const manifest = findXmlChildByNameNS(NS.opf, "manifest", packageElement);
2185
1972
  if (!manifest)
2186
1973
  throw new Error("Failed to parse EPUB: Found no manifest element in package document");
2187
- // TODO: Should we ensure that there isn't already a manifest
2188
- // item with this id first?
2189
- Epub.getXmlChildren(manifest).push(Epub.createXmlElement("item", {
2190
- id: item.id,
2191
- href: item.href,
2192
- ...(item.mediaType && { "media-type": item.mediaType }),
2193
- ...(item.fallback && { fallback: item.fallback }),
2194
- ...(item.mediaOverlay && { "media-overlay": item.mediaOverlay }),
2195
- ...(item.properties && {
2196
- properties: item.properties.join(" "),
2197
- }),
2198
- }));
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(" ") }) }));
2199
1975
  });
2200
1976
  // Reset the cached manifest, so that it will be read from
2201
1977
  // the updated XML next time
@@ -2206,7 +1982,7 @@ export class Epub {
2206
1982
  const data = encoding === "utf-8" || encoding === "xml"
2207
1983
  ? new TextEncoder().encode(encoding === "utf-8"
2208
1984
  ? contents
2209
- : Epub.xmlBuilder.build(contents))
1985
+ : serialize(contents))
2210
1986
  : contents;
2211
1987
  /* oxlint-enable typescript/no-unnecessary-type-assertion */
2212
1988
  await this.writeEntryContents(filename, data);
@@ -2221,22 +1997,17 @@ export class Epub {
2221
1997
  */
2222
1998
  async updateManifestItem(id, newItem) {
2223
1999
  await this.withPackage((packageElement) => {
2224
- const manifest = Epub.findXmlChildByName("manifest", Epub.getXmlChildren(packageElement));
2000
+ const manifest = findXmlChildByNameNS(NS.opf, "manifest", packageElement);
2225
2001
  if (!manifest)
2226
2002
  throw new Error("Failed to parse EPUB: Found no manifest element in package document");
2227
- const itemIndex = manifest["manifest"].findIndex((item) => !Epub.isXmlTextNode(item) && item[":@"]?.["@_id"] === id);
2228
- Epub.getXmlChildren(manifest).splice(itemIndex, 1, Epub.createXmlElement("item", {
2229
- id: id,
2230
- href: newItem.href,
2231
- ...(newItem.mediaType && { "media-type": newItem.mediaType }),
2232
- ...(newItem.fallback && { fallback: newItem.fallback }),
2233
- ...(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 && {
2234
2007
  "media-overlay": newItem.mediaOverlay,
2235
- }),
2236
- ...(newItem.properties && {
2008
+ }), ...(newItem.properties && {
2237
2009
  properties: newItem.properties.join(" "),
2238
- }),
2239
- }));
2010
+ }) }), existing);
2240
2011
  });
2241
2012
  // Reset the cached manifest, so that it will be read from
2242
2013
  // the updated XML next time
@@ -2254,15 +2025,13 @@ export class Epub {
2254
2025
  */
2255
2026
  async addMetadata(entry) {
2256
2027
  await this.withPackage((packageElement) => {
2257
- const metadata = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
2028
+ const metadata = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
2258
2029
  if (!metadata)
2259
2030
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
2260
- Epub.getXmlChildren(metadata).push(Epub.createXmlElement(entry.type, {
2031
+ insert(metadata, x(entry.type, {
2261
2032
  ...(entry.id && { id: entry.id }),
2262
2033
  ...entry.properties,
2263
- }, entry.value !== undefined
2264
- ? [Epub.createXmlTextNode(entry.value)]
2265
- : []));
2034
+ }, entry.value));
2266
2035
  });
2267
2036
  }
2268
2037
  /**
@@ -2280,19 +2049,22 @@ export class Epub {
2280
2049
  */
2281
2050
  async replaceMetadata(predicate, entry) {
2282
2051
  await this.withPackage((packageElement) => {
2283
- const metadataElement = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
2052
+ const metadataElement = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
2284
2053
  if (!metadataElement)
2285
2054
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
2286
2055
  const oldEntryIndex = this.findMetadataIndex(packageElement, predicate);
2287
- const newElement = Epub.createXmlElement(entry.type, {
2056
+ const newElement = x(entry.type, {
2288
2057
  ...(entry.id && { id: entry.id }),
2289
2058
  ...entry.properties,
2290
- }, entry.value !== undefined ? [Epub.createXmlTextNode(entry.value)] : []);
2291
- if (oldEntryIndex === -1) {
2292
- 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);
2293
2065
  }
2294
2066
  else {
2295
- metadataElement.metadata.splice(oldEntryIndex, 1, newElement);
2067
+ insert(metadataElement, newElement);
2296
2068
  }
2297
2069
  });
2298
2070
  }
@@ -2310,19 +2082,14 @@ export class Epub {
2310
2082
  */
2311
2083
  async removeMetadata(predicate) {
2312
2084
  await this.withPackage((packageElement) => {
2313
- const metadataElement = Epub.findXmlChildByName("metadata", Epub.getXmlChildren(packageElement));
2085
+ const metadataElement = findXmlChildByNameNS(NS.opf, "metadata", packageElement);
2314
2086
  if (!metadataElement) {
2315
2087
  throw new Error("Failed to parse EPUB: found no metadata element in package document");
2316
2088
  }
2317
- const metadataEntries = Epub.getXmlChildren(metadataElement);
2318
- for (let i = metadataEntries.length - 1; i >= 0; i--) {
2319
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2320
- const meta = metadataEntries[i];
2089
+ for (const meta of Array.from(metadataElement.children)) {
2321
2090
  const item = Epub.parseMetadataItem(meta);
2322
- if (!item)
2323
- continue;
2324
2091
  if (predicate(item)) {
2325
- metadataEntries.splice(i, 1);
2092
+ metadataElement.removeChild(meta);
2326
2093
  }
2327
2094
  }
2328
2095
  });
@@ -2332,7 +2099,7 @@ export class Epub {
2332
2099
  */
2333
2100
  async getVersion() {
2334
2101
  const packageElement = await this.getPackageElement();
2335
- return packageElement[":@"]?.["@_version"] ?? "2.0";
2102
+ return packageElement.getAttribute("version") ?? "2.0";
2336
2103
  }
2337
2104
  /**
2338
2105
  * Parse the NCX table of contents, if one exists, and return
@@ -2347,49 +2114,42 @@ export class Epub {
2347
2114
  this.getManifest(),
2348
2115
  this.getPackageElement(),
2349
2116
  ]);
2350
- const spine = Epub.findXmlChildByName("spine", Epub.getXmlChildren(packageElement));
2351
- const spineTocId = spine?.[":@"]?.["@_toc"];
2117
+ const spine = findXmlChildByNameNS(NS.opf, "spine", packageElement);
2118
+ const spineTocId = spine?.getAttribute("toc");
2352
2119
  const ncxItem = spineTocId
2353
2120
  ? manifest[spineTocId]
2354
2121
  : Object.values(manifest).find((item) => MediaType.fromMime(item.mediaType ?? "") === MediaType.NCX);
2355
2122
  if (!ncxItem)
2356
2123
  return [];
2357
2124
  const ncxContent = await this.readItemContents(ncxItem.id, "utf-8");
2358
- const ncxXml = Epub.xmlParser.parse(ncxContent);
2359
- const ncxElement = Epub.findXmlChildByName("ncx", ncxXml);
2360
- if (!ncxElement)
2125
+ const ncxElement = parse(ncxContent, "text/xml").documentElement;
2126
+ if (!ncxElement || ncxElement.localName !== "ncx")
2361
2127
  return [];
2362
- const ncxChildren = Epub.getXmlChildren(ncxElement);
2363
- const navMap = Epub.findXmlChildByName("navMap", ncxChildren) ??
2364
- Epub.findXmlChildByName("navmap", ncxChildren);
2128
+ const navMap = findXmlChildByName("navMap", ncxElement) ??
2129
+ findXmlChildByName("navmap", ncxElement);
2365
2130
  if (!navMap)
2366
2131
  return [];
2367
- return this.parseNavPoints(Epub.getXmlChildren(navMap), ncxItem.href);
2132
+ return this.parseNavPoints(navMap, ncxItem.href);
2368
2133
  }
2369
- async parseNavPoints(nodes, ncxHref) {
2134
+ async parseNavPoints(parent, ncxHref) {
2370
2135
  const entries = [];
2371
- for (const node of nodes) {
2372
- if (Epub.isXmlTextNode(node))
2373
- continue;
2374
- const name = Epub.getXmlElementName(node);
2375
- const isNavPoint = name === "navPoint" || name === "navpoint";
2376
- if (!isNavPoint)
2136
+ for (const node of Array.from(parent.children)) {
2137
+ if (node.nodeName !== "navPoint" && node.nodeName !== "navpoint") {
2377
2138
  continue;
2378
- const children = Epub.getXmlChildren(node);
2379
- const navLabel = Epub.findXmlChildByName("navLabel", children) ??
2380
- Epub.findXmlChildByName("navlabel", children);
2139
+ }
2140
+ const navLabel = findXmlChildByName("navLabel", node) ??
2141
+ findXmlChildByName("navlabel", node);
2381
2142
  let title = null;
2382
2143
  if (navLabel) {
2383
- const textEl = Epub.findXmlChildByName("text", Epub.getXmlChildren(navLabel));
2144
+ const textEl = findXmlChildByName("text", navLabel);
2384
2145
  if (textEl) {
2385
- title =
2386
- Epub.getXhtmlTextContent(Epub.getXmlChildren(textEl)).trim() || null;
2146
+ title = textEl.textContent?.trim() || null;
2387
2147
  }
2388
2148
  }
2389
- const contentEl = Epub.findXmlChildByName("content", children);
2390
- const src = contentEl?.[":@"]?.["@_src"];
2149
+ const contentEl = findXmlChildByName("content", node);
2150
+ const src = contentEl?.getAttribute("src");
2391
2151
  const href = src ? await this.resolveHref(src, ncxHref) : null;
2392
- const childEntries = await this.parseNavPoints(children, ncxHref);
2152
+ const childEntries = await this.parseNavPoints(node, ncxHref);
2393
2153
  entries.push({
2394
2154
  title: title ?? `${entries.length}`,
2395
2155
  ...(href && { href }),
@@ -2406,15 +2166,15 @@ export class Epub {
2406
2166
  */
2407
2167
  async getGuideEntries() {
2408
2168
  const packageElement = await this.getPackageElement();
2409
- const guide = Epub.findXmlChildByName("guide", Epub.getXmlChildren(packageElement));
2169
+ const guide = findXmlChildByNameNS(NS.opf, "guide", packageElement);
2410
2170
  if (!guide)
2411
2171
  return [];
2412
- return Epub.getXmlChildren(guide)
2413
- .filter((node) => !Epub.isXmlTextNode(node) && "reference" in node)
2414
- .map((ref) => ({
2415
- href: ref[":@"]?.["@_href"] ?? "",
2416
- title: ref[":@"]?.["@_title"] ?? "",
2417
- 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(),
2418
2178
  }))
2419
2179
  .filter((entry) => entry.href);
2420
2180
  }
@@ -2496,6 +2256,16 @@ export class EpubFactory {
2496
2256
  }
2497
2257
  return epub;
2498
2258
  }
2259
+ async readPackageDocumentSource(source, options = {}) {
2260
+ const adapter = await this.adapterClass.init(source, options);
2261
+ const epub = new Epub(this.adapterClass, adapter, typeof source === "string" ? source : undefined, true);
2262
+ try {
2263
+ return await epub.getPackageDocumentSource();
2264
+ }
2265
+ finally {
2266
+ epub.discardAndClose();
2267
+ }
2268
+ }
2499
2269
  /**
2500
2270
  * Construct a new EPUB on this factory's adapter, optionally seeded
2501
2271
  * with the provided metadata. Requires a writable adapter that
@@ -2515,27 +2285,14 @@ export class EpubFactory {
2515
2285
  // unreachable: a writable adapter must implement write
2516
2286
  throw new Error(`adapter ${this.adapterClass.kind} declared writable but did not implement write()`);
2517
2287
  }
2518
- const encoder = new TextEncoder();
2519
- const container = encoder.encode(`<?xml version="1.0"?>
2520
- <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
2521
- <rootfiles>
2522
- <rootfile media-type="${MediaType.OPF.mime}" full-path="OEBPS/content.opf"/>
2523
- </rootfiles>
2524
- </container>
2525
- `);
2526
- await adapter.write("META-INF/container.xml", container);
2527
- const packageDocument = encoder.encode(`<?xml version="1.0"?>
2528
- <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/">
2529
- <metadata>
2530
- </metadata>
2531
- <manifest>
2532
- </manifest>
2533
- <spine>
2534
- </spine>
2535
- </package>
2536
- `);
2537
- await adapter.write("OEBPS/content.opf", packageDocument);
2288
+ const textInfo = typeof language.getTextInfo === "function"
2289
+ ? language.getTextInfo()
2290
+ : language.textInfo;
2538
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");
2539
2296
  const metadata = [
2540
2297
  {
2541
2298
  id: "pub-id",
@@ -2559,7 +2316,7 @@ export class EpubFactory {
2559
2316
  await Promise.all(creators.map((creator) => epub.addCreator(creator)));
2560
2317
  }
2561
2318
  if (contributors) {
2562
- await Promise.all(contributors.map((contributor) => epub.addCreator(contributor)));
2319
+ await Promise.all(contributors.map((contributor) => epub.addContributor(contributor)));
2563
2320
  }
2564
2321
  return epub; // oxlint-disable-line typescript/no-unnecessary-type-assertion
2565
2322
  }
@@ -2587,10 +2344,8 @@ export class EpubFactory {
2587
2344
  throw new EpubReadOnlyError(`adapter ${this.adapterClass.kind} is read-only; cannot upgrade`);
2588
2345
  }
2589
2346
  const { removeNcx = false, outputPath } = options;
2590
- if (outputPath) {
2591
- await mkdir(dirname(outputPath), { recursive: true });
2592
- await cp(path, outputPath, { force: true });
2593
- }
2347
+ if (outputPath)
2348
+ await copyFile(path, outputPath);
2594
2349
  const source = outputPath ?? path;
2595
2350
  const adapter = await this.adapterClass.init(source, // oxlint-disable-line typescript/no-unnecessary-type-assertion
2596
2351
  options);
@@ -2635,14 +2390,14 @@ export class EpubFactory {
2635
2390
  mediaType: MediaType.XHTML.mime,
2636
2391
  properties: ["nav"],
2637
2392
  }, navContent, "utf-8");
2638
- // go over each xhtml item and replace the outdated
2639
- // <!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.1//EN' 'http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd'>
2640
- // with <!DOCTYPE html>
2393
+ // XHTML 1.x doctypes are not valid HTML5, so rewrite them to
2394
+ // <!DOCTYPE html> as part of the upgrade.
2641
2395
  const manifest = await epub.getManifest();
2642
2396
  for (const item of Object.values(manifest)) {
2643
2397
  if (MediaType.fromMime(item.mediaType ?? "") !== MediaType.XHTML)
2644
2398
  continue;
2645
2399
  const contents = await epub.readXhtmlItemContents(item.id);
2400
+ Upgrade.normalizeDoctype(contents);
2646
2401
  await epub.writeXhtmlItemContents(item.id, contents);
2647
2402
  }
2648
2403
  // oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- tsgolint treats the cast as unnecessary but it still narrows the type