@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/README.md +8243 -572
- 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 +48 -120
- package/dist/index.js +401 -646
- 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,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 =
|
|
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);
|
|
441
254
|
return this.rootfile;
|
|
442
255
|
}
|
|
443
|
-
async
|
|
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
|
-
|
|
449
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
480
|
-
const updatedPackageDocument =
|
|
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 =
|
|
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
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
[
|
|
509
|
-
id
|
|
510
|
-
href
|
|
511
|
-
mediaType: item
|
|
512
|
-
mediaOverlay: item
|
|
513
|
-
fallback: item
|
|
514
|
-
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,
|
|
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 =
|
|
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.
|
|
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 =
|
|
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.
|
|
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(
|
|
569
|
-
|
|
570
|
-
|
|
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 || !
|
|
391
|
+
const value = !textNode || !isText(textNode)
|
|
578
392
|
? undefined
|
|
579
|
-
: textNode
|
|
580
|
-
const
|
|
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 =
|
|
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.
|
|
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 =
|
|
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
|
|
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
|
-
|
|
693
|
-
Epub.createXmlTextNode(identifier),
|
|
694
|
-
]));
|
|
496
|
+
replace(metadata, _jsx("dc:identifier", { id: uniqueId, children: identifier }), entry);
|
|
695
497
|
return;
|
|
696
498
|
}
|
|
697
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
658
|
+
}
|
|
859
659
|
const removedIds = new Set();
|
|
860
|
-
for (
|
|
861
|
-
|
|
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 =
|
|
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
|
-
|
|
671
|
+
metadata.removeChild(child);
|
|
875
672
|
}
|
|
876
|
-
Epub.removeRefiningMetas(
|
|
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
|
-
|
|
884
|
-
|
|
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
|
-
|
|
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 `
|
|
693
|
+
* Mutates `metadata` in place.
|
|
904
694
|
*/
|
|
905
|
-
static removeRefiningMetas(
|
|
906
|
-
for (
|
|
907
|
-
|
|
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 =
|
|
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 =
|
|
703
|
+
const refines = child.getAttribute("refines")?.slice(1);
|
|
916
704
|
if (refines && ids.has(refines)) {
|
|
917
|
-
|
|
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 =
|
|
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
|
-
|
|
728
|
+
}
|
|
941
729
|
const removedIds = new Set();
|
|
942
|
-
for (
|
|
943
|
-
|
|
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 =
|
|
734
|
+
const id = child.getAttribute("id");
|
|
950
735
|
if (id) {
|
|
951
736
|
removedIds.add(id);
|
|
952
737
|
}
|
|
953
|
-
|
|
738
|
+
metadata.removeChild(child);
|
|
954
739
|
}
|
|
955
|
-
Epub.removeRefiningMetas(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
995
|
-
const manifestItemId = coverImageElement?.
|
|
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
|
|
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 =
|
|
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
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
1217
|
+
}
|
|
1218
|
+
const titleElement = findXmlChildByNameNS(NS.dc, "title", metadata);
|
|
1451
1219
|
if (!titleElement) {
|
|
1452
|
-
|
|
1453
|
-
Epub.createXmlTextNode(title),
|
|
1454
|
-
]));
|
|
1220
|
+
insert(metadata, _jsx("dc:title", { children: title }));
|
|
1455
1221
|
}
|
|
1456
1222
|
else {
|
|
1457
|
-
titleElement
|
|
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 =
|
|
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
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
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
|
-
|
|
1484
|
-
Epub.createXmlTextNode(entry.title),
|
|
1485
|
-
]));
|
|
1247
|
+
insert(metadata, _jsx("dc:title", { id: id, children: entry.title }));
|
|
1486
1248
|
if (entry.type) {
|
|
1487
|
-
|
|
1249
|
+
insert(metadata, _jsx("meta", { refines: `#${id}`, property: "title-type", children: entry.type }));
|
|
1488
1250
|
}
|
|
1489
|
-
|
|
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 =
|
|
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
|
|
1297
|
+
for (const meta of metadata.childNodes) {
|
|
1536
1298
|
if (collectionCount === index)
|
|
1537
1299
|
break;
|
|
1538
1300
|
metadataIndex++;
|
|
1539
|
-
if (
|
|
1301
|
+
if (!isElement(meta))
|
|
1540
1302
|
continue;
|
|
1541
|
-
if (
|
|
1303
|
+
if (meta.nodeName !== "meta")
|
|
1542
1304
|
continue;
|
|
1543
|
-
if (meta
|
|
1305
|
+
if (meta.getAttribute("property") !== "belongs-to-collection")
|
|
1544
1306
|
continue;
|
|
1545
1307
|
collectionCount++;
|
|
1546
1308
|
}
|
|
1547
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
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 =
|
|
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
|
|
1441
|
+
for (const meta of metadata.childNodes) {
|
|
1698
1442
|
if (creatorCount === index)
|
|
1699
1443
|
break;
|
|
1700
1444
|
metadataIndex++;
|
|
1701
|
-
if (
|
|
1445
|
+
if (!isElement(meta))
|
|
1446
|
+
continue;
|
|
1447
|
+
if (meta.nodeName !== `dc:${type}`)
|
|
1702
1448
|
continue;
|
|
1703
|
-
if (
|
|
1449
|
+
if (meta.getAttribute("property") !== "belongs-to-collection")
|
|
1704
1450
|
continue;
|
|
1705
1451
|
creatorCount++;
|
|
1706
1452
|
}
|
|
1707
|
-
|
|
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 =
|
|
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
|
-
|
|
1760
|
-
|
|
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
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
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 =
|
|
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
|
|
1826
|
-
.
|
|
1827
|
-
|
|
1828
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1867
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
1894
|
-
for (const childEl of
|
|
1895
|
-
const [firstChild, secondChild] =
|
|
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
|
-
|
|
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:
|
|
1908
|
-
...(
|
|
1909
|
-
|
|
1910
|
-
href: await this.resolveHref(
|
|
1911
|
-
|
|
1912
|
-
|
|
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
|
-
|
|
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
|
|
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] =
|
|
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 =
|
|
1940
|
-
?
|
|
1680
|
+
const title = /h[1-6]/.test(firstChild.nodeName)
|
|
1681
|
+
? (firstChild.textContent ?? "")
|
|
1941
1682
|
: null;
|
|
1942
|
-
const list =
|
|
1683
|
+
const list = firstChild.nodeName === "ol"
|
|
1943
1684
|
? firstChild
|
|
1944
|
-
: secondChild &&
|
|
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
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
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
|
|
1882
|
+
const document = parse(contents, "application/xhtml+xml");
|
|
2116
1883
|
if (as === "xhtml")
|
|
2117
|
-
return
|
|
2118
|
-
|
|
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,
|
|
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 =
|
|
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
|
|
2171
|
-
if (
|
|
1936
|
+
const item = Array.from(manifest.children).find((element) => element.getAttribute("id") === id);
|
|
1937
|
+
if (!item)
|
|
2172
1938
|
return;
|
|
2173
|
-
const
|
|
2174
|
-
|
|
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(
|
|
1950
|
+
await this.removeEntry(href);
|
|
2177
1951
|
});
|
|
2178
|
-
// Reset the cached manifest, so that
|
|
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 =
|
|
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
|
-
|
|
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
|
-
:
|
|
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 =
|
|
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
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
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 =
|
|
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
|
-
|
|
2031
|
+
insert(metadata, x(entry.type, {
|
|
2261
2032
|
...(entry.id && { id: entry.id }),
|
|
2262
2033
|
...entry.properties,
|
|
2263
|
-
}, entry.value
|
|
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 =
|
|
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 =
|
|
2056
|
+
const newElement = x(entry.type, {
|
|
2288
2057
|
...(entry.id && { id: entry.id }),
|
|
2289
2058
|
...entry.properties,
|
|
2290
|
-
}, entry.value
|
|
2291
|
-
|
|
2292
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
2351
|
-
const spineTocId = spine?.
|
|
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
|
|
2359
|
-
|
|
2360
|
-
if (!ncxElement)
|
|
2125
|
+
const ncxElement = parse(ncxContent, "text/xml").documentElement;
|
|
2126
|
+
if (!ncxElement || ncxElement.localName !== "ncx")
|
|
2361
2127
|
return [];
|
|
2362
|
-
const
|
|
2363
|
-
|
|
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(
|
|
2132
|
+
return this.parseNavPoints(navMap, ncxItem.href);
|
|
2368
2133
|
}
|
|
2369
|
-
async parseNavPoints(
|
|
2134
|
+
async parseNavPoints(parent, ncxHref) {
|
|
2370
2135
|
const entries = [];
|
|
2371
|
-
for (const node of
|
|
2372
|
-
if (
|
|
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
|
-
|
|
2379
|
-
const navLabel =
|
|
2380
|
-
|
|
2139
|
+
}
|
|
2140
|
+
const navLabel = findXmlChildByName("navLabel", node) ??
|
|
2141
|
+
findXmlChildByName("navlabel", node);
|
|
2381
2142
|
let title = null;
|
|
2382
2143
|
if (navLabel) {
|
|
2383
|
-
const textEl =
|
|
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 =
|
|
2390
|
-
const src = contentEl?.
|
|
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(
|
|
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 =
|
|
2169
|
+
const guide = findXmlChildByNameNS(NS.opf, "guide", packageElement);
|
|
2410
2170
|
if (!guide)
|
|
2411
2171
|
return [];
|
|
2412
|
-
return
|
|
2413
|
-
.filter((
|
|
2414
|
-
.map((
|
|
2415
|
-
href:
|
|
2416
|
-
title:
|
|
2417
|
-
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(),
|
|
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
|
|
2519
|
-
|
|
2520
|
-
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
2639
|
-
// <!DOCTYPE html
|
|
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
|