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