@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/README.md +7799 -1018
- package/dist/adapters/interface.d.ts +20 -1
- package/dist/adapters/memory.d.ts +2 -2
- package/dist/adapters/memory.js +16 -30
- package/dist/adapters/tmpfs.browser.d.ts +11 -0
- package/dist/adapters/tmpfs.browser.js +9 -0
- package/dist/adapters/tmpfs.d.ts +2 -2
- package/dist/adapters/tmpfs.js +22 -35
- package/dist/files.browser.d.ts +2 -0
- package/dist/files.browser.js +7 -0
- package/dist/files.node.d.ts +2 -0
- package/dist/files.node.js +10 -0
- package/dist/index.d.ts +42 -119
- package/dist/index.js +371 -643
- package/dist/jsx-dev-runtime.d.ts +9 -0
- package/dist/jsx-dev-runtime.js +9 -0
- package/dist/jsx-runtime.d.ts +42 -0
- package/dist/jsx-runtime.js +69 -0
- package/dist/upgrade.d.ts +26 -17
- package/dist/upgrade.js +117 -157
- package/dist/xml.d.ts +103 -0
- package/dist/xml.js +248 -0
- package/dist/zip.browser.d.ts +2 -0
- package/dist/zip.browser.js +33 -0
- package/dist/zip.node.d.ts +9 -0
- package/dist/zip.node.js +246 -0
- package/package.json +92 -10
- package/dist/epubcheck.d.ts +0 -21
- package/dist/epubcheck.js +0 -34
- package/dist/zip.d.ts +0 -2
- package/dist/zip.js +0 -137
package/dist/index.js
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
9
|
-
import { TmpFsAdapter } from "
|
|
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
|
-
|
|
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
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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 =
|
|
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 =
|
|
429
|
-
const container =
|
|
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 =
|
|
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 =
|
|
436
|
-
|
|
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 =
|
|
452
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
483
|
-
const updatedPackageDocument =
|
|
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 =
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
-
[
|
|
512
|
-
id
|
|
513
|
-
href
|
|
514
|
-
mediaType: item
|
|
515
|
-
mediaOverlay: item
|
|
516
|
-
fallback: item
|
|
517
|
-
properties: item
|
|
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 =
|
|
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.
|
|
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 =
|
|
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.
|
|
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(
|
|
572
|
-
|
|
573
|
-
|
|
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 || !
|
|
391
|
+
const value = !textNode || !isText(textNode)
|
|
581
392
|
? undefined
|
|
582
|
-
: textNode
|
|
583
|
-
const
|
|
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 =
|
|
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.
|
|
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 =
|
|
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
|
|
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
|
-
|
|
696
|
-
Epub.createXmlTextNode(identifier),
|
|
697
|
-
]));
|
|
496
|
+
replace(metadata, _jsx("dc:identifier", { id: uniqueId, children: identifier }), entry);
|
|
698
497
|
return;
|
|
699
498
|
}
|
|
700
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
658
|
+
}
|
|
862
659
|
const removedIds = new Set();
|
|
863
|
-
for (
|
|
864
|
-
|
|
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 =
|
|
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
|
-
|
|
671
|
+
metadata.removeChild(child);
|
|
878
672
|
}
|
|
879
|
-
Epub.removeRefiningMetas(
|
|
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
|
-
|
|
887
|
-
|
|
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
|
-
|
|
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 `
|
|
693
|
+
* Mutates `metadata` in place.
|
|
907
694
|
*/
|
|
908
|
-
static removeRefiningMetas(
|
|
909
|
-
for (
|
|
910
|
-
|
|
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 =
|
|
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 =
|
|
703
|
+
const refines = child.getAttribute("refines")?.slice(1);
|
|
919
704
|
if (refines && ids.has(refines)) {
|
|
920
|
-
|
|
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 =
|
|
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
|
-
|
|
728
|
+
}
|
|
944
729
|
const removedIds = new Set();
|
|
945
|
-
for (
|
|
946
|
-
|
|
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 =
|
|
734
|
+
const id = child.getAttribute("id");
|
|
953
735
|
if (id) {
|
|
954
736
|
removedIds.add(id);
|
|
955
737
|
}
|
|
956
|
-
|
|
738
|
+
metadata.removeChild(child);
|
|
957
739
|
}
|
|
958
|
-
Epub.removeRefiningMetas(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
998
|
-
const manifestItemId = coverImageElement?.
|
|
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
|
|
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 =
|
|
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
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
1217
|
+
}
|
|
1218
|
+
const titleElement = findXmlChildByNameNS(NS.dc, "title", metadata);
|
|
1454
1219
|
if (!titleElement) {
|
|
1455
|
-
|
|
1456
|
-
Epub.createXmlTextNode(title),
|
|
1457
|
-
]));
|
|
1220
|
+
insert(metadata, _jsx("dc:title", { children: title }));
|
|
1458
1221
|
}
|
|
1459
1222
|
else {
|
|
1460
|
-
titleElement
|
|
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 =
|
|
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
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
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
|
-
|
|
1487
|
-
Epub.createXmlTextNode(entry.title),
|
|
1488
|
-
]));
|
|
1247
|
+
insert(metadata, _jsx("dc:title", { id: id, children: entry.title }));
|
|
1489
1248
|
if (entry.type) {
|
|
1490
|
-
|
|
1249
|
+
insert(metadata, _jsx("meta", { refines: `#${id}`, property: "title-type", children: entry.type }));
|
|
1491
1250
|
}
|
|
1492
|
-
|
|
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 =
|
|
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
|
|
1297
|
+
for (const meta of metadata.childNodes) {
|
|
1539
1298
|
if (collectionCount === index)
|
|
1540
1299
|
break;
|
|
1541
1300
|
metadataIndex++;
|
|
1542
|
-
if (
|
|
1301
|
+
if (!isElement(meta))
|
|
1543
1302
|
continue;
|
|
1544
|
-
if (
|
|
1303
|
+
if (meta.nodeName !== "meta")
|
|
1545
1304
|
continue;
|
|
1546
|
-
if (meta
|
|
1305
|
+
if (meta.getAttribute("property") !== "belongs-to-collection")
|
|
1547
1306
|
continue;
|
|
1548
1307
|
collectionCount++;
|
|
1549
1308
|
}
|
|
1550
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
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 =
|
|
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
|
|
1441
|
+
for (const meta of metadata.childNodes) {
|
|
1701
1442
|
if (creatorCount === index)
|
|
1702
1443
|
break;
|
|
1703
1444
|
metadataIndex++;
|
|
1704
|
-
if (
|
|
1445
|
+
if (!isElement(meta))
|
|
1446
|
+
continue;
|
|
1447
|
+
if (meta.nodeName !== `dc:${type}`)
|
|
1705
1448
|
continue;
|
|
1706
|
-
if (
|
|
1449
|
+
if (meta.getAttribute("property") !== "belongs-to-collection")
|
|
1707
1450
|
continue;
|
|
1708
1451
|
creatorCount++;
|
|
1709
1452
|
}
|
|
1710
|
-
|
|
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 =
|
|
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
|
-
|
|
1763
|
-
|
|
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
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
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 =
|
|
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
|
|
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:
|
|
1835
|
-
linear: linear
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1884
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
1911
|
-
for (const childEl of
|
|
1912
|
-
const [firstChild, secondChild] =
|
|
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
|
-
|
|
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
|
|
1644
|
+
const role = firstChild.getAttribute("epub:type")?.trim();
|
|
1645
|
+
const href = firstChild.getAttribute("href");
|
|
1924
1646
|
children.push({
|
|
1925
|
-
title:
|
|
1926
|
-
...(
|
|
1927
|
-
|
|
1928
|
-
href: await this.resolveHref(
|
|
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
|
-
|
|
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
|
|
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] =
|
|
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 =
|
|
1957
|
-
?
|
|
1680
|
+
const title = /h[1-6]/.test(firstChild.nodeName)
|
|
1681
|
+
? (firstChild.textContent ?? "")
|
|
1958
1682
|
: null;
|
|
1959
|
-
const list =
|
|
1683
|
+
const list = firstChild.nodeName === "ol"
|
|
1960
1684
|
? firstChild
|
|
1961
|
-
: secondChild &&
|
|
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
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
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
|
|
1882
|
+
const document = parse(contents, "application/xhtml+xml");
|
|
2133
1883
|
if (as === "xhtml")
|
|
2134
|
-
return
|
|
2135
|
-
|
|
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,
|
|
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 =
|
|
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
|
|
2188
|
-
if (
|
|
1936
|
+
const item = Array.from(manifest.children).find((element) => element.getAttribute("id") === id);
|
|
1937
|
+
if (!item)
|
|
2189
1938
|
return;
|
|
2190
|
-
const
|
|
2191
|
-
|
|
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(
|
|
1950
|
+
await this.removeEntry(href);
|
|
2194
1951
|
});
|
|
2195
|
-
// Reset the cached manifest, so that
|
|
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 =
|
|
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
|
-
|
|
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
|
-
:
|
|
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 =
|
|
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
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
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 =
|
|
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
|
-
|
|
2031
|
+
insert(metadata, x(entry.type, {
|
|
2278
2032
|
...(entry.id && { id: entry.id }),
|
|
2279
2033
|
...entry.properties,
|
|
2280
|
-
}, entry.value
|
|
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 =
|
|
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 =
|
|
2056
|
+
const newElement = x(entry.type, {
|
|
2305
2057
|
...(entry.id && { id: entry.id }),
|
|
2306
2058
|
...entry.properties,
|
|
2307
|
-
}, entry.value
|
|
2308
|
-
|
|
2309
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
2368
|
-
const spineTocId = spine?.
|
|
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
|
|
2376
|
-
|
|
2377
|
-
if (!ncxElement)
|
|
2125
|
+
const ncxElement = parse(ncxContent, "text/xml").documentElement;
|
|
2126
|
+
if (!ncxElement || ncxElement.localName !== "ncx")
|
|
2378
2127
|
return [];
|
|
2379
|
-
const
|
|
2380
|
-
|
|
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(
|
|
2132
|
+
return this.parseNavPoints(navMap, ncxItem.href);
|
|
2385
2133
|
}
|
|
2386
|
-
async parseNavPoints(
|
|
2134
|
+
async parseNavPoints(parent, ncxHref) {
|
|
2387
2135
|
const entries = [];
|
|
2388
|
-
for (const node of
|
|
2389
|
-
if (
|
|
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
|
-
|
|
2396
|
-
const navLabel =
|
|
2397
|
-
|
|
2139
|
+
}
|
|
2140
|
+
const navLabel = findXmlChildByName("navLabel", node) ??
|
|
2141
|
+
findXmlChildByName("navlabel", node);
|
|
2398
2142
|
let title = null;
|
|
2399
2143
|
if (navLabel) {
|
|
2400
|
-
const textEl =
|
|
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 =
|
|
2407
|
-
const src = contentEl?.
|
|
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(
|
|
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 =
|
|
2169
|
+
const guide = findXmlChildByNameNS(NS.opf, "guide", packageElement);
|
|
2427
2170
|
if (!guide)
|
|
2428
2171
|
return [];
|
|
2429
|
-
return
|
|
2430
|
-
.filter((
|
|
2431
|
-
.map((
|
|
2432
|
-
href:
|
|
2433
|
-
title:
|
|
2434
|
-
type: (
|
|
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
|
|
2546
|
-
|
|
2547
|
-
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
2666
|
-
// <!DOCTYPE html
|
|
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
|