@storyteller-platform/epub 0.7.1 → 0.8.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 +544 -775
- package/dist/adapters/interface.d.ts +6 -8
- package/dist/adapters/interface.js +1 -0
- package/dist/adapters/memory.d.ts +3 -6
- package/dist/adapters/memory.js +69 -64
- package/dist/adapters/tmpfs.d.ts +2 -5
- package/dist/adapters/tmpfs.js +230 -161
- package/dist/epubcheck.d.ts +4 -6
- package/dist/epubcheck.js +24 -26
- package/dist/index.d.ts +1163 -5
- package/dist/index.js +2587 -2739
- package/dist/upgrade.d.ts +23 -1183
- package/dist/upgrade.js +412 -432
- package/dist/zip.d.ts +2 -5
- package/dist/zip.js +113 -100
- package/package.json +18 -31
- package/dist/adapters/interface.cjs +0 -16
- package/dist/adapters/interface.d.cts +0 -75
- package/dist/adapters/memory.cjs +0 -93
- package/dist/adapters/memory.d.cts +0 -36
- package/dist/adapters/tmpfs.cjs +0 -240
- package/dist/adapters/tmpfs.d.cts +0 -31
- package/dist/chunk-BIEQXUOY.js +0 -50
- package/dist/epubcheck.cjs +0 -60
- package/dist/epubcheck.d.cts +0 -23
- package/dist/index.cjs +0 -2846
- package/dist/index.d.cts +0 -5
- package/dist/upgrade.cjs +0 -523
- package/dist/upgrade.d.cts +0 -1196
- package/dist/zip.cjs +0 -142
- package/dist/zip.d.cts +0 -5
package/dist/index.cjs
DELETED
|
@@ -1,2846 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __export = (target, all) => {
|
|
9
|
-
for (var name in all)
|
|
10
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
-
};
|
|
12
|
-
var __copyProps = (to, from, except, desc) => {
|
|
13
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
-
for (let key of __getOwnPropNames(from))
|
|
15
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
-
}
|
|
18
|
-
return to;
|
|
19
|
-
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
-
var index_exports = {};
|
|
30
|
-
__export(index_exports, {
|
|
31
|
-
Epub: () => Epub,
|
|
32
|
-
EpubFactory: () => EpubFactory,
|
|
33
|
-
EpubReadOnlyError: () => EpubReadOnlyError,
|
|
34
|
-
EpubVersionError: () => EpubVersionError,
|
|
35
|
-
MemoryAdapter: () => import_memory.MemoryAdapter,
|
|
36
|
-
TmpFsAdapter: () => import_tmpfs2.TmpFsAdapter
|
|
37
|
-
});
|
|
38
|
-
module.exports = __toCommonJS(index_exports);
|
|
39
|
-
var import_promises = require("node:fs/promises");
|
|
40
|
-
var import_async_mutex = require("async-mutex");
|
|
41
|
-
var import_fast_xml_parser = require("fast-xml-parser");
|
|
42
|
-
var import_mem = __toESM(require("mem"), 1);
|
|
43
|
-
var import_nanoid = require("nanoid");
|
|
44
|
-
var import_media_types = require("@storyteller-platform/media-types");
|
|
45
|
-
var import_path = require("@storyteller-platform/path");
|
|
46
|
-
var import_tmpfs = require("./adapters/tmpfs.cjs");
|
|
47
|
-
var Upgrade = __toESM(require("./upgrade.ts"), 1);
|
|
48
|
-
var import_memory = require("./adapters/memory.cjs");
|
|
49
|
-
var import_tmpfs2 = require("./adapters/tmpfs.cjs");
|
|
50
|
-
class EpubVersionError extends Error {
|
|
51
|
-
}
|
|
52
|
-
class EpubReadOnlyError extends Error {
|
|
53
|
-
}
|
|
54
|
-
const OPF_NAMESPACE = "http://www.idpf.org/2007/opf";
|
|
55
|
-
const ENTRY_ORIGIN = "epub:///";
|
|
56
|
-
function entryToUrlPath(entry) {
|
|
57
|
-
return entry.split("/").map(encodeURIComponent).join("/");
|
|
58
|
-
}
|
|
59
|
-
function resolveEntryUrl(from, href) {
|
|
60
|
-
const base = new URL(entryToUrlPath(from), ENTRY_ORIGIN);
|
|
61
|
-
if (!URL.canParse(href, base.toString())) return null;
|
|
62
|
-
const url = new URL(href, base);
|
|
63
|
-
return url.href.startsWith(ENTRY_ORIGIN) ? url : null;
|
|
64
|
-
}
|
|
65
|
-
function renameOpfAttributes(attrs, prefix) {
|
|
66
|
-
const attrPrefix = `@_${prefix}:`;
|
|
67
|
-
const renamed = {};
|
|
68
|
-
for (const [key, value] of Object.entries(attrs)) {
|
|
69
|
-
if (key === `@_xmlns:${prefix}`) continue;
|
|
70
|
-
if (key.startsWith(attrPrefix)) {
|
|
71
|
-
renamed[`@_opf:${key.slice(attrPrefix.length)}`] = value;
|
|
72
|
-
} else {
|
|
73
|
-
renamed[key] = value;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return renamed;
|
|
77
|
-
}
|
|
78
|
-
function stripOpfPrefix(node, prefix) {
|
|
79
|
-
if (Epub.isXmlTextNode(node)) return node;
|
|
80
|
-
const name = Epub.getXmlElementName(node);
|
|
81
|
-
const bareName = name.startsWith(`${prefix}:`) ? name.slice(prefix.length + 1) : name;
|
|
82
|
-
const element = {
|
|
83
|
-
[bareName]: Epub.getXmlChildren(node).map(
|
|
84
|
-
(child) => stripOpfPrefix(child, prefix)
|
|
85
|
-
)
|
|
86
|
-
};
|
|
87
|
-
if (node[":@"]) {
|
|
88
|
-
element[":@"] = renameOpfAttributes(node[":@"], prefix);
|
|
89
|
-
}
|
|
90
|
-
return element;
|
|
91
|
-
}
|
|
92
|
-
function normalizeOpfNamespacePrefixes(doc) {
|
|
93
|
-
var _a;
|
|
94
|
-
const root = doc.find(
|
|
95
|
-
(node) => !Epub.isXmlTextNode(node) && Epub.getXmlElementName(node).endsWith(":package")
|
|
96
|
-
);
|
|
97
|
-
if (!root) return doc;
|
|
98
|
-
const prefix = Epub.getXmlElementName(root).slice(0, -":package".length);
|
|
99
|
-
if (((_a = root[":@"]) == null ? void 0 : _a[`@_xmlns:${prefix}`]) !== OPF_NAMESPACE) return doc;
|
|
100
|
-
const stripped = stripOpfPrefix(root, prefix);
|
|
101
|
-
stripped[":@"] = {
|
|
102
|
-
...stripped[":@"],
|
|
103
|
-
"@_xmlns": OPF_NAMESPACE,
|
|
104
|
-
"@_xmlns:opf": OPF_NAMESPACE
|
|
105
|
-
};
|
|
106
|
-
doc[doc.indexOf(root)] = stripped;
|
|
107
|
-
return doc;
|
|
108
|
-
}
|
|
109
|
-
class Epub {
|
|
110
|
-
/**
|
|
111
|
-
* Prefer the static factories ({@link Epub.using}, {@link Epub.from},
|
|
112
|
-
* {@link Epub.create}, {@link Epub.upgrade}) over calling this constructor
|
|
113
|
-
* directly. It's public so {@link EpubFactory} can construct instances; nothing
|
|
114
|
-
* else should need to.
|
|
115
|
-
*/
|
|
116
|
-
constructor(adapterClass, adapter, inputPath, readonlyOverride = false) {
|
|
117
|
-
this.adapterClass = adapterClass;
|
|
118
|
-
this.adapter = adapter;
|
|
119
|
-
this.inputPath = inputPath;
|
|
120
|
-
this.readonlyOverride = readonlyOverride;
|
|
121
|
-
this.storage = adapterClass.kind;
|
|
122
|
-
this.readXhtmlItemContents = (0, import_mem.default)(
|
|
123
|
-
this.readXhtmlItemContents.bind(this),
|
|
124
|
-
// This isn't unnecessary, the generic here just isn't handling the
|
|
125
|
-
// overloaded method type correctly
|
|
126
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
127
|
-
{ cacheKey: ([id, as]) => `${id}:${as ?? "xhtml"}` }
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
static xmlParser = new import_fast_xml_parser.XMLParser({
|
|
131
|
-
allowBooleanAttributes: true,
|
|
132
|
-
preserveOrder: true,
|
|
133
|
-
ignoreAttributes: false,
|
|
134
|
-
parseTagValue: false
|
|
135
|
-
});
|
|
136
|
-
static xhtmlParser = (() => {
|
|
137
|
-
const parser = new import_fast_xml_parser.XMLParser({
|
|
138
|
-
allowBooleanAttributes: true,
|
|
139
|
-
alwaysCreateTextNode: true,
|
|
140
|
-
preserveOrder: true,
|
|
141
|
-
ignoreAttributes: false,
|
|
142
|
-
htmlEntities: true,
|
|
143
|
-
trimValues: false,
|
|
144
|
-
stopNodes: ["*.pre", "*.script"],
|
|
145
|
-
parseTagValue: false,
|
|
146
|
-
updateTag(_tagName, _jPath, attrs) {
|
|
147
|
-
if (attrs && "@_/" in attrs) {
|
|
148
|
-
delete attrs["@_/"];
|
|
149
|
-
}
|
|
150
|
-
return true;
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
parser.addEntity("nbsp", "\xA0");
|
|
154
|
-
parser.addEntity("#160", "\xA0");
|
|
155
|
-
return parser;
|
|
156
|
-
})();
|
|
157
|
-
static xmlBuilder = new import_fast_xml_parser.XMLBuilder({
|
|
158
|
-
preserveOrder: true,
|
|
159
|
-
format: true,
|
|
160
|
-
ignoreAttributes: false,
|
|
161
|
-
suppressEmptyNode: true
|
|
162
|
-
});
|
|
163
|
-
static xhtmlBuilder = new import_fast_xml_parser.XMLBuilder({
|
|
164
|
-
preserveOrder: true,
|
|
165
|
-
ignoreAttributes: false,
|
|
166
|
-
stopNodes: ["*.pre", "*.script"],
|
|
167
|
-
suppressEmptyNode: true
|
|
168
|
-
});
|
|
169
|
-
/**
|
|
170
|
-
* Format a duration, provided as a number of seconds, as
|
|
171
|
-
* a SMIL clock value, to be used for Media Overlays.
|
|
172
|
-
*
|
|
173
|
-
* @link https://www.w3.org/TR/epub-33/#sec-duration
|
|
174
|
-
*/
|
|
175
|
-
static formatSmilDuration(duration) {
|
|
176
|
-
const hours = Math.floor(duration / 3600);
|
|
177
|
-
const minutes = Math.floor(duration / 60 - hours * 60);
|
|
178
|
-
const secondsAndMillis = duration - minutes * 60 - hours * 3600;
|
|
179
|
-
const [seconds, millis] = secondsAndMillis.toFixed(2).split(".");
|
|
180
|
-
return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.padStart(2, "0")}.${millis ?? "0"}`;
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* Given an XML structure representing a complete XHTML document,
|
|
184
|
-
* add a `link` element to the `head` of the document.
|
|
185
|
-
*
|
|
186
|
-
* This method modifies the provided XML structure.
|
|
187
|
-
*/
|
|
188
|
-
static addLinkToXhtmlHead(xml, link) {
|
|
189
|
-
const html = Epub.findXmlChildByName("html", xml);
|
|
190
|
-
if (!html) throw new Error("Invalid XHTML document: no html element");
|
|
191
|
-
const head = Epub.findXmlChildByName("head", html.html);
|
|
192
|
-
if (!head) throw new Error("Invalid XHTML document: no head element");
|
|
193
|
-
head["head"].push({
|
|
194
|
-
link: [],
|
|
195
|
-
":@": {
|
|
196
|
-
"@_rel": link.rel,
|
|
197
|
-
"@_href": link.href,
|
|
198
|
-
"@_type": link.type
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Given an XML structure representing a complete XHTML document,
|
|
204
|
-
* return the sub-structure representing the children of the
|
|
205
|
-
* document's body element.
|
|
206
|
-
*/
|
|
207
|
-
static getXhtmlBody(xml) {
|
|
208
|
-
const html = Epub.findXmlChildByName("html", xml);
|
|
209
|
-
if (!html) throw new Error("Invalid XHTML document: no html element");
|
|
210
|
-
const body = Epub.findXmlChildByName("body", html["html"]);
|
|
211
|
-
if (!body) throw new Error("Invalid XHTML document: No body element");
|
|
212
|
-
return body["body"];
|
|
213
|
-
}
|
|
214
|
-
static createXmlElement(name, properties, children = []) {
|
|
215
|
-
return {
|
|
216
|
-
":@": Object.fromEntries(
|
|
217
|
-
Object.entries(properties).map(([prop, value]) => [`@_${prop}`, value])
|
|
218
|
-
),
|
|
219
|
-
[name]: children
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
static createXmlTextNode(text) {
|
|
223
|
-
return { ["#text"]: text };
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Given an XML structure representing a complete XHTML document,
|
|
227
|
-
* return a string representing the concatenation of all text nodes
|
|
228
|
-
* in the document.
|
|
229
|
-
*/
|
|
230
|
-
static getXhtmlTextContent(xml) {
|
|
231
|
-
let text = "";
|
|
232
|
-
for (const child of xml) {
|
|
233
|
-
if (Epub.isXmlTextNode(child)) {
|
|
234
|
-
text += child["#text"];
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
const children = Epub.getXmlChildren(child);
|
|
238
|
-
text += Epub.getXhtmlTextContent(children);
|
|
239
|
-
}
|
|
240
|
-
return text;
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* Given an XMLElement, return its attributes.
|
|
244
|
-
*/
|
|
245
|
-
static getXmlAttributes(element) {
|
|
246
|
-
return Object.fromEntries(
|
|
247
|
-
Object.entries(element[":@"] ?? {}).map(([key, value]) => [
|
|
248
|
-
key.slice(2),
|
|
249
|
-
value
|
|
250
|
-
])
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Given an XMLElement, return its tag name.
|
|
255
|
-
*/
|
|
256
|
-
static getXmlElementName(element) {
|
|
257
|
-
const keys = Object.keys(element);
|
|
258
|
-
const elementName = keys.find((key) => key !== ":@" && key !== "#text");
|
|
259
|
-
if (!elementName)
|
|
260
|
-
throw new Error(
|
|
261
|
-
`Invalid XML Element: missing tag name
|
|
262
|
-
${JSON.stringify(element, null, 2)}`
|
|
263
|
-
);
|
|
264
|
-
return elementName;
|
|
265
|
-
}
|
|
266
|
-
/**
|
|
267
|
-
* Given an XMLElement, return a list of its children
|
|
268
|
-
*/
|
|
269
|
-
static getXmlChildren(element) {
|
|
270
|
-
const elementName = Epub.getXmlElementName(element);
|
|
271
|
-
return element[elementName];
|
|
272
|
-
}
|
|
273
|
-
static replaceXmlChildren(element, children) {
|
|
274
|
-
const elementName = Epub.getXmlElementName(element);
|
|
275
|
-
element[elementName] = children;
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* Given an XML structure, find the first child matching
|
|
279
|
-
* the provided name and optional filter.
|
|
280
|
-
*/
|
|
281
|
-
static findXmlChildByName(name, xml, filter) {
|
|
282
|
-
const element = xml.find(
|
|
283
|
-
(e) => name in e && (filter ? filter(e) : true)
|
|
284
|
-
);
|
|
285
|
-
return element;
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Given an XML structure, find the first descendant matching
|
|
289
|
-
* the provided name and optional filter.
|
|
290
|
-
*
|
|
291
|
-
* Will perform a breadth first search for the element, returning
|
|
292
|
-
* the highest element in the tree matching the name and filter.
|
|
293
|
-
*/
|
|
294
|
-
static findXmlDescendantByName(name, xml, filter) {
|
|
295
|
-
const found = Epub.findXmlChildByName(name, xml, filter);
|
|
296
|
-
if (found) return found;
|
|
297
|
-
for (const node of xml) {
|
|
298
|
-
if (Epub.isXmlTextNode(node)) continue;
|
|
299
|
-
const children = Epub.getXmlChildren(node);
|
|
300
|
-
const found2 = this.findXmlDescendantByName(name, children, filter);
|
|
301
|
-
if (found2) return found2;
|
|
302
|
-
}
|
|
303
|
-
return void 0;
|
|
304
|
-
}
|
|
305
|
-
/**
|
|
306
|
-
* Given an XMLNode, determine whether it represents
|
|
307
|
-
* a text node or an XML element.
|
|
308
|
-
*/
|
|
309
|
-
static isXmlTextNode(node) {
|
|
310
|
-
return "#text" in node;
|
|
311
|
-
}
|
|
312
|
-
rootfile = null;
|
|
313
|
-
manifest = null;
|
|
314
|
-
spine = null;
|
|
315
|
-
packageMutex = new import_async_mutex.Mutex();
|
|
316
|
-
/**
|
|
317
|
-
* Storage backend kind in use for this instance
|
|
318
|
-
*
|
|
319
|
-
* Public so callers can declare type-level requirements via {@link InMemoryEpubReader}
|
|
320
|
-
* Orthogonal to the read-only / writable axis (controlled by `readonlyOverride`
|
|
321
|
-
* and the adapter's capability bag)
|
|
322
|
-
*/
|
|
323
|
-
storage;
|
|
324
|
-
/**
|
|
325
|
-
* Runtime guard for mutation methods
|
|
326
|
-
*/
|
|
327
|
-
assertWritable() {
|
|
328
|
-
if (!this.adapterClass.capabilities.writable || this.readonlyOverride) {
|
|
329
|
-
throw new EpubReadOnlyError(
|
|
330
|
-
"cannot mutate a read-only Epub. open via Epub.using(TmpFsAdapter).from(path) (without { readonly: true }) to modify."
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
/**
|
|
335
|
-
* Construct a new EPUB on a writable backend, optionally seeded
|
|
336
|
-
* with the provided metadata. Equivalent to
|
|
337
|
-
* `Epub.using(TmpFsAdapter).create(...)`.
|
|
338
|
-
*
|
|
339
|
-
* @param dublinCore Core metadata terms
|
|
340
|
-
* @param additionalMetadata An array of additional metadata entries
|
|
341
|
-
*/
|
|
342
|
-
static async create(path, dublinCore, additionalMetadata = []) {
|
|
343
|
-
return Epub.using(import_tmpfs.TmpFsAdapter).create(path, dublinCore, additionalMetadata);
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Specify the storage backend to use for the EPUB
|
|
347
|
-
*
|
|
348
|
-
* The returned factory exposes `from`, `create`, and `upgrade`,
|
|
349
|
-
* which route through the supplied adapter.
|
|
350
|
-
*
|
|
351
|
-
* @example
|
|
352
|
-
* ```ts
|
|
353
|
-
* using epub = await Epub.using(TmpFsAdapter).from(path)
|
|
354
|
-
* using reader = await Epub.using(MemoryAdapter).from(buffer, { cache: false })
|
|
355
|
-
* ```
|
|
356
|
-
*/
|
|
357
|
-
static using(adapterClass) {
|
|
358
|
-
return new EpubFactory(adapterClass);
|
|
359
|
-
}
|
|
360
|
-
static async from(pathOrData, options = {}) {
|
|
361
|
-
return Epub.using(import_tmpfs.TmpFsAdapter).from(pathOrData, options);
|
|
362
|
-
}
|
|
363
|
-
static async assertEpub3(epub) {
|
|
364
|
-
const version = await epub.getVersion();
|
|
365
|
-
if (!version.startsWith("3.")) {
|
|
366
|
-
epub.discardAndClose();
|
|
367
|
-
throw new EpubVersionError(
|
|
368
|
-
"This is not a valid EPUB 3 publication. This library only supports EPUB 3, not EPUB 2. Use Epub.upgrade(path) to convert."
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
async copy(path) {
|
|
373
|
-
if (!this.adapter.duplicate) {
|
|
374
|
-
throw new Error(
|
|
375
|
-
`cannot copy an Epub backed by ${this.adapterClass.kind}: adapter does not implement duplicate()`
|
|
376
|
-
);
|
|
377
|
-
}
|
|
378
|
-
const newAdapter = await this.adapter.duplicate();
|
|
379
|
-
return new Epub(this.adapterClass, newAdapter, path);
|
|
380
|
-
}
|
|
381
|
-
async removeEntry(href) {
|
|
382
|
-
this.assertWritable();
|
|
383
|
-
if (!this.adapter.remove) {
|
|
384
|
-
throw new EpubReadOnlyError(
|
|
385
|
-
`adapter ${this.adapterClass.kind} does not support entry removal`
|
|
386
|
-
);
|
|
387
|
-
}
|
|
388
|
-
const rootfile = await this.getRootfile();
|
|
389
|
-
await this.adapter.remove(await this.resolveEntry(rootfile, href));
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* Length of the underlying archive entry for a manifest item, in bytes
|
|
393
|
-
* Necessary to compute the readium page count which is for COMPRESSED content
|
|
394
|
-
* @see {@link https://github.com/readium/architecture/issues/123}
|
|
395
|
-
*/
|
|
396
|
-
async getItemArchiveLength(id) {
|
|
397
|
-
const rootfile = await this.getRootfile();
|
|
398
|
-
const manifest = await this.getManifest();
|
|
399
|
-
const manifestItem = manifest[id];
|
|
400
|
-
if (!manifestItem)
|
|
401
|
-
throw new Error(`Could not find item with id "${id}" in manifest`);
|
|
402
|
-
return this.adapter.archiveLength(
|
|
403
|
-
await this.resolveEntry(rootfile, manifestItem.href)
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
async getRootfile() {
|
|
407
|
-
var _a;
|
|
408
|
-
if (this.rootfile !== null) return this.rootfile;
|
|
409
|
-
const containerString = await this.adapter.read(
|
|
410
|
-
"META-INF/container.xml",
|
|
411
|
-
"utf-8"
|
|
412
|
-
);
|
|
413
|
-
if (!containerString)
|
|
414
|
-
throw new Error("Failed to parse EPUB: Missing META-INF/container.xml");
|
|
415
|
-
const containerDocument = Epub.xmlParser.parse(containerString);
|
|
416
|
-
const container = Epub.findXmlChildByName("container", containerDocument);
|
|
417
|
-
if (!container)
|
|
418
|
-
throw new Error(
|
|
419
|
-
"Failed to parse EPUB container.xml: Found no container element"
|
|
420
|
-
);
|
|
421
|
-
const rootfiles = Epub.findXmlChildByName(
|
|
422
|
-
"rootfiles",
|
|
423
|
-
Epub.getXmlChildren(container)
|
|
424
|
-
);
|
|
425
|
-
if (!rootfiles)
|
|
426
|
-
throw new Error(
|
|
427
|
-
"Failed to parse EPUB container.xml: Found no rootfiles element"
|
|
428
|
-
);
|
|
429
|
-
const rootfile = Epub.findXmlChildByName(
|
|
430
|
-
"rootfile",
|
|
431
|
-
Epub.getXmlChildren(rootfiles),
|
|
432
|
-
(node) => {
|
|
433
|
-
var _a2;
|
|
434
|
-
return !Epub.isXmlTextNode(node) && ((_a2 = node[":@"]) == null ? void 0 : _a2["@_media-type"]) === import_media_types.MediaType.OPF.mime;
|
|
435
|
-
}
|
|
436
|
-
);
|
|
437
|
-
const fullPath = (_a = rootfile == null ? void 0 : rootfile[":@"]) == null ? void 0 : _a["@_full-path"];
|
|
438
|
-
if (!fullPath)
|
|
439
|
-
throw new Error(
|
|
440
|
-
"Failed to parse EPUB container.xml: Found no rootfile element"
|
|
441
|
-
);
|
|
442
|
-
this.rootfile = await this.resolveEntry("", fullPath);
|
|
443
|
-
return this.rootfile;
|
|
444
|
-
}
|
|
445
|
-
async getPackageDocument() {
|
|
446
|
-
const rootfile = await this.getRootfile();
|
|
447
|
-
const packageDocumentString = await this.adapter.read(rootfile, "utf-8");
|
|
448
|
-
if (!packageDocumentString)
|
|
449
|
-
throw new Error(
|
|
450
|
-
`Failed to parse EPUB: could not find package document at ${rootfile}`
|
|
451
|
-
);
|
|
452
|
-
const packageDocument = Epub.xmlParser.parse(
|
|
453
|
-
packageDocumentString
|
|
454
|
-
);
|
|
455
|
-
return normalizeOpfNamespacePrefixes(packageDocument);
|
|
456
|
-
}
|
|
457
|
-
async getPackageElement() {
|
|
458
|
-
const packageDocument = await this.getPackageDocument();
|
|
459
|
-
const packageElement = Epub.findXmlChildByName("package", packageDocument);
|
|
460
|
-
if (!packageElement) {
|
|
461
|
-
throw new Error(
|
|
462
|
-
"Failed to parse EPUB: Found no package element in package document"
|
|
463
|
-
);
|
|
464
|
-
}
|
|
465
|
-
return packageElement;
|
|
466
|
-
}
|
|
467
|
-
/**
|
|
468
|
-
* Safely modify the package document, without race conditions.
|
|
469
|
-
*
|
|
470
|
-
* Since the reading the package document is an async process,
|
|
471
|
-
* multiple simultaneously dispatched function calls that all
|
|
472
|
-
* attempt to modify it can clobber each other's changes. This
|
|
473
|
-
* method uses a mutex to ensure that each update runs exclusively.
|
|
474
|
-
*
|
|
475
|
-
* @param producer The function to update the package document. If
|
|
476
|
-
* it returns a new package document, that will be persisted, otherwise
|
|
477
|
-
* it will be assumed that the package document was modified in place.
|
|
478
|
-
*/
|
|
479
|
-
async withPackage(producer) {
|
|
480
|
-
this.assertWritable();
|
|
481
|
-
await this.packageMutex.runExclusive(async () => {
|
|
482
|
-
const packageDocument = await this.getPackageDocument();
|
|
483
|
-
const packageElement = Epub.findXmlChildByName("package", packageDocument);
|
|
484
|
-
if (!packageElement) {
|
|
485
|
-
throw new Error(
|
|
486
|
-
"Failed to parse EPUB: Found no package element in package document"
|
|
487
|
-
);
|
|
488
|
-
}
|
|
489
|
-
const produced = await producer(packageElement);
|
|
490
|
-
const updatedPackageDocument = await Epub.xmlBuilder.build(
|
|
491
|
-
produced ?? packageDocument
|
|
492
|
-
);
|
|
493
|
-
const rootfile = await this.getRootfile();
|
|
494
|
-
await this.writeEntryContents(rootfile, updatedPackageDocument, "utf-8");
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
/**
|
|
498
|
-
* Retrieve the manifest for the Epub.
|
|
499
|
-
*
|
|
500
|
-
* This is represented as a map from each manifest items'
|
|
501
|
-
* id to the rest of its properties.
|
|
502
|
-
*
|
|
503
|
-
* @link https://www.w3.org/TR/epub-33/#sec-pkg-manifest
|
|
504
|
-
*/
|
|
505
|
-
async getManifest() {
|
|
506
|
-
if (this.manifest !== null) return this.manifest;
|
|
507
|
-
const packageElement = await this.getPackageElement();
|
|
508
|
-
const manifest = Epub.findXmlChildByName(
|
|
509
|
-
"manifest",
|
|
510
|
-
Epub.getXmlChildren(packageElement)
|
|
511
|
-
);
|
|
512
|
-
if (!manifest)
|
|
513
|
-
throw new Error(
|
|
514
|
-
"Failed to parse EPUB: Found no manifest element in package document"
|
|
515
|
-
);
|
|
516
|
-
this.manifest = Epub.getXmlChildren(manifest).reduce((acc, item) => {
|
|
517
|
-
var _a, _b;
|
|
518
|
-
if (Epub.isXmlTextNode(item)) return acc;
|
|
519
|
-
if (!((_a = item[":@"]) == null ? void 0 : _a["@_id"]) || !item[":@"]["@_href"]) {
|
|
520
|
-
return acc;
|
|
521
|
-
}
|
|
522
|
-
return {
|
|
523
|
-
...acc,
|
|
524
|
-
[item[":@"]["@_id"]]: {
|
|
525
|
-
id: item[":@"]["@_id"],
|
|
526
|
-
href: item[":@"]["@_href"],
|
|
527
|
-
mediaType: item[":@"]["@_media-type"],
|
|
528
|
-
mediaOverlay: item[":@"]["@_media-overlay"],
|
|
529
|
-
fallback: item[":@"]["@_fallback"],
|
|
530
|
-
properties: (_b = item[":@"]["@_properties"]) == null ? void 0 : _b.split(" ")
|
|
531
|
-
}
|
|
532
|
-
};
|
|
533
|
-
}, {});
|
|
534
|
-
return this.manifest;
|
|
535
|
-
}
|
|
536
|
-
/**
|
|
537
|
-
* Returns the first index in the metadata element's children array
|
|
538
|
-
* that matches the provided predicate.
|
|
539
|
-
*
|
|
540
|
-
* Note: This may technically be different than the index in the
|
|
541
|
-
* getMetadata() array, as it includes non-metadata nodes, like
|
|
542
|
-
* text nodes. These are technically not allowed, but may exist,
|
|
543
|
-
* nonetheless. As consumers only ever see the getMetadata()
|
|
544
|
-
* array, this method is only meant to be used internally.
|
|
545
|
-
*/
|
|
546
|
-
findMetadataIndex(packageElement, predicate) {
|
|
547
|
-
const metadataElement = Epub.findXmlChildByName(
|
|
548
|
-
"metadata",
|
|
549
|
-
Epub.getXmlChildren(packageElement)
|
|
550
|
-
);
|
|
551
|
-
if (!metadataElement)
|
|
552
|
-
throw new Error(
|
|
553
|
-
"Failed to parse EPUB: Found no metadata element in package document"
|
|
554
|
-
);
|
|
555
|
-
return metadataElement.metadata.findIndex((node) => {
|
|
556
|
-
const item = Epub.parseMetadataItem(node);
|
|
557
|
-
if (!item) return false;
|
|
558
|
-
return predicate(item);
|
|
559
|
-
});
|
|
560
|
-
}
|
|
561
|
-
/**
|
|
562
|
-
* Returns the item in the metadata element's children array
|
|
563
|
-
* that matches the provided predicate.
|
|
564
|
-
*/
|
|
565
|
-
async findMetadataItem(predicate) {
|
|
566
|
-
const [first] = await this.findAllMetadataItems(predicate);
|
|
567
|
-
return first ?? null;
|
|
568
|
-
}
|
|
569
|
-
/**
|
|
570
|
-
* Returns the item in the metadata element's children array
|
|
571
|
-
* that matches the provided predicate.
|
|
572
|
-
*/
|
|
573
|
-
async findAllMetadataItems(predicate) {
|
|
574
|
-
const packageElement = await this.getPackageElement();
|
|
575
|
-
const metadataElement = Epub.findXmlChildByName(
|
|
576
|
-
"metadata",
|
|
577
|
-
Epub.getXmlChildren(packageElement)
|
|
578
|
-
);
|
|
579
|
-
if (!metadataElement)
|
|
580
|
-
throw new Error(
|
|
581
|
-
"Failed to parse EPUB: Found no metadata element in package document"
|
|
582
|
-
);
|
|
583
|
-
const elements = metadataElement.metadata.filter((node) => {
|
|
584
|
-
const item = Epub.parseMetadataItem(node);
|
|
585
|
-
if (!item) return false;
|
|
586
|
-
return predicate(item);
|
|
587
|
-
});
|
|
588
|
-
return elements.map((element) => Epub.parseMetadataItem(element)).filter((item) => !!item);
|
|
589
|
-
}
|
|
590
|
-
static parseMetadataItem(node) {
|
|
591
|
-
if (Epub.isXmlTextNode(node)) return null;
|
|
592
|
-
const elementName = Epub.getXmlElementName(node);
|
|
593
|
-
const textNode = Epub.getXmlChildren(node)[0];
|
|
594
|
-
const value = !textNode || !Epub.isXmlTextNode(textNode) ? void 0 : textNode["#text"].replaceAll(/\s+/g, " ");
|
|
595
|
-
const attributes = node[":@"] ?? {};
|
|
596
|
-
const { id, ...properties } = Object.fromEntries(
|
|
597
|
-
Object.entries(attributes).map(([attrName, value2]) => [
|
|
598
|
-
attrName.slice(2),
|
|
599
|
-
value2
|
|
600
|
-
])
|
|
601
|
-
);
|
|
602
|
-
return {
|
|
603
|
-
id,
|
|
604
|
-
type: elementName,
|
|
605
|
-
properties,
|
|
606
|
-
value
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
/**
|
|
610
|
-
* Retrieve the metadata entries for the Epub.
|
|
611
|
-
*
|
|
612
|
-
* This is represented as an array of metadata entries,
|
|
613
|
-
* in the order that they're presented in the Epub package document.
|
|
614
|
-
*
|
|
615
|
-
* For more useful semantic representations of metadata, use
|
|
616
|
-
* specific methods such as `getTitle()` and `getAuthors()`.
|
|
617
|
-
*
|
|
618
|
-
* @link https://www.w3.org/TR/epub-33/#sec-pkg-metadata
|
|
619
|
-
*/
|
|
620
|
-
async getMetadata() {
|
|
621
|
-
const packageElement = await this.getPackageElement();
|
|
622
|
-
const metadataElement = Epub.findXmlChildByName(
|
|
623
|
-
"metadata",
|
|
624
|
-
Epub.getXmlChildren(packageElement)
|
|
625
|
-
);
|
|
626
|
-
if (!metadataElement)
|
|
627
|
-
throw new Error(
|
|
628
|
-
"Failed to parse EPUB: Found no metadata element in package document"
|
|
629
|
-
);
|
|
630
|
-
const metadata = metadataElement.metadata.map((node) => Epub.parseMetadataItem(node)).filter((node) => !!node);
|
|
631
|
-
return metadata;
|
|
632
|
-
}
|
|
633
|
-
/**
|
|
634
|
-
* Retrieve the first identifier from the dc:identifier element
|
|
635
|
-
* in the EPUB metadata.
|
|
636
|
-
*
|
|
637
|
-
* If there is no dc:identifier element, returns null.
|
|
638
|
-
*
|
|
639
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dcidentifier
|
|
640
|
-
*
|
|
641
|
-
* @deprecated Use {@link getUniqueIdentifier} instead to get the unique identifier,
|
|
642
|
-
* or {@link getIdentifiers} to get all identifiers.
|
|
643
|
-
*/
|
|
644
|
-
async getIdentifier() {
|
|
645
|
-
const metadata = await this.getMetadata();
|
|
646
|
-
const entry = metadata.find(({ type }) => type === "dc:identifier");
|
|
647
|
-
return (entry == null ? void 0 : entry.value) ?? null;
|
|
648
|
-
}
|
|
649
|
-
/**
|
|
650
|
-
* Set the dc:identifier metadata element with the provided string.
|
|
651
|
-
*
|
|
652
|
-
* Updates the existing dc:identifier element if one exists.
|
|
653
|
-
* Otherwise creates a new element
|
|
654
|
-
*
|
|
655
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dcidentifier
|
|
656
|
-
*
|
|
657
|
-
* @deprecated Use {@link setUniqueIdentifier} instead.
|
|
658
|
-
*/
|
|
659
|
-
async setIdentifier(identifier) {
|
|
660
|
-
await this.replaceMetadata(({ type }) => type === "dc:identifier", {
|
|
661
|
-
type: "dc:identifier",
|
|
662
|
-
properties: {},
|
|
663
|
-
value: identifier
|
|
664
|
-
});
|
|
665
|
-
}
|
|
666
|
-
/**
|
|
667
|
-
* Retrieve the identifier with the unique identifier id
|
|
668
|
-
* in the EPUB metadata.
|
|
669
|
-
*
|
|
670
|
-
* If there is no unique identifier id, returns the first dc:identifier element.
|
|
671
|
-
*
|
|
672
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dcidentifier
|
|
673
|
-
*/
|
|
674
|
-
async getUniqueIdentifier() {
|
|
675
|
-
const metadata = await this.getMetadata();
|
|
676
|
-
const uniqueId = await this.getUniqueIdentifierId();
|
|
677
|
-
if (!uniqueId) {
|
|
678
|
-
return null;
|
|
679
|
-
}
|
|
680
|
-
const entry = metadata.find(
|
|
681
|
-
({ type, id }) => type === "dc:identifier" && id === uniqueId
|
|
682
|
-
);
|
|
683
|
-
return (entry == null ? void 0 : entry.value) ?? null;
|
|
684
|
-
}
|
|
685
|
-
/**
|
|
686
|
-
* Set the unique identifier id for the EPUB.
|
|
687
|
-
*
|
|
688
|
-
* Updates the existing dc:identifier element referenced by the unique identifier id if one exists.
|
|
689
|
-
* Otherwise creates a new element with the provided identifier, and sets the unique identifier id to the new element's id.
|
|
690
|
-
*
|
|
691
|
-
* Note: you likely shouldn't change the unique identifier id unless you are producing a new EPUB.
|
|
692
|
-
*
|
|
693
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dcidentifier
|
|
694
|
-
*/
|
|
695
|
-
async setUniqueIdentifier(identifier) {
|
|
696
|
-
await this.withPackage(async (packageElement) => {
|
|
697
|
-
const metadata = Epub.findXmlChildByName(
|
|
698
|
-
"metadata",
|
|
699
|
-
Epub.getXmlChildren(packageElement)
|
|
700
|
-
);
|
|
701
|
-
if (!metadata) {
|
|
702
|
-
throw new Error(
|
|
703
|
-
"Failed to parse EPUB: found no metadata element in package document"
|
|
704
|
-
);
|
|
705
|
-
}
|
|
706
|
-
let uniqueId = await this.getUniqueIdentifierId();
|
|
707
|
-
if (!uniqueId) {
|
|
708
|
-
const newUniqueId = (0, import_nanoid.nanoid)();
|
|
709
|
-
packageElement[":@"] = {
|
|
710
|
-
...packageElement[":@"],
|
|
711
|
-
"@_unique-identifier": newUniqueId
|
|
712
|
-
};
|
|
713
|
-
uniqueId = newUniqueId;
|
|
714
|
-
}
|
|
715
|
-
const children = Epub.getXmlChildren(metadata);
|
|
716
|
-
const entry = Epub.findXmlChildByName(
|
|
717
|
-
"dc:identifier",
|
|
718
|
-
children,
|
|
719
|
-
(node) => {
|
|
720
|
-
var _a;
|
|
721
|
-
return ((_a = node[":@"]) == null ? void 0 : _a["@_id"]) === uniqueId;
|
|
722
|
-
}
|
|
723
|
-
);
|
|
724
|
-
if (entry) {
|
|
725
|
-
children.splice(
|
|
726
|
-
children.indexOf(entry),
|
|
727
|
-
1,
|
|
728
|
-
Epub.createXmlElement("dc:identifier", { id: uniqueId }, [
|
|
729
|
-
Epub.createXmlTextNode(identifier)
|
|
730
|
-
])
|
|
731
|
-
);
|
|
732
|
-
return;
|
|
733
|
-
}
|
|
734
|
-
children.push(
|
|
735
|
-
Epub.createXmlElement("dc:identifier", { id: uniqueId }, [
|
|
736
|
-
Epub.createXmlTextNode(identifier)
|
|
737
|
-
])
|
|
738
|
-
);
|
|
739
|
-
});
|
|
740
|
-
}
|
|
741
|
-
/**
|
|
742
|
-
* Retrieve the id of the publication's unique identifier, as declared by the
|
|
743
|
-
* package element's `unique-identifier` attribute.
|
|
744
|
-
*
|
|
745
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dcidentifier
|
|
746
|
-
*/
|
|
747
|
-
async getUniqueIdentifierId() {
|
|
748
|
-
var _a;
|
|
749
|
-
const packageElement = await this.getPackageElement();
|
|
750
|
-
return ((_a = packageElement[":@"]) == null ? void 0 : _a["@_unique-identifier"]) ?? null;
|
|
751
|
-
}
|
|
752
|
-
/**
|
|
753
|
-
* Collect `dc:identifier` or `dc:source` entries, attaching the value and
|
|
754
|
-
* scheme of any refining `identifier-type` meta (spec D.3.8) and a legacy
|
|
755
|
-
* `opf:scheme` attribute. Values are not interpreted.
|
|
756
|
-
*
|
|
757
|
-
* `onRefinement` is invoked for every other meta refining a collected entry,
|
|
758
|
-
* so callers can surface element-specific refinements (e.g. `source-of` on a
|
|
759
|
-
* `dc:source`).
|
|
760
|
-
*/
|
|
761
|
-
static collectDcEntries(metadata, type, onRefinement) {
|
|
762
|
-
const entries = metadata.filter((entry) => entry.type === type && entry.value !== void 0).map((entry) => ({
|
|
763
|
-
// filtered above, so value is defined
|
|
764
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
765
|
-
value: entry.value,
|
|
766
|
-
...entry.id && { id: entry.id },
|
|
767
|
-
...entry.properties["opf:scheme"] && {
|
|
768
|
-
scheme: entry.properties["opf:scheme"]
|
|
769
|
-
}
|
|
770
|
-
}));
|
|
771
|
-
for (const meta of metadata) {
|
|
772
|
-
if (meta.type !== "meta" || meta.value === void 0) continue;
|
|
773
|
-
const property = meta.properties["property"];
|
|
774
|
-
const refines = meta.properties["refines"];
|
|
775
|
-
if (!property || !refines) continue;
|
|
776
|
-
const target = entries.find((t) => t.id === refines.slice(1));
|
|
777
|
-
if (!target) continue;
|
|
778
|
-
if (property === "identifier-type") {
|
|
779
|
-
target.identifierType = meta.value;
|
|
780
|
-
if (meta.properties["scheme"]) target.scheme = meta.properties["scheme"];
|
|
781
|
-
} else {
|
|
782
|
-
onRefinement == null ? void 0 : onRefinement(target, property, meta.value);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
return entries;
|
|
786
|
-
}
|
|
787
|
-
/**
|
|
788
|
-
* Retrieve every `dc:identifier` entry, returned as found.
|
|
789
|
-
*
|
|
790
|
-
* Values are not interpreted. Any refining `identifier-type` meta (spec
|
|
791
|
-
* D.3.8) or legacy `opf:scheme` attribute is surfaced on the entry, but no
|
|
792
|
-
* parsing of the value itself is attempted. To read `dc:source` entries, use
|
|
793
|
-
* {@link getSources}.
|
|
794
|
-
*
|
|
795
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dcidentifier
|
|
796
|
-
*/
|
|
797
|
-
async getIdentifiers() {
|
|
798
|
-
const metadata = await this.getMetadata();
|
|
799
|
-
return Epub.collectDcEntries(metadata, "dc:identifier");
|
|
800
|
-
}
|
|
801
|
-
/**
|
|
802
|
-
* Retrieve every `dc:source` entry, returned as found.
|
|
803
|
-
*
|
|
804
|
-
* Like {@link getIdentifiers}, values are not interpreted. In addition to a
|
|
805
|
-
* refining `identifier-type`, a refining `source-of` meta (spec D.3.11) is
|
|
806
|
-
* surfaced as `sourceOf`.
|
|
807
|
-
*
|
|
808
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dcsource
|
|
809
|
-
*/
|
|
810
|
-
async getSources() {
|
|
811
|
-
const metadata = await this.getMetadata();
|
|
812
|
-
return Epub.collectDcEntries(
|
|
813
|
-
metadata,
|
|
814
|
-
"dc:source",
|
|
815
|
-
(source, property) => {
|
|
816
|
-
if (property === "source-of") {
|
|
817
|
-
source.isPageBreakSource = true;
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
);
|
|
821
|
-
}
|
|
822
|
-
/**
|
|
823
|
-
* Retrieve the `pageBreakSource` property (EPUB 3.4, spec D.2.9), the
|
|
824
|
-
* publication-level source for the source of its page break markers.
|
|
825
|
-
*
|
|
826
|
-
* This property replaces the refining `source-of="pagination"` meta (spec
|
|
827
|
-
* D.3.11), see {@link EpubSource.sourceOf}. If no `pageBreakSource` property is found,
|
|
828
|
-
* we fall back to finding a `dc:source` with a `source-of="pagination"` refinement.
|
|
829
|
-
*
|
|
830
|
-
* @link https://www.w3.org/TR/epub/#pageBreakSource
|
|
831
|
-
*/
|
|
832
|
-
async getPageBreakSource() {
|
|
833
|
-
const entry = await this.findMetadataItem(
|
|
834
|
-
(item) => item.type === "meta" && item.properties["property"] === "pageBreakSource" && !!item.value
|
|
835
|
-
);
|
|
836
|
-
if (entry) {
|
|
837
|
-
return {
|
|
838
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
839
|
-
value: entry.value,
|
|
840
|
-
id: entry.id,
|
|
841
|
-
identifierType: void 0,
|
|
842
|
-
scheme: void 0,
|
|
843
|
-
isPageBreakSource: true
|
|
844
|
-
};
|
|
845
|
-
}
|
|
846
|
-
const sources = await this.getSources();
|
|
847
|
-
const source = sources.find((source2) => source2.isPageBreakSource);
|
|
848
|
-
if (source) {
|
|
849
|
-
return source;
|
|
850
|
-
}
|
|
851
|
-
return null;
|
|
852
|
-
}
|
|
853
|
-
/**
|
|
854
|
-
* Set the `pageBreakSource` property (EPUB 3.4, spec D.2.9), or remove it when
|
|
855
|
-
* passed null. Replaces an existing `pageBreakSource` meta if present.
|
|
856
|
-
*
|
|
857
|
-
* Pass `none` to indicate the pagination is unique to this publication.
|
|
858
|
-
*
|
|
859
|
-
* @link https://www.w3.org/TR/epub/#pageBreakSource
|
|
860
|
-
*/
|
|
861
|
-
async setPageBreakSource(value) {
|
|
862
|
-
if (value === null) {
|
|
863
|
-
await this.removeMetadata(
|
|
864
|
-
(item) => item.properties["property"] === "pageBreakSource"
|
|
865
|
-
);
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
await this.replaceMetadata(
|
|
869
|
-
(item) => item.properties["property"] === "pageBreakSource",
|
|
870
|
-
{ type: "meta", properties: { property: "pageBreakSource" }, value }
|
|
871
|
-
);
|
|
872
|
-
}
|
|
873
|
-
/**
|
|
874
|
-
* Replace the publication's `dc:identifier` entries.
|
|
875
|
-
*
|
|
876
|
-
* This replaces ALL existing `dc:identifier` elements except the publication's
|
|
877
|
-
* unique identifier (the one referenced by the package element's
|
|
878
|
-
* `unique-identifier` attribute), which is always preserved and must not be
|
|
879
|
-
* included in the provided list. If included anyway, it is ignored. See
|
|
880
|
-
* {@link setUniqueIdentifier} to change it.
|
|
881
|
-
*
|
|
882
|
-
* Identifiers are placed in the order they are provided.
|
|
883
|
-
*
|
|
884
|
-
* `dc:source` entries are not touched, use {@link setSources} for those.
|
|
885
|
-
*
|
|
886
|
-
* When an entry has an `identifierType`, it is written in the refining form
|
|
887
|
-
* (a `meta` with `property="identifier-type"`, carrying the `scheme`
|
|
888
|
-
* attribute when provided). An entry with only a `scheme` is written using
|
|
889
|
-
* the legacy `opf:scheme` attribute.
|
|
890
|
-
*
|
|
891
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dcidentifier
|
|
892
|
-
*/
|
|
893
|
-
async setIdentifiers(identifiers) {
|
|
894
|
-
const uniqueId = await this.getUniqueIdentifierId();
|
|
895
|
-
await this.withPackage((packageElement) => {
|
|
896
|
-
var _a;
|
|
897
|
-
const metadata = Epub.findXmlChildByName(
|
|
898
|
-
"metadata",
|
|
899
|
-
Epub.getXmlChildren(packageElement)
|
|
900
|
-
);
|
|
901
|
-
if (!metadata)
|
|
902
|
-
throw new Error(
|
|
903
|
-
"Failed to parse EPUB: found no metadata element in package document"
|
|
904
|
-
);
|
|
905
|
-
const children = Epub.getXmlChildren(metadata);
|
|
906
|
-
const removedIds = /* @__PURE__ */ new Set();
|
|
907
|
-
for (let i = children.length - 1; i >= 0; i--) {
|
|
908
|
-
const node = children[i];
|
|
909
|
-
if (Epub.isXmlTextNode(node) || Epub.getXmlElementName(node) !== "dc:identifier") {
|
|
910
|
-
continue;
|
|
911
|
-
}
|
|
912
|
-
const id = (_a = node[":@"]) == null ? void 0 : _a["@_id"];
|
|
913
|
-
if (id && id === uniqueId) {
|
|
914
|
-
continue;
|
|
915
|
-
}
|
|
916
|
-
if (id) {
|
|
917
|
-
removedIds.add(id);
|
|
918
|
-
}
|
|
919
|
-
children.splice(i, 1);
|
|
920
|
-
}
|
|
921
|
-
Epub.removeRefiningMetas(children, removedIds, ["identifier-type"]);
|
|
922
|
-
for (const identifier of identifiers) {
|
|
923
|
-
if (identifier.id && identifier.id === uniqueId) continue;
|
|
924
|
-
const id = identifier.id ?? (identifier.identifierType !== void 0 ? (0, import_nanoid.nanoid)() : void 0);
|
|
925
|
-
children.push(
|
|
926
|
-
Epub.createXmlElement(
|
|
927
|
-
"dc:identifier",
|
|
928
|
-
{
|
|
929
|
-
...id && { id },
|
|
930
|
-
...identifier.scheme && identifier.identifierType === void 0 && {
|
|
931
|
-
"opf:scheme": identifier.scheme
|
|
932
|
-
}
|
|
933
|
-
},
|
|
934
|
-
[Epub.createXmlTextNode(identifier.value)]
|
|
935
|
-
)
|
|
936
|
-
);
|
|
937
|
-
if (identifier.identifierType !== void 0 && id) {
|
|
938
|
-
children.push(
|
|
939
|
-
Epub.createXmlElement(
|
|
940
|
-
"meta",
|
|
941
|
-
{
|
|
942
|
-
refines: `#${id}`,
|
|
943
|
-
property: "identifier-type",
|
|
944
|
-
...identifier.scheme && { scheme: identifier.scheme }
|
|
945
|
-
},
|
|
946
|
-
[Epub.createXmlTextNode(identifier.identifierType)]
|
|
947
|
-
)
|
|
948
|
-
);
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
});
|
|
952
|
-
}
|
|
953
|
-
/**
|
|
954
|
-
* Remove `meta` refinements pointing at any of the given ids,
|
|
955
|
-
* restricted to the given `property` values as a cleanup step
|
|
956
|
-
* Mutates `children` in place.
|
|
957
|
-
*/
|
|
958
|
-
static removeRefiningMetas(children, ids, properties) {
|
|
959
|
-
var _a, _b, _c;
|
|
960
|
-
for (let i = children.length - 1; i >= 0; i--) {
|
|
961
|
-
const node = children[i];
|
|
962
|
-
if (Epub.isXmlTextNode(node) || Epub.getXmlElementName(node) !== "meta") {
|
|
963
|
-
continue;
|
|
964
|
-
}
|
|
965
|
-
const property = (_a = node[":@"]) == null ? void 0 : _a["@_property"];
|
|
966
|
-
if (!property || !properties.includes(property)) continue;
|
|
967
|
-
const refines = (_c = (_b = node[":@"]) == null ? void 0 : _b["@_refines"]) == null ? void 0 : _c.slice(1);
|
|
968
|
-
if (refines && ids.has(refines)) {
|
|
969
|
-
children.splice(i, 1);
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
/**
|
|
974
|
-
* Replace the publication's `dc:source` entries.
|
|
975
|
-
*
|
|
976
|
-
* This replaces ALL existing `dc:source` elements (and their refining
|
|
977
|
-
* `identifier-type` / `source-of` metas). `dc:identifier` entries are not
|
|
978
|
-
* touched; use {@link setIdentifiers} for those. Pass an empty array to
|
|
979
|
-
* remove all sources.
|
|
980
|
-
*
|
|
981
|
-
* When an entry has an `identifierType`, it is written in the refining form.
|
|
982
|
-
* A `sourceOf` value is written as a refining `source-of` meta (spec D.3.11).
|
|
983
|
-
* An entry with only a `scheme` uses the legacy `opf:scheme` attribute.
|
|
984
|
-
*
|
|
985
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dcsource
|
|
986
|
-
*/
|
|
987
|
-
async setSources(sources) {
|
|
988
|
-
await this.withPackage((packageElement) => {
|
|
989
|
-
var _a;
|
|
990
|
-
const metadata = Epub.findXmlChildByName(
|
|
991
|
-
"metadata",
|
|
992
|
-
Epub.getXmlChildren(packageElement)
|
|
993
|
-
);
|
|
994
|
-
if (!metadata)
|
|
995
|
-
throw new Error(
|
|
996
|
-
"Failed to parse EPUB: found no metadata element in package document"
|
|
997
|
-
);
|
|
998
|
-
const children = Epub.getXmlChildren(metadata);
|
|
999
|
-
const removedIds = /* @__PURE__ */ new Set();
|
|
1000
|
-
for (let i = children.length - 1; i >= 0; i--) {
|
|
1001
|
-
const node = children[i];
|
|
1002
|
-
if (Epub.isXmlTextNode(node) || Epub.getXmlElementName(node) !== "dc:source") {
|
|
1003
|
-
continue;
|
|
1004
|
-
}
|
|
1005
|
-
const id = (_a = node[":@"]) == null ? void 0 : _a["@_id"];
|
|
1006
|
-
if (id) {
|
|
1007
|
-
removedIds.add(id);
|
|
1008
|
-
}
|
|
1009
|
-
children.splice(i, 1);
|
|
1010
|
-
}
|
|
1011
|
-
Epub.removeRefiningMetas(children, removedIds, [
|
|
1012
|
-
"identifier-type",
|
|
1013
|
-
"source-of"
|
|
1014
|
-
]);
|
|
1015
|
-
for (const source of sources) {
|
|
1016
|
-
const needsId = source.identifierType !== void 0 || source.isPageBreakSource;
|
|
1017
|
-
const id = source.id ?? (needsId ? (0, import_nanoid.nanoid)() : void 0);
|
|
1018
|
-
children.push(
|
|
1019
|
-
Epub.createXmlElement(
|
|
1020
|
-
"dc:source",
|
|
1021
|
-
{
|
|
1022
|
-
...id && { id },
|
|
1023
|
-
...source.scheme && source.identifierType === void 0 && {
|
|
1024
|
-
"opf:scheme": source.scheme
|
|
1025
|
-
}
|
|
1026
|
-
},
|
|
1027
|
-
[Epub.createXmlTextNode(source.value)]
|
|
1028
|
-
)
|
|
1029
|
-
);
|
|
1030
|
-
if (source.identifierType !== void 0 && id) {
|
|
1031
|
-
children.push(
|
|
1032
|
-
Epub.createXmlElement(
|
|
1033
|
-
"meta",
|
|
1034
|
-
{
|
|
1035
|
-
refines: `#${id}`,
|
|
1036
|
-
property: "identifier-type",
|
|
1037
|
-
...source.scheme && { scheme: source.scheme }
|
|
1038
|
-
},
|
|
1039
|
-
[Epub.createXmlTextNode(source.identifierType)]
|
|
1040
|
-
)
|
|
1041
|
-
);
|
|
1042
|
-
}
|
|
1043
|
-
if (source.isPageBreakSource && id) {
|
|
1044
|
-
children.push(
|
|
1045
|
-
Epub.createXmlElement(
|
|
1046
|
-
"meta",
|
|
1047
|
-
{ refines: `#${id}`, property: "source-of" },
|
|
1048
|
-
[Epub.createXmlTextNode("pagination")]
|
|
1049
|
-
)
|
|
1050
|
-
);
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
});
|
|
1054
|
-
}
|
|
1055
|
-
/**
|
|
1056
|
-
* Even "EPUB 3" publications sometimes still only use the
|
|
1057
|
-
* EPUB 2 specification for identifying the cover image.
|
|
1058
|
-
* This is a private method that is used as a fallback if
|
|
1059
|
-
* we fail to find the cover image according to the EPUB 3
|
|
1060
|
-
* spec.
|
|
1061
|
-
*/
|
|
1062
|
-
async getEpub2CoverImage() {
|
|
1063
|
-
var _a;
|
|
1064
|
-
const packageElement = await this.getPackageElement();
|
|
1065
|
-
const metadataElement = Epub.findXmlChildByName(
|
|
1066
|
-
"metadata",
|
|
1067
|
-
Epub.getXmlChildren(packageElement)
|
|
1068
|
-
);
|
|
1069
|
-
if (!metadataElement)
|
|
1070
|
-
throw new Error(
|
|
1071
|
-
"Failed to parse EPUB: Found no metadata element in package document"
|
|
1072
|
-
);
|
|
1073
|
-
const coverImageElement = Epub.getXmlChildren(metadataElement).find(
|
|
1074
|
-
(node) => {
|
|
1075
|
-
var _a2;
|
|
1076
|
-
return !Epub.isXmlTextNode(node) && ((_a2 = node[":@"]) == null ? void 0 : _a2["@_name"]) === "cover";
|
|
1077
|
-
}
|
|
1078
|
-
);
|
|
1079
|
-
const manifestItemId = (_a = coverImageElement == null ? void 0 : coverImageElement[":@"]) == null ? void 0 : _a["@_content"];
|
|
1080
|
-
if (!manifestItemId) return null;
|
|
1081
|
-
const manifest = await this.getManifest();
|
|
1082
|
-
return Object.values(manifest).find((item) => item.id === manifestItemId) ?? null;
|
|
1083
|
-
}
|
|
1084
|
-
/**
|
|
1085
|
-
* Retrieve the cover image manifest item.
|
|
1086
|
-
*
|
|
1087
|
-
* This does not return the actual image data. To
|
|
1088
|
-
* retrieve the image data, pass this item's id to
|
|
1089
|
-
* epub.readItemContents, or use epub.getCoverImage()
|
|
1090
|
-
* instead.
|
|
1091
|
-
*
|
|
1092
|
-
* @link https://www.w3.org/TR/epub-33/#sec-cover-image
|
|
1093
|
-
*/
|
|
1094
|
-
async getCoverImageItem() {
|
|
1095
|
-
const manifest = await this.getManifest();
|
|
1096
|
-
const coverImage = Object.values(manifest).find(
|
|
1097
|
-
(item) => {
|
|
1098
|
-
var _a;
|
|
1099
|
-
return (_a = item.properties) == null ? void 0 : _a.includes("cover-image");
|
|
1100
|
-
}
|
|
1101
|
-
);
|
|
1102
|
-
if (coverImage) return coverImage;
|
|
1103
|
-
return this.getEpub2CoverImage();
|
|
1104
|
-
}
|
|
1105
|
-
/**
|
|
1106
|
-
* Retrieve the cover image data as a byte array.
|
|
1107
|
-
*
|
|
1108
|
-
* This does not include, for example, the cover image's
|
|
1109
|
-
* filename or mime type. To retrieve the image manifest
|
|
1110
|
-
* item, use epub.getCoverImageItem().
|
|
1111
|
-
*
|
|
1112
|
-
* @link https://www.w3.org/TR/epub-33/#sec-cover-image
|
|
1113
|
-
*/
|
|
1114
|
-
async getCoverImage() {
|
|
1115
|
-
const coverImageItem = await this.getCoverImageItem();
|
|
1116
|
-
if (!coverImageItem) return coverImageItem;
|
|
1117
|
-
return this.readItemContents(coverImageItem.id);
|
|
1118
|
-
}
|
|
1119
|
-
/**
|
|
1120
|
-
* Set the cover image for the EPUB.
|
|
1121
|
-
*
|
|
1122
|
-
* Adds a manifest item with the `cover-image` property, per
|
|
1123
|
-
* the EPUB 3 spec, and then writes the provided image data to
|
|
1124
|
-
* the provided href within the publication.
|
|
1125
|
-
*/
|
|
1126
|
-
async setCoverImage(href, data) {
|
|
1127
|
-
var _a;
|
|
1128
|
-
const coverImageItem = await this.getCoverImageItem();
|
|
1129
|
-
if (coverImageItem) {
|
|
1130
|
-
await this.removeManifestItem(coverImageItem.id);
|
|
1131
|
-
}
|
|
1132
|
-
const mediaType = (_a = import_media_types.MediaType.fromPath(href)) == null ? void 0 : _a.mime;
|
|
1133
|
-
if (!mediaType)
|
|
1134
|
-
throw new Error(`Invalid file extension for cover image: ${href}`);
|
|
1135
|
-
await this.addManifestItem(
|
|
1136
|
-
{ id: "cover-image", href, mediaType, properties: ["cover-image"] },
|
|
1137
|
-
data
|
|
1138
|
-
);
|
|
1139
|
-
}
|
|
1140
|
-
/**
|
|
1141
|
-
* Retrieve the publication date from the dc:date element
|
|
1142
|
-
* in the EPUB metadata as a Date object.
|
|
1143
|
-
*
|
|
1144
|
-
* If there is no dc:date element, returns null.
|
|
1145
|
-
*
|
|
1146
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dcdate
|
|
1147
|
-
*/
|
|
1148
|
-
async getPublicationDate() {
|
|
1149
|
-
const metadata = await this.getMetadata();
|
|
1150
|
-
const entry = metadata.find(({ type }) => type === "dc:date");
|
|
1151
|
-
if (!(entry == null ? void 0 : entry.value)) return null;
|
|
1152
|
-
return new Date(entry.value);
|
|
1153
|
-
}
|
|
1154
|
-
/**
|
|
1155
|
-
* Set the dc:date metadata element with the provided date.
|
|
1156
|
-
*
|
|
1157
|
-
* Updates the existing dc:date element if one exists.
|
|
1158
|
-
* Otherwise creates a new element
|
|
1159
|
-
*
|
|
1160
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dcdate
|
|
1161
|
-
*/
|
|
1162
|
-
async setPublicationDate(date) {
|
|
1163
|
-
await this.replaceMetadata(({ type }) => type === "dc:date", {
|
|
1164
|
-
type: "dc:date",
|
|
1165
|
-
properties: {},
|
|
1166
|
-
value: date.toISOString()
|
|
1167
|
-
});
|
|
1168
|
-
}
|
|
1169
|
-
/**
|
|
1170
|
-
* Retrieve the modified date from the dcterms:modified metadata
|
|
1171
|
-
* in the EPUB metadata as a Date object.
|
|
1172
|
-
*
|
|
1173
|
-
* If there is no meta element with dcterms:modified, returns null.
|
|
1174
|
-
*
|
|
1175
|
-
* @link https://www.w3.org/TR/epub-33/#sec-metadata-last-modified
|
|
1176
|
-
*/
|
|
1177
|
-
async getModifiedDate() {
|
|
1178
|
-
const metadata = await this.getMetadata();
|
|
1179
|
-
const entry = metadata.find(
|
|
1180
|
-
({ properties }) => properties["property"] === "dcterms:modified"
|
|
1181
|
-
);
|
|
1182
|
-
if (!(entry == null ? void 0 : entry.value)) return null;
|
|
1183
|
-
return new Date(entry.value);
|
|
1184
|
-
}
|
|
1185
|
-
/**
|
|
1186
|
-
* Retrieve the layout from the rendition:layout meta element
|
|
1187
|
-
* in the EPUB metadata.
|
|
1188
|
-
*
|
|
1189
|
-
* If there is no meta element, returns 'reflowable'.
|
|
1190
|
-
*
|
|
1191
|
-
* @link https://www.w3.org/TR/epub-33/#layout
|
|
1192
|
-
*/
|
|
1193
|
-
async getLayout() {
|
|
1194
|
-
const metadata = await this.getMetadata();
|
|
1195
|
-
const entry = metadata.find(
|
|
1196
|
-
({ properties }) => properties["property"] === "rendition:layout"
|
|
1197
|
-
);
|
|
1198
|
-
if ((entry == null ? void 0 : entry.value) !== "reflowable" && (entry == null ? void 0 : entry.value) !== "pre-paginated") {
|
|
1199
|
-
return "reflowable";
|
|
1200
|
-
}
|
|
1201
|
-
return entry.value;
|
|
1202
|
-
}
|
|
1203
|
-
/**
|
|
1204
|
-
* Retrieve the base direction from the package element.
|
|
1205
|
-
*
|
|
1206
|
-
* If there is no `dir` attribute on the package element,
|
|
1207
|
-
* returns 'auto'.
|
|
1208
|
-
*
|
|
1209
|
-
* @link https://www.w3.org/TR/epub-33/#attrdef-dir
|
|
1210
|
-
*/
|
|
1211
|
-
async getBaseDirection() {
|
|
1212
|
-
var _a;
|
|
1213
|
-
const packageEl = await this.getPackageElement();
|
|
1214
|
-
const dir = (_a = packageEl[":@"]) == null ? void 0 : _a["@_dir"];
|
|
1215
|
-
if (dir !== "ltr" && dir !== "rtl" && dir !== "auto") {
|
|
1216
|
-
return "auto";
|
|
1217
|
-
}
|
|
1218
|
-
return dir;
|
|
1219
|
-
}
|
|
1220
|
-
/**
|
|
1221
|
-
* Set the dc:type metadata element.
|
|
1222
|
-
*
|
|
1223
|
-
* Updates the existing dc:type element if one exists.
|
|
1224
|
-
* Otherwise creates a new element.
|
|
1225
|
-
*
|
|
1226
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dctype
|
|
1227
|
-
*/
|
|
1228
|
-
async setType(type) {
|
|
1229
|
-
await this.replaceMetadata(({ type: type2 }) => type2 === "dc:type", {
|
|
1230
|
-
type: "dc:type",
|
|
1231
|
-
properties: {},
|
|
1232
|
-
value: type
|
|
1233
|
-
});
|
|
1234
|
-
}
|
|
1235
|
-
/**
|
|
1236
|
-
* Retrieve the publication type from the dc:type element
|
|
1237
|
-
* in the EPUB metadata.
|
|
1238
|
-
*
|
|
1239
|
-
* If there is no dc:type element, returns null.
|
|
1240
|
-
*
|
|
1241
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dctype
|
|
1242
|
-
*/
|
|
1243
|
-
async getType() {
|
|
1244
|
-
const metadata = await this.getMetadata();
|
|
1245
|
-
return metadata.find(({ type }) => type === "dc:type") ?? null;
|
|
1246
|
-
}
|
|
1247
|
-
/**
|
|
1248
|
-
* Add a subject to the EPUB metadata.
|
|
1249
|
-
*
|
|
1250
|
-
* @param subject May be a string representing just a schema-less
|
|
1251
|
-
* subject name, or a DcSubject object
|
|
1252
|
-
*
|
|
1253
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dcsubject
|
|
1254
|
-
*/
|
|
1255
|
-
async addSubject(subject) {
|
|
1256
|
-
const subjectEntry = typeof subject === "string" ? {
|
|
1257
|
-
value: subject
|
|
1258
|
-
} : subject;
|
|
1259
|
-
const subjectId = (0, import_nanoid.nanoid)();
|
|
1260
|
-
await this.addMetadata({
|
|
1261
|
-
id: subjectId,
|
|
1262
|
-
type: "dc:subject",
|
|
1263
|
-
properties: {},
|
|
1264
|
-
value: subjectEntry.value
|
|
1265
|
-
});
|
|
1266
|
-
if ("authority" in subjectEntry) {
|
|
1267
|
-
await this.addMetadata({
|
|
1268
|
-
type: "meta",
|
|
1269
|
-
properties: { refines: `#${subjectId}`, property: "authority" },
|
|
1270
|
-
value: subjectEntry.authority
|
|
1271
|
-
});
|
|
1272
|
-
await this.addMetadata({
|
|
1273
|
-
type: "meta",
|
|
1274
|
-
properties: { refines: `#${subjectId}`, property: "term" },
|
|
1275
|
-
value: subjectEntry.term
|
|
1276
|
-
});
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
/**
|
|
1280
|
-
* Remove a subject from the EPUB metadata.
|
|
1281
|
-
*
|
|
1282
|
-
* Removes the subject at the provided index. This index
|
|
1283
|
-
* refers to the array returned by `epub.getSubjects()`.
|
|
1284
|
-
*
|
|
1285
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dccreator
|
|
1286
|
-
*/
|
|
1287
|
-
async removeSubject(index) {
|
|
1288
|
-
await this.withPackage((packageElement) => {
|
|
1289
|
-
const metadata = Epub.findXmlChildByName(
|
|
1290
|
-
"metadata",
|
|
1291
|
-
Epub.getXmlChildren(packageElement)
|
|
1292
|
-
);
|
|
1293
|
-
if (!metadata)
|
|
1294
|
-
throw new Error(
|
|
1295
|
-
"Failed to parse EPUB: found no metadata element in package document"
|
|
1296
|
-
);
|
|
1297
|
-
let subjectCount = null;
|
|
1298
|
-
let metadataIndex = null;
|
|
1299
|
-
for (const meta of Epub.getXmlChildren(metadata)) {
|
|
1300
|
-
if (subjectCount === index) break;
|
|
1301
|
-
metadataIndex = metadataIndex === null ? 0 : metadataIndex + 1;
|
|
1302
|
-
if (Epub.isXmlTextNode(meta)) continue;
|
|
1303
|
-
if (Epub.getXmlElementName(meta) !== "dc:subject") continue;
|
|
1304
|
-
subjectCount = subjectCount === null ? 0 : subjectCount + 1;
|
|
1305
|
-
}
|
|
1306
|
-
if (subjectCount === null || metadataIndex === null) return;
|
|
1307
|
-
Epub.getXmlChildren(metadata).splice(metadataIndex, 1);
|
|
1308
|
-
});
|
|
1309
|
-
}
|
|
1310
|
-
/**
|
|
1311
|
-
* Retrieve the list of subjects for this EPUB.
|
|
1312
|
-
*
|
|
1313
|
-
* Subjects without associated authority and term metadata
|
|
1314
|
-
* will be returned as strings. Otherwise, they will
|
|
1315
|
-
* be represented as DcSubject objects, with a value,
|
|
1316
|
-
* authority, and term.
|
|
1317
|
-
*
|
|
1318
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dcsubject
|
|
1319
|
-
*/
|
|
1320
|
-
async getSubjects() {
|
|
1321
|
-
const metadata = await this.getMetadata();
|
|
1322
|
-
const subjectEntries = metadata.filter(({ type }) => type === "dc:subject");
|
|
1323
|
-
const subjects = subjectEntries.map(({ value }) => value).filter((value) => !!value);
|
|
1324
|
-
metadata.forEach((entry) => {
|
|
1325
|
-
if (entry.type !== "meta" || entry.properties["property"] !== "term" && entry.properties["property"] !== "authority") {
|
|
1326
|
-
return;
|
|
1327
|
-
}
|
|
1328
|
-
const subjectIdref = entry.properties["refines"];
|
|
1329
|
-
if (!subjectIdref) return;
|
|
1330
|
-
const subjectId = subjectIdref.slice(1);
|
|
1331
|
-
const index = subjectEntries.findIndex((entry2) => entry2.id === subjectId);
|
|
1332
|
-
if (index === -1) return;
|
|
1333
|
-
const subject = typeof subjects[index] === "string" ? { value: subjects[index], authority: void 0, term: void 0 } : (
|
|
1334
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1335
|
-
subjects[index]
|
|
1336
|
-
);
|
|
1337
|
-
subject[entry.properties["property"]] = entry.value;
|
|
1338
|
-
subjects.splice(index, 1, subject);
|
|
1339
|
-
});
|
|
1340
|
-
return subjects;
|
|
1341
|
-
}
|
|
1342
|
-
/**
|
|
1343
|
-
* Retrieve the Epub's language as specified in its
|
|
1344
|
-
* package document metadata.
|
|
1345
|
-
*
|
|
1346
|
-
* If no language metadata is specified, returns null.
|
|
1347
|
-
* Returns the language as an Intl.Locale instance.
|
|
1348
|
-
*
|
|
1349
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dclanguage
|
|
1350
|
-
*/
|
|
1351
|
-
async getLanguage() {
|
|
1352
|
-
const metadata = await this.getMetadata();
|
|
1353
|
-
const languageEntries = metadata.filter(
|
|
1354
|
-
(entry) => entry.type === "dc:language"
|
|
1355
|
-
);
|
|
1356
|
-
const primaryLanguage = languageEntries[0];
|
|
1357
|
-
if (!primaryLanguage) return null;
|
|
1358
|
-
const locale = primaryLanguage.value;
|
|
1359
|
-
if (!locale || locale.toLowerCase() === "und") return null;
|
|
1360
|
-
try {
|
|
1361
|
-
return new Intl.Locale(locale);
|
|
1362
|
-
} catch {
|
|
1363
|
-
return null;
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
/**
|
|
1367
|
-
* Update the Epub's language metadata entry.
|
|
1368
|
-
*
|
|
1369
|
-
* Updates the existing dc:language element if one exists.
|
|
1370
|
-
* Otherwise creates a new element
|
|
1371
|
-
*
|
|
1372
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dclanguage
|
|
1373
|
-
*/
|
|
1374
|
-
async setLanguage(locale) {
|
|
1375
|
-
await this.replaceMetadata(({ type }) => type === "dc:language", {
|
|
1376
|
-
type: "dc:language",
|
|
1377
|
-
properties: {},
|
|
1378
|
-
value: locale.toString()
|
|
1379
|
-
});
|
|
1380
|
-
}
|
|
1381
|
-
/**
|
|
1382
|
-
* Retrieve the title of the Epub.
|
|
1383
|
-
*
|
|
1384
|
-
* @param main Optional - whether to return only the first title segment
|
|
1385
|
-
* if multiple are found. Otherwise, will follow the spec to combine title
|
|
1386
|
-
* segments
|
|
1387
|
-
*
|
|
1388
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dctitle
|
|
1389
|
-
*/
|
|
1390
|
-
async getTitle(expanded = false) {
|
|
1391
|
-
var _a;
|
|
1392
|
-
const entries = await this.getTitles();
|
|
1393
|
-
if (!expanded) {
|
|
1394
|
-
const mainEntry = entries.find((entry) => entry.type === "main");
|
|
1395
|
-
if (mainEntry) return mainEntry.title;
|
|
1396
|
-
const shortEntry = entries.find((entry) => entry.type === "short");
|
|
1397
|
-
if (shortEntry) return shortEntry.title;
|
|
1398
|
-
return ((_a = entries[0]) == null ? void 0 : _a.title) ?? null;
|
|
1399
|
-
}
|
|
1400
|
-
const expandedEntry = entries.find((entry) => entry.type === "expanded");
|
|
1401
|
-
if (expandedEntry) return expandedEntry.title;
|
|
1402
|
-
return entries.map((entry) => entry.title).join(", ");
|
|
1403
|
-
}
|
|
1404
|
-
/**
|
|
1405
|
-
* Retrieve the subtitle of the Epub, if it exists.
|
|
1406
|
-
*
|
|
1407
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dctitle
|
|
1408
|
-
*/
|
|
1409
|
-
async getSubtitle() {
|
|
1410
|
-
const entries = await this.getTitles();
|
|
1411
|
-
const subtitleEntry = entries.find((entry) => entry.type === "subtitle");
|
|
1412
|
-
return (subtitleEntry == null ? void 0 : subtitleEntry.title) ?? null;
|
|
1413
|
-
}
|
|
1414
|
-
/**
|
|
1415
|
-
* Retrieve all title entries of the Epub.
|
|
1416
|
-
*
|
|
1417
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dctitle
|
|
1418
|
-
*/
|
|
1419
|
-
async getTitles() {
|
|
1420
|
-
const metadata = await this.getMetadata();
|
|
1421
|
-
const titleEntries = metadata.filter((entry) => entry.type === "dc:title");
|
|
1422
|
-
const titleRefinements = metadata.filter(
|
|
1423
|
-
(entry) => entry.type === "meta" && entry.properties["refines"] && (entry.properties["property"] === "title-type" || entry.properties["property"] === "display-seq")
|
|
1424
|
-
);
|
|
1425
|
-
const sortedTitleParts = titleEntries.filter(
|
|
1426
|
-
(titleEntry) => titleEntry.id && titleRefinements.some(
|
|
1427
|
-
(entry) => {
|
|
1428
|
-
var _a;
|
|
1429
|
-
return entry.value && ((_a = entry.properties["refines"]) == null ? void 0 : _a.slice(1)) === titleEntry.id && entry.properties["property"] === "display-seq" && !Number.isNaN(parseInt(entry.value, 10));
|
|
1430
|
-
}
|
|
1431
|
-
)
|
|
1432
|
-
).sort((a, b) => {
|
|
1433
|
-
const refinementA = titleRefinements.find(
|
|
1434
|
-
(entry) => entry.properties["property"] === "display-seq" && entry.properties["refines"].slice(1) === a.id
|
|
1435
|
-
);
|
|
1436
|
-
const refinementB = titleRefinements.find(
|
|
1437
|
-
(entry) => entry.properties["property"] === "display-seq" && entry.properties["refines"].slice(1) === b.id
|
|
1438
|
-
);
|
|
1439
|
-
const sortA = parseInt(refinementA.value, 10);
|
|
1440
|
-
const sortB = parseInt(refinementB.value, 10);
|
|
1441
|
-
return sortA - sortB;
|
|
1442
|
-
});
|
|
1443
|
-
return (sortedTitleParts.length === 0 ? titleEntries : sortedTitleParts).map((entry) => {
|
|
1444
|
-
const titleType = titleRefinements.find(
|
|
1445
|
-
(refinement) => {
|
|
1446
|
-
var _a;
|
|
1447
|
-
return ((_a = refinement.properties["refines"]) == null ? void 0 : _a.slice(1)) === entry.id && refinement.properties["property"] === "title-type";
|
|
1448
|
-
}
|
|
1449
|
-
);
|
|
1450
|
-
return {
|
|
1451
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1452
|
-
title: entry.value,
|
|
1453
|
-
type: (titleType == null ? void 0 : titleType.value) ?? null
|
|
1454
|
-
};
|
|
1455
|
-
});
|
|
1456
|
-
}
|
|
1457
|
-
/**
|
|
1458
|
-
* Update the Epub's description metadata entry.
|
|
1459
|
-
*
|
|
1460
|
-
* Updates the existing dc:description element if one exists.
|
|
1461
|
-
* Otherwise creates a new element. Any non-ASCII symbols,
|
|
1462
|
-
* `&`, `<`, `>`, `"`, `'`, and `\``` will be encoded as HTML entities.
|
|
1463
|
-
*/
|
|
1464
|
-
async setDescription(description) {
|
|
1465
|
-
await this.replaceMetadata(({ type }) => type === "dc:description", {
|
|
1466
|
-
type: "dc:description",
|
|
1467
|
-
value: description,
|
|
1468
|
-
properties: {}
|
|
1469
|
-
});
|
|
1470
|
-
}
|
|
1471
|
-
/**
|
|
1472
|
-
* Retrieve the Epub's description as specified in its
|
|
1473
|
-
* package document metadata.
|
|
1474
|
-
*
|
|
1475
|
-
* If no description metadata is specified, returns null.
|
|
1476
|
-
* Returns the description as a string. Descriptions may
|
|
1477
|
-
* include HTML markup.
|
|
1478
|
-
*/
|
|
1479
|
-
async getDescription() {
|
|
1480
|
-
const metadata = await this.getMetadata();
|
|
1481
|
-
const descriptionEntry = metadata.find(
|
|
1482
|
-
(entry) => entry.type === "dc:description"
|
|
1483
|
-
);
|
|
1484
|
-
if (!(descriptionEntry == null ? void 0 : descriptionEntry.value)) return null;
|
|
1485
|
-
return descriptionEntry.value;
|
|
1486
|
-
}
|
|
1487
|
-
/**
|
|
1488
|
-
* Return the set of custom vocabulary prefixes set on this publication's
|
|
1489
|
-
* root package element.
|
|
1490
|
-
*
|
|
1491
|
-
* Returns a map from prefix to URI
|
|
1492
|
-
*
|
|
1493
|
-
* @link https://www.w3.org/TR/epub-33/#sec-prefix-attr
|
|
1494
|
-
*/
|
|
1495
|
-
async getPackageVocabularyPrefixes() {
|
|
1496
|
-
var _a;
|
|
1497
|
-
const packageElement = await this.getPackageElement();
|
|
1498
|
-
const prefixValue = (_a = packageElement[":@"]) == null ? void 0 : _a["@_prefix"];
|
|
1499
|
-
if (!prefixValue) return {};
|
|
1500
|
-
const matches = prefixValue.matchAll(/(?:([a-z]+): +(\S+)\s*)/gs);
|
|
1501
|
-
return Array.from(matches).reduce(
|
|
1502
|
-
(acc, match) => match[1] && match[2] ? { ...acc, [match[1]]: match[2] } : acc,
|
|
1503
|
-
{}
|
|
1504
|
-
);
|
|
1505
|
-
}
|
|
1506
|
-
/**
|
|
1507
|
-
* Set a custom vocabulary prefix on the root package element.
|
|
1508
|
-
*
|
|
1509
|
-
* @link https://www.w3.org/TR/epub-33/#sec-prefix-attr
|
|
1510
|
-
*/
|
|
1511
|
-
async setPackageVocabularyPrefix(prefix, uri) {
|
|
1512
|
-
await this.withPackage(async (packageElement) => {
|
|
1513
|
-
const prefixes = await this.getPackageVocabularyPrefixes();
|
|
1514
|
-
prefixes[prefix] = uri;
|
|
1515
|
-
packageElement[":@"] ??= {};
|
|
1516
|
-
packageElement[":@"]["@_prefix"] = Object.entries(prefixes).map(([p, u]) => `${p}: ${u}`).join("\n ");
|
|
1517
|
-
});
|
|
1518
|
-
}
|
|
1519
|
-
/**
|
|
1520
|
-
* Set the title of the Epub.
|
|
1521
|
-
*
|
|
1522
|
-
* This will replace all existing dc:title elements with
|
|
1523
|
-
* this title. It will be given title-type "main".
|
|
1524
|
-
*
|
|
1525
|
-
* To set specific titles and their types, use epub.setTitles().
|
|
1526
|
-
*
|
|
1527
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dctitle
|
|
1528
|
-
*/
|
|
1529
|
-
// TODO: This should allow users to optionally specify an array,
|
|
1530
|
-
// rather than a single string, to support expanded titles.
|
|
1531
|
-
async setTitle(title) {
|
|
1532
|
-
await this.withPackage((packageElement) => {
|
|
1533
|
-
const metadata = Epub.findXmlChildByName(
|
|
1534
|
-
"metadata",
|
|
1535
|
-
Epub.getXmlChildren(packageElement)
|
|
1536
|
-
);
|
|
1537
|
-
if (!metadata)
|
|
1538
|
-
throw new Error(
|
|
1539
|
-
"Failed to parse EPUB: found no metadata element in package document"
|
|
1540
|
-
);
|
|
1541
|
-
const titleElement = Epub.findXmlChildByName(
|
|
1542
|
-
"dc:title",
|
|
1543
|
-
metadata.metadata
|
|
1544
|
-
);
|
|
1545
|
-
if (!titleElement) {
|
|
1546
|
-
Epub.getXmlChildren(metadata).push(
|
|
1547
|
-
Epub.createXmlElement("dc:title", {}, [
|
|
1548
|
-
Epub.createXmlTextNode(title)
|
|
1549
|
-
])
|
|
1550
|
-
);
|
|
1551
|
-
} else {
|
|
1552
|
-
titleElement["dc:title"] = [Epub.createXmlTextNode(title)];
|
|
1553
|
-
}
|
|
1554
|
-
});
|
|
1555
|
-
}
|
|
1556
|
-
async setTitles(entries) {
|
|
1557
|
-
await this.withPackage((packageElement) => {
|
|
1558
|
-
var _a, _b;
|
|
1559
|
-
const metadata = Epub.findXmlChildByName(
|
|
1560
|
-
"metadata",
|
|
1561
|
-
Epub.getXmlChildren(packageElement)
|
|
1562
|
-
);
|
|
1563
|
-
if (!metadata) {
|
|
1564
|
-
throw new Error(
|
|
1565
|
-
"Failed to parse EPUB: found no metadata element in package document"
|
|
1566
|
-
);
|
|
1567
|
-
}
|
|
1568
|
-
const metadataEntries = Epub.getXmlChildren(metadata);
|
|
1569
|
-
for (let i = metadataEntries.length - 1; i >= 0; i--) {
|
|
1570
|
-
const meta = metadataEntries[i];
|
|
1571
|
-
if (Epub.isXmlTextNode(meta)) continue;
|
|
1572
|
-
if (Epub.getXmlElementName(meta) === "dc:title" || ((_a = meta[":@"]) == null ? void 0 : _a["@_property"]) === "title-type" || ((_b = meta[":@"]) == null ? void 0 : _b["@_property"]) === "display-seq") {
|
|
1573
|
-
metadataEntries.splice(i, 1);
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
for (let i = 0; i < entries.length; i++) {
|
|
1577
|
-
const entry = entries[i];
|
|
1578
|
-
const id = (0, import_nanoid.nanoid)();
|
|
1579
|
-
metadataEntries.push(
|
|
1580
|
-
Epub.createXmlElement("dc:title", { id }, [
|
|
1581
|
-
Epub.createXmlTextNode(entry.title)
|
|
1582
|
-
])
|
|
1583
|
-
);
|
|
1584
|
-
if (entry.type) {
|
|
1585
|
-
metadataEntries.push(
|
|
1586
|
-
Epub.createXmlElement(
|
|
1587
|
-
"meta",
|
|
1588
|
-
{ refines: `#${id}`, property: "title-type" },
|
|
1589
|
-
[Epub.createXmlTextNode(entry.type)]
|
|
1590
|
-
)
|
|
1591
|
-
);
|
|
1592
|
-
}
|
|
1593
|
-
metadataEntries.push(
|
|
1594
|
-
Epub.createXmlElement(
|
|
1595
|
-
"meta",
|
|
1596
|
-
{ refines: `#${id}`, property: "display-seq" },
|
|
1597
|
-
[Epub.createXmlTextNode((i + 1).toString())]
|
|
1598
|
-
)
|
|
1599
|
-
);
|
|
1600
|
-
}
|
|
1601
|
-
});
|
|
1602
|
-
}
|
|
1603
|
-
/**
|
|
1604
|
-
* Retrieve the list of collections.
|
|
1605
|
-
*/
|
|
1606
|
-
async getCollections() {
|
|
1607
|
-
var _a, _b;
|
|
1608
|
-
const metadata = await this.getMetadata();
|
|
1609
|
-
const collections = [];
|
|
1610
|
-
for (const entry of metadata) {
|
|
1611
|
-
if (entry.properties["property"] === "belongs-to-collection" && entry.value) {
|
|
1612
|
-
const type = (_a = metadata.find(
|
|
1613
|
-
(e) => e.properties["refines"] === `#${entry.id ?? ""}` && e.properties["property"] === "collection-type"
|
|
1614
|
-
)) == null ? void 0 : _a.value;
|
|
1615
|
-
const position = (_b = metadata.find(
|
|
1616
|
-
(e) => e.properties["refines"] === `#${entry.id ?? ""}` && e.properties["property"] === "group-position"
|
|
1617
|
-
)) == null ? void 0 : _b.value;
|
|
1618
|
-
collections.push({
|
|
1619
|
-
name: entry.value,
|
|
1620
|
-
...type && { type },
|
|
1621
|
-
...position && { position }
|
|
1622
|
-
});
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
return collections;
|
|
1626
|
-
}
|
|
1627
|
-
/**
|
|
1628
|
-
* Add a collection to the EPUB metadata.
|
|
1629
|
-
*
|
|
1630
|
-
* If index is provided, the collection will be placed at
|
|
1631
|
-
* that index in the list of collections. Otherwise, it
|
|
1632
|
-
* will be added to the end of the list.
|
|
1633
|
-
*/
|
|
1634
|
-
async addCollection(collection, index) {
|
|
1635
|
-
const collectionId = (0, import_nanoid.nanoid)();
|
|
1636
|
-
await this.withPackage((packageElement) => {
|
|
1637
|
-
var _a;
|
|
1638
|
-
const metadata = Epub.findXmlChildByName(
|
|
1639
|
-
"metadata",
|
|
1640
|
-
Epub.getXmlChildren(packageElement)
|
|
1641
|
-
);
|
|
1642
|
-
if (!metadata)
|
|
1643
|
-
throw new Error(
|
|
1644
|
-
"Failed to parse EPUB: found no metadata element in package document"
|
|
1645
|
-
);
|
|
1646
|
-
let collectionCount = 0;
|
|
1647
|
-
let metadataIndex = 0;
|
|
1648
|
-
for (const meta of Epub.getXmlChildren(metadata)) {
|
|
1649
|
-
if (collectionCount === index) break;
|
|
1650
|
-
metadataIndex++;
|
|
1651
|
-
if (Epub.isXmlTextNode(meta)) continue;
|
|
1652
|
-
if (Epub.getXmlElementName(meta) !== "meta") continue;
|
|
1653
|
-
if (((_a = meta[":@"]) == null ? void 0 : _a["@_property"]) !== "belongs-to-collection") continue;
|
|
1654
|
-
collectionCount++;
|
|
1655
|
-
}
|
|
1656
|
-
Epub.getXmlChildren(metadata).splice(
|
|
1657
|
-
metadataIndex,
|
|
1658
|
-
0,
|
|
1659
|
-
Epub.createXmlElement(
|
|
1660
|
-
"meta",
|
|
1661
|
-
{ id: collectionId, property: "belongs-to-collection" },
|
|
1662
|
-
[Epub.createXmlTextNode(collection.name)]
|
|
1663
|
-
)
|
|
1664
|
-
);
|
|
1665
|
-
});
|
|
1666
|
-
if (collection.position) {
|
|
1667
|
-
await this.addMetadata({
|
|
1668
|
-
type: "meta",
|
|
1669
|
-
properties: { refines: `#${collectionId}`, property: "group-position" },
|
|
1670
|
-
value: collection.position
|
|
1671
|
-
});
|
|
1672
|
-
}
|
|
1673
|
-
if (collection.type) {
|
|
1674
|
-
await this.addMetadata({
|
|
1675
|
-
type: "meta",
|
|
1676
|
-
properties: {
|
|
1677
|
-
refines: `#${collectionId}`,
|
|
1678
|
-
property: "collection-type"
|
|
1679
|
-
},
|
|
1680
|
-
value: collection.type
|
|
1681
|
-
});
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
/**
|
|
1685
|
-
* Remove a collection from the EPUB metadata.
|
|
1686
|
-
*
|
|
1687
|
-
* Removes the collection at the provided index. This index
|
|
1688
|
-
* refers to the array returned by `epub.getCollections()`.
|
|
1689
|
-
*/
|
|
1690
|
-
async removeCollection(index) {
|
|
1691
|
-
await this.withPackage((packageElement) => {
|
|
1692
|
-
var _a, _b;
|
|
1693
|
-
const metadata = Epub.findXmlChildByName(
|
|
1694
|
-
"metadata",
|
|
1695
|
-
Epub.getXmlChildren(packageElement)
|
|
1696
|
-
);
|
|
1697
|
-
if (!metadata)
|
|
1698
|
-
throw new Error(
|
|
1699
|
-
"Failed to parse EPUB: found no metadata element in package document"
|
|
1700
|
-
);
|
|
1701
|
-
let collectionCount = null;
|
|
1702
|
-
let metadataIndex = null;
|
|
1703
|
-
for (const meta of Epub.getXmlChildren(metadata)) {
|
|
1704
|
-
if (collectionCount === index) break;
|
|
1705
|
-
metadataIndex = metadataIndex === null ? 0 : metadataIndex + 1;
|
|
1706
|
-
if (Epub.isXmlTextNode(meta)) continue;
|
|
1707
|
-
if (Epub.getXmlElementName(meta) !== "meta") continue;
|
|
1708
|
-
if (((_a = meta[":@"]) == null ? void 0 : _a["@_property"]) !== "belongs-to-collection") continue;
|
|
1709
|
-
collectionCount = collectionCount === null ? 0 : collectionCount + 1;
|
|
1710
|
-
}
|
|
1711
|
-
if (collectionCount === null || metadataIndex === null) return;
|
|
1712
|
-
const [removed] = Epub.getXmlChildren(metadata).splice(metadataIndex, 1);
|
|
1713
|
-
if (removed && !Epub.isXmlTextNode(removed) && ((_b = removed[":@"]) == null ? void 0 : _b["@_id"])) {
|
|
1714
|
-
const id = removed[":@"]["@_id"];
|
|
1715
|
-
const newChildren = Epub.getXmlChildren(metadata).filter((node) => {
|
|
1716
|
-
var _a2;
|
|
1717
|
-
if (Epub.isXmlTextNode(node)) return true;
|
|
1718
|
-
if (Epub.getXmlElementName(node) !== "meta") return true;
|
|
1719
|
-
if (((_a2 = node[":@"]) == null ? void 0 : _a2["@_refines"]) !== `#${id}`) return true;
|
|
1720
|
-
return false;
|
|
1721
|
-
});
|
|
1722
|
-
Epub.replaceXmlChildren(metadata, newChildren);
|
|
1723
|
-
}
|
|
1724
|
-
});
|
|
1725
|
-
}
|
|
1726
|
-
/**
|
|
1727
|
-
* Retrieve the list of creators.
|
|
1728
|
-
*
|
|
1729
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dccreator
|
|
1730
|
-
*/
|
|
1731
|
-
async getCreators(type = "creator") {
|
|
1732
|
-
const metadata = await this.getMetadata();
|
|
1733
|
-
const creatorEntries = metadata.filter(
|
|
1734
|
-
(entry) => entry.type === `dc:${type}`
|
|
1735
|
-
);
|
|
1736
|
-
const creators = [];
|
|
1737
|
-
const creatorsById = /* @__PURE__ */ new Map();
|
|
1738
|
-
for (const entry of creatorEntries) {
|
|
1739
|
-
if (!entry.value) continue;
|
|
1740
|
-
const creator = { name: entry.value };
|
|
1741
|
-
creators.push(creator);
|
|
1742
|
-
if (entry.id) creatorsById.set(entry.id, creator);
|
|
1743
|
-
}
|
|
1744
|
-
metadata.forEach((entry) => {
|
|
1745
|
-
if (entry.type !== "meta" || entry.properties["property"] !== "file-as" && entry.properties["property"] !== "role" && entry.properties["property"] !== "alternate-script" || !entry.value) {
|
|
1746
|
-
return;
|
|
1747
|
-
}
|
|
1748
|
-
const creatorIdref = entry.properties["refines"];
|
|
1749
|
-
if (!creatorIdref) return;
|
|
1750
|
-
const creatorId = creatorIdref.slice(1);
|
|
1751
|
-
const creator = creatorsById.get(creatorId);
|
|
1752
|
-
if (!creator) return;
|
|
1753
|
-
if (entry.properties["alternate-script"]) {
|
|
1754
|
-
if (!entry.properties["xml:lang"]) return;
|
|
1755
|
-
creator.alternateScripts ??= [];
|
|
1756
|
-
creator.alternateScripts.push({
|
|
1757
|
-
name: entry.value,
|
|
1758
|
-
locale: new Intl.Locale(entry.properties["xml:lang"])
|
|
1759
|
-
});
|
|
1760
|
-
return;
|
|
1761
|
-
}
|
|
1762
|
-
const prop = entry.properties["property"] === "file-as" ? "fileAs" : "role";
|
|
1763
|
-
creator[prop] = entry.value;
|
|
1764
|
-
if (prop === "role" && entry.properties["scheme"]) {
|
|
1765
|
-
creator.roleScheme = entry.properties["scheme"];
|
|
1766
|
-
}
|
|
1767
|
-
});
|
|
1768
|
-
return creators;
|
|
1769
|
-
}
|
|
1770
|
-
/**
|
|
1771
|
-
* Retrieve the list of contributors.
|
|
1772
|
-
*
|
|
1773
|
-
* This is a convenience method for
|
|
1774
|
-
* `epub.getCreators('contributor')`.
|
|
1775
|
-
*
|
|
1776
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dccontributor
|
|
1777
|
-
*/
|
|
1778
|
-
getContributors() {
|
|
1779
|
-
return this.getCreators("contributor");
|
|
1780
|
-
}
|
|
1781
|
-
/**
|
|
1782
|
-
* Add a creator to the EPUB metadata.
|
|
1783
|
-
*
|
|
1784
|
-
* If index is provided, the creator will be placed at
|
|
1785
|
-
* that index in the list of creators. Otherwise, it
|
|
1786
|
-
* will be added to the end of the list.
|
|
1787
|
-
*
|
|
1788
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dccreator
|
|
1789
|
-
*/
|
|
1790
|
-
async addCreator(creator, index, type = "creator") {
|
|
1791
|
-
const creatorId = (0, import_nanoid.nanoid)();
|
|
1792
|
-
await this.withPackage((packageElement) => {
|
|
1793
|
-
const metadata = Epub.findXmlChildByName(
|
|
1794
|
-
"metadata",
|
|
1795
|
-
Epub.getXmlChildren(packageElement)
|
|
1796
|
-
);
|
|
1797
|
-
if (!metadata)
|
|
1798
|
-
throw new Error(
|
|
1799
|
-
"Failed to parse EPUB: found no metadata element in package document"
|
|
1800
|
-
);
|
|
1801
|
-
let creatorCount = 0;
|
|
1802
|
-
let metadataIndex = 0;
|
|
1803
|
-
for (const meta of Epub.getXmlChildren(metadata)) {
|
|
1804
|
-
if (creatorCount === index) break;
|
|
1805
|
-
metadataIndex++;
|
|
1806
|
-
if (Epub.isXmlTextNode(meta)) continue;
|
|
1807
|
-
if (Epub.getXmlElementName(meta) !== `dc:${type}`) continue;
|
|
1808
|
-
creatorCount++;
|
|
1809
|
-
}
|
|
1810
|
-
Epub.getXmlChildren(metadata).splice(
|
|
1811
|
-
metadataIndex,
|
|
1812
|
-
0,
|
|
1813
|
-
Epub.createXmlElement(`dc:${type}`, { id: creatorId }, [
|
|
1814
|
-
Epub.createXmlTextNode(creator.name)
|
|
1815
|
-
])
|
|
1816
|
-
);
|
|
1817
|
-
});
|
|
1818
|
-
if (creator.role) {
|
|
1819
|
-
await this.addMetadata({
|
|
1820
|
-
type: "meta",
|
|
1821
|
-
properties: {
|
|
1822
|
-
refines: `#${creatorId}`,
|
|
1823
|
-
property: "role",
|
|
1824
|
-
...creator.roleScheme && { scheme: creator.roleScheme }
|
|
1825
|
-
},
|
|
1826
|
-
value: creator.role
|
|
1827
|
-
});
|
|
1828
|
-
}
|
|
1829
|
-
if (creator.fileAs) {
|
|
1830
|
-
await this.addMetadata({
|
|
1831
|
-
type: "meta",
|
|
1832
|
-
properties: { refines: `#${creatorId}`, property: "file-as" },
|
|
1833
|
-
value: creator.fileAs
|
|
1834
|
-
});
|
|
1835
|
-
}
|
|
1836
|
-
if (creator.alternateScripts) {
|
|
1837
|
-
for (const alternate of creator.alternateScripts) {
|
|
1838
|
-
await this.addMetadata({
|
|
1839
|
-
type: "meta",
|
|
1840
|
-
properties: {
|
|
1841
|
-
refines: `#${creatorId}`,
|
|
1842
|
-
property: "alternate-script",
|
|
1843
|
-
"xml:lang": alternate.locale.toString()
|
|
1844
|
-
},
|
|
1845
|
-
value: alternate.name
|
|
1846
|
-
});
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
}
|
|
1850
|
-
/**
|
|
1851
|
-
* Remove a creator from the EPUB metadata.
|
|
1852
|
-
*
|
|
1853
|
-
* Removes the creator at the provided index. This index
|
|
1854
|
-
* refers to the array returned by `epub.getCreators()`.
|
|
1855
|
-
*
|
|
1856
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dccreator
|
|
1857
|
-
*/
|
|
1858
|
-
async removeCreator(index, type = "creator") {
|
|
1859
|
-
await this.withPackage((packageElement) => {
|
|
1860
|
-
var _a;
|
|
1861
|
-
const metadata = Epub.findXmlChildByName(
|
|
1862
|
-
"metadata",
|
|
1863
|
-
Epub.getXmlChildren(packageElement)
|
|
1864
|
-
);
|
|
1865
|
-
if (!metadata)
|
|
1866
|
-
throw new Error(
|
|
1867
|
-
"Failed to parse EPUB: found no metadata element in package document"
|
|
1868
|
-
);
|
|
1869
|
-
let creatorCount = null;
|
|
1870
|
-
let metadataIndex = null;
|
|
1871
|
-
for (const meta of Epub.getXmlChildren(metadata)) {
|
|
1872
|
-
if (creatorCount === index) break;
|
|
1873
|
-
metadataIndex = metadataIndex === null ? 0 : metadataIndex + 1;
|
|
1874
|
-
if (Epub.isXmlTextNode(meta)) continue;
|
|
1875
|
-
if (Epub.getXmlElementName(meta) !== `dc:${type}`) continue;
|
|
1876
|
-
creatorCount = creatorCount === null ? 0 : creatorCount + 1;
|
|
1877
|
-
}
|
|
1878
|
-
if (creatorCount === null || metadataIndex === null) return;
|
|
1879
|
-
const [removed] = Epub.getXmlChildren(metadata).splice(metadataIndex, 1);
|
|
1880
|
-
if (removed && !Epub.isXmlTextNode(removed) && ((_a = removed[":@"]) == null ? void 0 : _a["@_id"])) {
|
|
1881
|
-
const id = removed[":@"]["@_id"];
|
|
1882
|
-
const newChildren = Epub.getXmlChildren(metadata).filter((node) => {
|
|
1883
|
-
var _a2;
|
|
1884
|
-
if (Epub.isXmlTextNode(node)) return true;
|
|
1885
|
-
if (Epub.getXmlElementName(node) !== "meta") return true;
|
|
1886
|
-
if (((_a2 = node[":@"]) == null ? void 0 : _a2["@_refines"]) !== `#${id}`) return true;
|
|
1887
|
-
return false;
|
|
1888
|
-
});
|
|
1889
|
-
Epub.replaceXmlChildren(metadata, newChildren);
|
|
1890
|
-
}
|
|
1891
|
-
});
|
|
1892
|
-
}
|
|
1893
|
-
/**
|
|
1894
|
-
* Remove a contributor from the EPUB metadata.
|
|
1895
|
-
*
|
|
1896
|
-
* Removes the contributor at the provided index. This index
|
|
1897
|
-
* refers to the array returned by `epub.getContributors()`.
|
|
1898
|
-
*
|
|
1899
|
-
* This is a convenience method for
|
|
1900
|
-
* `epub.removeCreator(index, 'contributor')`.
|
|
1901
|
-
*
|
|
1902
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dccreator
|
|
1903
|
-
*/
|
|
1904
|
-
async removeContributor(index) {
|
|
1905
|
-
return this.removeCreator(index, "contributor");
|
|
1906
|
-
}
|
|
1907
|
-
/**
|
|
1908
|
-
* Add a contributor to the EPUB metadata.
|
|
1909
|
-
*
|
|
1910
|
-
* If index is provided, the creator will be placed at
|
|
1911
|
-
* that index in the list of creators. Otherwise, it
|
|
1912
|
-
* will be added to the end of the list.
|
|
1913
|
-
*
|
|
1914
|
-
* This is a convenience method for
|
|
1915
|
-
* `epub.addCreator(contributor, index, 'contributor')`.
|
|
1916
|
-
*
|
|
1917
|
-
* @link https://www.w3.org/TR/epub-33/#sec-opf-dccreator
|
|
1918
|
-
*/
|
|
1919
|
-
addContributor(contributor, index) {
|
|
1920
|
-
return this.addCreator(contributor, index, "contributor");
|
|
1921
|
-
}
|
|
1922
|
-
async getSpine() {
|
|
1923
|
-
if (this.spine !== null) return this.spine;
|
|
1924
|
-
const packageElement = await this.getPackageElement();
|
|
1925
|
-
const spine = Epub.findXmlChildByName(
|
|
1926
|
-
"spine",
|
|
1927
|
-
Epub.getXmlChildren(packageElement)
|
|
1928
|
-
);
|
|
1929
|
-
if (!spine)
|
|
1930
|
-
throw new Error(
|
|
1931
|
-
"Failed to parse EPUB: Found no spine element in package document"
|
|
1932
|
-
);
|
|
1933
|
-
this.spine = spine["spine"].filter((node) => !Epub.isXmlTextNode(node)).map((itemref) => {
|
|
1934
|
-
var _a;
|
|
1935
|
-
return (_a = itemref[":@"]) == null ? void 0 : _a["@_idref"];
|
|
1936
|
-
}).filter((idref) => !!idref);
|
|
1937
|
-
return this.spine;
|
|
1938
|
-
}
|
|
1939
|
-
/**
|
|
1940
|
-
* Retrieve the manifest items that make up the Epub's spine.
|
|
1941
|
-
*
|
|
1942
|
-
* The spine specifies the order that the contents of the Epub
|
|
1943
|
-
* should be displayed to users by default.
|
|
1944
|
-
*
|
|
1945
|
-
* @link https://www.w3.org/TR/epub-33/#sec-spine-elem
|
|
1946
|
-
*/
|
|
1947
|
-
async getSpineItems() {
|
|
1948
|
-
const spine = await this.getSpine();
|
|
1949
|
-
const manifest = await this.getManifest();
|
|
1950
|
-
return spine.map((itemref) => manifest[itemref]).filter((entry) => !!entry);
|
|
1951
|
-
}
|
|
1952
|
-
/**
|
|
1953
|
-
* Add an item to the spine of the EPUB.
|
|
1954
|
-
*
|
|
1955
|
-
* If `index` is undefined, the item will be added
|
|
1956
|
-
* to the end of the spine. Otherwise it will be
|
|
1957
|
-
* inserted at the specified index.
|
|
1958
|
-
*
|
|
1959
|
-
* If the manifestId does not correspond to an item
|
|
1960
|
-
* in the manifest, this will throw an error.
|
|
1961
|
-
*
|
|
1962
|
-
* @link https://www.w3.org/TR/epub-33/#sec-spine-elem
|
|
1963
|
-
*/
|
|
1964
|
-
async addSpineItem(manifestId, index) {
|
|
1965
|
-
const item = Epub.createXmlElement("itemref", { idref: manifestId });
|
|
1966
|
-
const manifest = await this.getManifest();
|
|
1967
|
-
const manifestItem = manifest[manifestId];
|
|
1968
|
-
if (!manifestItem)
|
|
1969
|
-
throw new Error(`Manifest item not found with id "${manifestId}"`);
|
|
1970
|
-
await this.withPackage((packageElement) => {
|
|
1971
|
-
const spine = Epub.findXmlChildByName(
|
|
1972
|
-
"spine",
|
|
1973
|
-
Epub.getXmlChildren(packageElement)
|
|
1974
|
-
);
|
|
1975
|
-
if (!spine)
|
|
1976
|
-
throw new Error(
|
|
1977
|
-
"Failed to parse EPUB: Found no spine element in package document"
|
|
1978
|
-
);
|
|
1979
|
-
if (index === void 0) {
|
|
1980
|
-
Epub.getXmlChildren(spine).push(item);
|
|
1981
|
-
} else {
|
|
1982
|
-
Epub.getXmlChildren(spine).splice(index, 0, item);
|
|
1983
|
-
}
|
|
1984
|
-
});
|
|
1985
|
-
this.spine = null;
|
|
1986
|
-
}
|
|
1987
|
-
/**
|
|
1988
|
-
* Remove the spine item at the specified index.
|
|
1989
|
-
*
|
|
1990
|
-
* @link https://www.w3.org/TR/epub-33/#sec-spine-elem
|
|
1991
|
-
*/
|
|
1992
|
-
async removeSpineItem(index) {
|
|
1993
|
-
await this.withPackage((packageElement) => {
|
|
1994
|
-
const spine = Epub.findXmlChildByName(
|
|
1995
|
-
"spine",
|
|
1996
|
-
Epub.getXmlChildren(packageElement)
|
|
1997
|
-
);
|
|
1998
|
-
if (!spine)
|
|
1999
|
-
throw new Error(
|
|
2000
|
-
"Failed to parse EPUB: Found no spine element in package document"
|
|
2001
|
-
);
|
|
2002
|
-
Epub.getXmlChildren(spine).splice(index, 1);
|
|
2003
|
-
});
|
|
2004
|
-
this.spine = null;
|
|
2005
|
-
}
|
|
2006
|
-
async getNavigationChildren(ol, navHref, { resolveToRoot } = {}) {
|
|
2007
|
-
var _a;
|
|
2008
|
-
const children = [];
|
|
2009
|
-
const childrenElements = Epub.getXmlChildren(ol).filter(
|
|
2010
|
-
(node) => !Epub.isXmlTextNode(node) && Epub.getXmlElementName(node) === "li"
|
|
2011
|
-
);
|
|
2012
|
-
for (const childEl of childrenElements) {
|
|
2013
|
-
const [firstChild, secondChild] = Epub.getXmlChildren(childEl).filter(
|
|
2014
|
-
(node) => !Epub.isXmlTextNode(node) && ["a", "span", "ol"].includes(Epub.getXmlElementName(node))
|
|
2015
|
-
);
|
|
2016
|
-
if (!firstChild) continue;
|
|
2017
|
-
if (!["a", "span"].includes(Epub.getXmlElementName(firstChild))) {
|
|
2018
|
-
continue;
|
|
2019
|
-
}
|
|
2020
|
-
if (Epub.getXmlElementName(firstChild) === "span" && (!secondChild || Epub.getXmlElementName(secondChild) !== "ol")) {
|
|
2021
|
-
continue;
|
|
2022
|
-
}
|
|
2023
|
-
children.push({
|
|
2024
|
-
title: Epub.getXhtmlTextContent(Epub.getXmlChildren(firstChild)),
|
|
2025
|
-
...Epub.getXmlElementName(firstChild) === "a" && ((_a = firstChild[":@"]) == null ? void 0 : _a["@_href"]) && {
|
|
2026
|
-
href: await this.resolveHref(
|
|
2027
|
-
firstChild[":@"]["@_href"],
|
|
2028
|
-
void 0,
|
|
2029
|
-
{ toRoot: resolveToRoot }
|
|
2030
|
-
)
|
|
2031
|
-
},
|
|
2032
|
-
...secondChild && Epub.getXmlElementName(secondChild) === "ol" && {
|
|
2033
|
-
children: await this.getNavigationChildren(secondChild, navHref, {
|
|
2034
|
-
resolveToRoot
|
|
2035
|
-
})
|
|
2036
|
-
}
|
|
2037
|
-
});
|
|
2038
|
-
}
|
|
2039
|
-
return children;
|
|
2040
|
-
}
|
|
2041
|
-
async getNavigation(role, { resolveToRoot } = {}) {
|
|
2042
|
-
const manifest = await this.getManifest();
|
|
2043
|
-
const navItem = Object.values(manifest).find(
|
|
2044
|
-
(item) => {
|
|
2045
|
-
var _a;
|
|
2046
|
-
return (_a = item.properties) == null ? void 0 : _a.includes("nav");
|
|
2047
|
-
}
|
|
2048
|
-
);
|
|
2049
|
-
if (!navItem) return null;
|
|
2050
|
-
const navContents = await this.readXhtmlItemContents(navItem.id);
|
|
2051
|
-
const navEl = Epub.findXmlDescendantByName(
|
|
2052
|
-
"nav",
|
|
2053
|
-
navContents,
|
|
2054
|
-
(node) => Epub.getXmlAttributes(node)["epub:type"] === role
|
|
2055
|
-
);
|
|
2056
|
-
if (!navEl) return null;
|
|
2057
|
-
const [firstChild, secondChild] = Epub.getXmlChildren(navEl).filter(
|
|
2058
|
-
(node) => !!(!Epub.isXmlTextNode(node) && Epub.getXmlElementName(node).match(/(?:h[1-6]|ol)/))
|
|
2059
|
-
);
|
|
2060
|
-
if (!firstChild) return null;
|
|
2061
|
-
const title = Epub.getXmlElementName(firstChild).match(/h[1-6]/) ? Epub.getXhtmlTextContent(Epub.getXmlChildren(firstChild)) : null;
|
|
2062
|
-
const list = Epub.getXmlElementName(firstChild) === "ol" ? firstChild : secondChild && Epub.getXmlElementName(secondChild) === "ol" ? secondChild : null;
|
|
2063
|
-
if (!list) return null;
|
|
2064
|
-
const children = await this.getNavigationChildren(list, navItem.href, {
|
|
2065
|
-
resolveToRoot
|
|
2066
|
-
});
|
|
2067
|
-
return {
|
|
2068
|
-
...title && { title },
|
|
2069
|
-
children
|
|
2070
|
-
};
|
|
2071
|
-
}
|
|
2072
|
-
/**
|
|
2073
|
-
* Returns the structured table of contents navigation document
|
|
2074
|
-
* as a Navigation object.
|
|
2075
|
-
*
|
|
2076
|
-
* @link https://www.w3.org/TR/epub-33/#sec-nav-toc
|
|
2077
|
-
*/
|
|
2078
|
-
async getTableOfContents({
|
|
2079
|
-
resolveToRoot
|
|
2080
|
-
} = {}) {
|
|
2081
|
-
const navigationToc = await this.getNavigation("toc", { resolveToRoot });
|
|
2082
|
-
if (navigationToc) return navigationToc;
|
|
2083
|
-
const ncxToc = await this.getNcxTableOfContents();
|
|
2084
|
-
return {
|
|
2085
|
-
children: ncxToc
|
|
2086
|
-
};
|
|
2087
|
-
}
|
|
2088
|
-
/**
|
|
2089
|
-
* Returns the structured landmarks navigation document
|
|
2090
|
-
* as a Navigation object
|
|
2091
|
-
*
|
|
2092
|
-
* @link https://www.w3.org/TR/epub-33/#sec-nav-landmarks
|
|
2093
|
-
*/
|
|
2094
|
-
async getLandmarks({
|
|
2095
|
-
resolveToRoot
|
|
2096
|
-
} = {}) {
|
|
2097
|
-
return this.getNavigation("landmarks", { resolveToRoot });
|
|
2098
|
-
}
|
|
2099
|
-
/**
|
|
2100
|
-
* Returns the structured page list navigation document
|
|
2101
|
-
* as a Navigation object
|
|
2102
|
-
*
|
|
2103
|
-
* @link https://www.w3.org/TR/epub-33/#sec-nav-landmarks
|
|
2104
|
-
*/
|
|
2105
|
-
async getPageList({
|
|
2106
|
-
resolveToRoot
|
|
2107
|
-
} = {}) {
|
|
2108
|
-
return this.getNavigation("page-list", { resolveToRoot });
|
|
2109
|
-
}
|
|
2110
|
-
/**
|
|
2111
|
-
* Name the entry a resolved URL addresses.
|
|
2112
|
-
*
|
|
2113
|
-
* A correctly authored publication percent-encodes its hrefs, so the
|
|
2114
|
-
* decoded reading is the one the spec calls for and the one we prefer.
|
|
2115
|
-
* Some publications instead write the entry name verbatim, so when the two
|
|
2116
|
-
* readings differ we ask the archive which one it holds. A verbatim name
|
|
2117
|
-
* carrying a bare `%`, as `100%.xhtml` does, is not a valid encoding at all
|
|
2118
|
-
* and can only be its own answer.
|
|
2119
|
-
*/
|
|
2120
|
-
async entryForUrl(url) {
|
|
2121
|
-
const asWritten = url.pathname.slice(1);
|
|
2122
|
-
let decoded;
|
|
2123
|
-
try {
|
|
2124
|
-
decoded = decodeURIComponent(asWritten);
|
|
2125
|
-
} catch {
|
|
2126
|
-
return asWritten;
|
|
2127
|
-
}
|
|
2128
|
-
if (decoded === asWritten) {
|
|
2129
|
-
return decoded;
|
|
2130
|
-
}
|
|
2131
|
-
if (await this.adapter.exists(decoded)) {
|
|
2132
|
-
return decoded;
|
|
2133
|
-
}
|
|
2134
|
-
return await this.adapter.exists(asWritten) ? asWritten : decoded;
|
|
2135
|
-
}
|
|
2136
|
-
/**
|
|
2137
|
-
* Resolve an href to the name of the archive entry it addresses.
|
|
2138
|
-
*
|
|
2139
|
-
* @param from The entry the href appears in; it resolves against that
|
|
2140
|
-
* entry's directory
|
|
2141
|
-
* @throws when the href addresses something outside the publication
|
|
2142
|
-
*/
|
|
2143
|
-
async resolveEntry(from, href) {
|
|
2144
|
-
const url = resolveEntryUrl(from, href);
|
|
2145
|
-
if (!url) {
|
|
2146
|
-
throw new Error(
|
|
2147
|
-
`href does not address an entry in this publication: ${href}`
|
|
2148
|
-
);
|
|
2149
|
-
}
|
|
2150
|
-
return this.entryForUrl(url);
|
|
2151
|
-
}
|
|
2152
|
-
/**
|
|
2153
|
-
* Returns a path-relative-scheme-less URL, relative to the
|
|
2154
|
-
* container root.
|
|
2155
|
-
*
|
|
2156
|
-
* An href that points outside the publication, such as an external link in
|
|
2157
|
-
* a navigation document, is handed back unchanged.
|
|
2158
|
-
*
|
|
2159
|
-
* @param href The href to resolve
|
|
2160
|
-
* @param [relativeTo] Optional - The href to resolve this href relative to.
|
|
2161
|
-
Use if resolving a relative href from a file other than the package document.
|
|
2162
|
-
*/
|
|
2163
|
-
async resolveHref(href, relativeTo, { toRoot } = {}) {
|
|
2164
|
-
if (href.startsWith("#")) return href;
|
|
2165
|
-
const rootfile = await this.getRootfile();
|
|
2166
|
-
const from = relativeTo ? await this.resolveEntry(rootfile, relativeTo) : rootfile;
|
|
2167
|
-
const url = resolveEntryUrl(from, href);
|
|
2168
|
-
if (!url) return href;
|
|
2169
|
-
const entry = (await this.entryForUrl(url)).split("/");
|
|
2170
|
-
const base = toRoot ? [] : rootfile.split("/").slice(0, -1);
|
|
2171
|
-
let shared = 0;
|
|
2172
|
-
while (shared < base.length && base[shared] === entry[shared]) {
|
|
2173
|
-
shared++;
|
|
2174
|
-
}
|
|
2175
|
-
const relative = [
|
|
2176
|
-
...Array(base.length - shared).fill(".."),
|
|
2177
|
-
...entry.slice(shared).map(encodeURIComponent)
|
|
2178
|
-
];
|
|
2179
|
-
return relative.join("/") + url.hash;
|
|
2180
|
-
}
|
|
2181
|
-
async readFileContents(href, relativeTo, encoding) {
|
|
2182
|
-
const rootfile = await this.getRootfile();
|
|
2183
|
-
const from = relativeTo ? await this.resolveEntry(rootfile, relativeTo) : rootfile;
|
|
2184
|
-
const entry = await this.resolveEntry(from, href);
|
|
2185
|
-
const itemEntry = encoding ? await this.adapter.read(entry, encoding) : await this.adapter.read(entry);
|
|
2186
|
-
return itemEntry;
|
|
2187
|
-
}
|
|
2188
|
-
async readItemContents(id, encoding) {
|
|
2189
|
-
const rootfile = await this.getRootfile();
|
|
2190
|
-
const manifest = await this.getManifest();
|
|
2191
|
-
const manifestItem = manifest[id];
|
|
2192
|
-
if (!manifestItem)
|
|
2193
|
-
throw new Error(`Could not find item with id "${id}" in manifest`);
|
|
2194
|
-
const entry = await this.resolveEntry(rootfile, manifestItem.href);
|
|
2195
|
-
const itemEntry = encoding ? await this.adapter.read(entry, encoding) : await this.adapter.read(entry);
|
|
2196
|
-
return itemEntry;
|
|
2197
|
-
}
|
|
2198
|
-
/**
|
|
2199
|
-
* Create a new XHTML document with the given body
|
|
2200
|
-
* and head.
|
|
2201
|
-
*
|
|
2202
|
-
* @param body The XML nodes to place in the body of the document
|
|
2203
|
-
* @param head Optional - the XMl nodes to place in the head
|
|
2204
|
-
* @param language Optional - defaults to the EPUB's language
|
|
2205
|
-
*/
|
|
2206
|
-
async createXhtmlDocument(body, head, language) {
|
|
2207
|
-
const lang = language ?? await this.getLanguage();
|
|
2208
|
-
return [
|
|
2209
|
-
Epub.createXmlElement("?xml", { version: "1.0", encoding: "UTF-8" }, [
|
|
2210
|
-
{ "#text": "" }
|
|
2211
|
-
]),
|
|
2212
|
-
Epub.createXmlElement(
|
|
2213
|
-
"html",
|
|
2214
|
-
{
|
|
2215
|
-
xmlns: "http://www.w3.org/1999/xhtml",
|
|
2216
|
-
"xmlns:epub": "http://www.idpf.org/2007/ops",
|
|
2217
|
-
...lang && { "xml:lang": lang.toString(), lang: lang.toString() }
|
|
2218
|
-
},
|
|
2219
|
-
[
|
|
2220
|
-
Epub.createXmlElement("head", {}, head),
|
|
2221
|
-
Epub.createXmlElement("body", {}, body)
|
|
2222
|
-
]
|
|
2223
|
-
)
|
|
2224
|
-
];
|
|
2225
|
-
}
|
|
2226
|
-
async readXhtmlItemContents(id, as = "xhtml") {
|
|
2227
|
-
const contents = await this.readItemContents(id, "utf-8");
|
|
2228
|
-
const xml = Epub.xhtmlParser.parse(contents);
|
|
2229
|
-
if (as === "xhtml") return xml;
|
|
2230
|
-
const body = Epub.getXhtmlBody(xml);
|
|
2231
|
-
return Epub.getXhtmlTextContent(body);
|
|
2232
|
-
}
|
|
2233
|
-
async writeEntryContents(path, contents, encoding) {
|
|
2234
|
-
this.assertWritable();
|
|
2235
|
-
if (!this.adapter.write) {
|
|
2236
|
-
throw new EpubReadOnlyError(
|
|
2237
|
-
`adapter ${this.adapterClass.kind} does not support writes`
|
|
2238
|
-
);
|
|
2239
|
-
}
|
|
2240
|
-
if (encoding === "utf-8") {
|
|
2241
|
-
await this.adapter.write(path, contents, encoding);
|
|
2242
|
-
} else {
|
|
2243
|
-
await this.adapter.write(path, contents);
|
|
2244
|
-
}
|
|
2245
|
-
}
|
|
2246
|
-
async writeItemContents(id, contents, encoding) {
|
|
2247
|
-
const rootfile = await this.getRootfile();
|
|
2248
|
-
const manifest = await this.getManifest();
|
|
2249
|
-
const manifestItem = manifest[id];
|
|
2250
|
-
if (!manifestItem)
|
|
2251
|
-
throw new Error(`Could not find item with id "${id}" in manifest`);
|
|
2252
|
-
import_mem.default.clear(this.readXhtmlItemContents);
|
|
2253
|
-
const entry = await this.resolveEntry(rootfile, manifestItem.href);
|
|
2254
|
-
if (encoding === "utf-8") {
|
|
2255
|
-
await this.writeEntryContents(entry, contents, encoding);
|
|
2256
|
-
} else {
|
|
2257
|
-
await this.writeEntryContents(entry, contents);
|
|
2258
|
-
}
|
|
2259
|
-
}
|
|
2260
|
-
/**
|
|
2261
|
-
* Write new contents for an existing XHTML item,
|
|
2262
|
-
* specified by its id.
|
|
2263
|
-
*
|
|
2264
|
-
* The id must reference an existing manifest item. If
|
|
2265
|
-
* creating a new item, use `epub.addManifestItem()` instead.
|
|
2266
|
-
*
|
|
2267
|
-
* @param id The id of the manifest item to write new contents for
|
|
2268
|
-
* @param contents The new contents. Must be a parsed XML tree.
|
|
2269
|
-
*
|
|
2270
|
-
* @link https://www.w3.org/TR/epub-33/#sec-xhtml
|
|
2271
|
-
*/
|
|
2272
|
-
async writeXhtmlItemContents(id, contents) {
|
|
2273
|
-
await this.writeItemContents(
|
|
2274
|
-
id,
|
|
2275
|
-
Epub.xhtmlBuilder.build(contents),
|
|
2276
|
-
"utf-8"
|
|
2277
|
-
);
|
|
2278
|
-
}
|
|
2279
|
-
async removeManifestItem(id) {
|
|
2280
|
-
await this.withPackage(async (packageElement) => {
|
|
2281
|
-
var _a;
|
|
2282
|
-
const manifest = Epub.findXmlChildByName(
|
|
2283
|
-
"manifest",
|
|
2284
|
-
Epub.getXmlChildren(packageElement)
|
|
2285
|
-
);
|
|
2286
|
-
if (!manifest)
|
|
2287
|
-
throw new Error(
|
|
2288
|
-
"Failed to parse EPUB: Found no manifest element in package document"
|
|
2289
|
-
);
|
|
2290
|
-
const itemIndex = Epub.getXmlChildren(manifest).findIndex(
|
|
2291
|
-
(node) => {
|
|
2292
|
-
var _a2;
|
|
2293
|
-
return !Epub.isXmlTextNode(node) && ((_a2 = node[":@"]) == null ? void 0 : _a2["@_id"]) === id;
|
|
2294
|
-
}
|
|
2295
|
-
);
|
|
2296
|
-
if (itemIndex === -1) return;
|
|
2297
|
-
const [item] = Epub.getXmlChildren(manifest).splice(itemIndex, 1);
|
|
2298
|
-
if (!item || Epub.isXmlTextNode(item) || !((_a = item[":@"]) == null ? void 0 : _a["@_href"])) return;
|
|
2299
|
-
await this.removeEntry(item[":@"]["@_href"]);
|
|
2300
|
-
});
|
|
2301
|
-
this.manifest = null;
|
|
2302
|
-
}
|
|
2303
|
-
async addManifestItem(item, contents, encoding) {
|
|
2304
|
-
await this.withPackage((packageElement) => {
|
|
2305
|
-
const manifest = Epub.findXmlChildByName(
|
|
2306
|
-
"manifest",
|
|
2307
|
-
Epub.getXmlChildren(packageElement)
|
|
2308
|
-
);
|
|
2309
|
-
if (!manifest)
|
|
2310
|
-
throw new Error(
|
|
2311
|
-
"Failed to parse EPUB: Found no manifest element in package document"
|
|
2312
|
-
);
|
|
2313
|
-
Epub.getXmlChildren(manifest).push(
|
|
2314
|
-
Epub.createXmlElement("item", {
|
|
2315
|
-
id: item.id,
|
|
2316
|
-
href: item.href,
|
|
2317
|
-
...item.mediaType && { "media-type": item.mediaType },
|
|
2318
|
-
...item.fallback && { fallback: item.fallback },
|
|
2319
|
-
...item.mediaOverlay && { "media-overlay": item.mediaOverlay },
|
|
2320
|
-
...item.properties && {
|
|
2321
|
-
properties: item.properties.join(" ")
|
|
2322
|
-
}
|
|
2323
|
-
})
|
|
2324
|
-
);
|
|
2325
|
-
});
|
|
2326
|
-
this.manifest = null;
|
|
2327
|
-
const rootfile = await this.getRootfile();
|
|
2328
|
-
const filename = await this.resolveEntry(rootfile, item.href);
|
|
2329
|
-
const data = encoding === "utf-8" || encoding === "xml" ? new TextEncoder().encode(
|
|
2330
|
-
encoding === "utf-8" ? contents : await Epub.xmlBuilder.build(
|
|
2331
|
-
contents
|
|
2332
|
-
)
|
|
2333
|
-
) : contents;
|
|
2334
|
-
await this.writeEntryContents(filename, data);
|
|
2335
|
-
}
|
|
2336
|
-
/**
|
|
2337
|
-
* Update the manifest entry for an existing item.
|
|
2338
|
-
*
|
|
2339
|
-
* To update the contents of an entry, use `epub.writeItemContents()`
|
|
2340
|
-
* or `epub.writeXhtmlItemContents()`
|
|
2341
|
-
*
|
|
2342
|
-
* @link https://www.w3.org/TR/epub-33/#sec-pkg-manifest
|
|
2343
|
-
*/
|
|
2344
|
-
async updateManifestItem(id, newItem) {
|
|
2345
|
-
await this.withPackage((packageElement) => {
|
|
2346
|
-
const manifest = Epub.findXmlChildByName(
|
|
2347
|
-
"manifest",
|
|
2348
|
-
Epub.getXmlChildren(packageElement)
|
|
2349
|
-
);
|
|
2350
|
-
if (!manifest)
|
|
2351
|
-
throw new Error(
|
|
2352
|
-
"Failed to parse EPUB: Found no manifest element in package document"
|
|
2353
|
-
);
|
|
2354
|
-
const itemIndex = manifest["manifest"].findIndex(
|
|
2355
|
-
(item) => {
|
|
2356
|
-
var _a;
|
|
2357
|
-
return !Epub.isXmlTextNode(item) && ((_a = item[":@"]) == null ? void 0 : _a["@_id"]) === id;
|
|
2358
|
-
}
|
|
2359
|
-
);
|
|
2360
|
-
Epub.getXmlChildren(manifest).splice(
|
|
2361
|
-
itemIndex,
|
|
2362
|
-
1,
|
|
2363
|
-
Epub.createXmlElement("item", {
|
|
2364
|
-
id,
|
|
2365
|
-
href: newItem.href,
|
|
2366
|
-
...newItem.mediaType && { "media-type": newItem.mediaType },
|
|
2367
|
-
...newItem.fallback && { fallback: newItem.fallback },
|
|
2368
|
-
...newItem.mediaOverlay && {
|
|
2369
|
-
"media-overlay": newItem.mediaOverlay
|
|
2370
|
-
},
|
|
2371
|
-
...newItem.properties && {
|
|
2372
|
-
properties: newItem.properties.join(" ")
|
|
2373
|
-
}
|
|
2374
|
-
})
|
|
2375
|
-
);
|
|
2376
|
-
});
|
|
2377
|
-
this.manifest = null;
|
|
2378
|
-
}
|
|
2379
|
-
/**
|
|
2380
|
-
* Add a new metadata entry to the Epub.
|
|
2381
|
-
*
|
|
2382
|
-
* This method, like `epub.getMetadata()`, operates on
|
|
2383
|
-
* metadata entries. For more useful semantic representations
|
|
2384
|
-
* of metadata, use specific methods such as `setTitle()` and
|
|
2385
|
-
* `setLanguage()`.
|
|
2386
|
-
*
|
|
2387
|
-
* @link https://www.w3.org/TR/epub-33/#sec-pkg-metadata
|
|
2388
|
-
*/
|
|
2389
|
-
async addMetadata(entry) {
|
|
2390
|
-
await this.withPackage((packageElement) => {
|
|
2391
|
-
const metadata = Epub.findXmlChildByName(
|
|
2392
|
-
"metadata",
|
|
2393
|
-
Epub.getXmlChildren(packageElement)
|
|
2394
|
-
);
|
|
2395
|
-
if (!metadata)
|
|
2396
|
-
throw new Error(
|
|
2397
|
-
"Failed to parse EPUB: found no metadata element in package document"
|
|
2398
|
-
);
|
|
2399
|
-
Epub.getXmlChildren(metadata).push(
|
|
2400
|
-
Epub.createXmlElement(
|
|
2401
|
-
entry.type,
|
|
2402
|
-
{
|
|
2403
|
-
...entry.id && { id: entry.id },
|
|
2404
|
-
...entry.properties
|
|
2405
|
-
},
|
|
2406
|
-
entry.value !== void 0 ? [Epub.createXmlTextNode(entry.value)] : []
|
|
2407
|
-
)
|
|
2408
|
-
);
|
|
2409
|
-
});
|
|
2410
|
-
}
|
|
2411
|
-
/**
|
|
2412
|
-
* Replace a metadata entry with a new one.
|
|
2413
|
-
*
|
|
2414
|
-
* The `predicate` argument will be used to determine which entry
|
|
2415
|
-
* to replace. The first metadata entry that matches the
|
|
2416
|
-
* predicate will be replaced.
|
|
2417
|
-
*
|
|
2418
|
-
* @param predicate Calls predicate once for each metadata entry,
|
|
2419
|
-
* until it finds one where predicate returns true
|
|
2420
|
-
* @param entry The new entry to replace the found entry with
|
|
2421
|
-
*
|
|
2422
|
-
* @link https://www.w3.org/TR/epub-33/#sec-pkg-metadata
|
|
2423
|
-
*/
|
|
2424
|
-
async replaceMetadata(predicate, entry) {
|
|
2425
|
-
await this.withPackage((packageElement) => {
|
|
2426
|
-
const metadataElement = Epub.findXmlChildByName(
|
|
2427
|
-
"metadata",
|
|
2428
|
-
Epub.getXmlChildren(packageElement)
|
|
2429
|
-
);
|
|
2430
|
-
if (!metadataElement)
|
|
2431
|
-
throw new Error(
|
|
2432
|
-
"Failed to parse EPUB: found no metadata element in package document"
|
|
2433
|
-
);
|
|
2434
|
-
const oldEntryIndex = this.findMetadataIndex(packageElement, predicate);
|
|
2435
|
-
const newElement = Epub.createXmlElement(
|
|
2436
|
-
entry.type,
|
|
2437
|
-
{
|
|
2438
|
-
...entry.id && { id: entry.id },
|
|
2439
|
-
...entry.properties
|
|
2440
|
-
},
|
|
2441
|
-
entry.value !== void 0 ? [Epub.createXmlTextNode(entry.value)] : []
|
|
2442
|
-
);
|
|
2443
|
-
if (oldEntryIndex === -1) {
|
|
2444
|
-
metadataElement.metadata.push(newElement);
|
|
2445
|
-
} else {
|
|
2446
|
-
metadataElement.metadata.splice(oldEntryIndex, 1, newElement);
|
|
2447
|
-
}
|
|
2448
|
-
});
|
|
2449
|
-
}
|
|
2450
|
-
/**
|
|
2451
|
-
* Remove one or more metadata entries.
|
|
2452
|
-
*
|
|
2453
|
-
* The `predicate` argument will be used to determine which entries
|
|
2454
|
-
* to remove. The all metadata entries that match the
|
|
2455
|
-
* predicate will be removed.
|
|
2456
|
-
*
|
|
2457
|
-
* @param predicate Calls predicate once for each metadata entry,
|
|
2458
|
-
* removing any for which it returns true
|
|
2459
|
-
*
|
|
2460
|
-
* @link https://www.w3.org/TR/epub-33/#sec-pkg-metadata
|
|
2461
|
-
*/
|
|
2462
|
-
async removeMetadata(predicate) {
|
|
2463
|
-
await this.withPackage((packageElement) => {
|
|
2464
|
-
const metadataElement = Epub.findXmlChildByName(
|
|
2465
|
-
"metadata",
|
|
2466
|
-
Epub.getXmlChildren(packageElement)
|
|
2467
|
-
);
|
|
2468
|
-
if (!metadataElement) {
|
|
2469
|
-
throw new Error(
|
|
2470
|
-
"Failed to parse EPUB: found no metadata element in package document"
|
|
2471
|
-
);
|
|
2472
|
-
}
|
|
2473
|
-
const metadataEntries = Epub.getXmlChildren(metadataElement);
|
|
2474
|
-
for (let i = metadataEntries.length - 1; i >= 0; i--) {
|
|
2475
|
-
const meta = metadataEntries[i];
|
|
2476
|
-
const item = Epub.parseMetadataItem(meta);
|
|
2477
|
-
if (!item) continue;
|
|
2478
|
-
if (predicate(item)) {
|
|
2479
|
-
metadataEntries.splice(i, 1);
|
|
2480
|
-
}
|
|
2481
|
-
}
|
|
2482
|
-
});
|
|
2483
|
-
}
|
|
2484
|
-
/**
|
|
2485
|
-
* Returns the EPUB version declared on the package element.
|
|
2486
|
-
*/
|
|
2487
|
-
async getVersion() {
|
|
2488
|
-
var _a;
|
|
2489
|
-
const packageElement = await this.getPackageElement();
|
|
2490
|
-
return ((_a = packageElement[":@"]) == null ? void 0 : _a["@_version"]) ?? "2.0";
|
|
2491
|
-
}
|
|
2492
|
-
/**
|
|
2493
|
-
* Parse the NCX table of contents, if one exists, and return
|
|
2494
|
-
* a tree of TocEntry nodes.
|
|
2495
|
-
*
|
|
2496
|
-
* Useful for both EPUB 2 publications (where the NCX is the
|
|
2497
|
-
* primary navigation) and EPUB 3 publications that retain an
|
|
2498
|
-
* NCX for backwards compatibility.
|
|
2499
|
-
*/
|
|
2500
|
-
async getNcxTableOfContents() {
|
|
2501
|
-
var _a;
|
|
2502
|
-
const [manifest, packageElement] = await Promise.all([
|
|
2503
|
-
this.getManifest(),
|
|
2504
|
-
this.getPackageElement()
|
|
2505
|
-
]);
|
|
2506
|
-
const spine = Epub.findXmlChildByName(
|
|
2507
|
-
"spine",
|
|
2508
|
-
Epub.getXmlChildren(packageElement)
|
|
2509
|
-
);
|
|
2510
|
-
const spineTocId = (_a = spine == null ? void 0 : spine[":@"]) == null ? void 0 : _a["@_toc"];
|
|
2511
|
-
const ncxItem = spineTocId ? manifest[spineTocId] : Object.values(manifest).find(
|
|
2512
|
-
(item) => import_media_types.MediaType.fromMime(item.mediaType ?? "") === import_media_types.MediaType.NCX
|
|
2513
|
-
);
|
|
2514
|
-
if (!ncxItem) return [];
|
|
2515
|
-
const ncxContent = await this.readItemContents(ncxItem.id, "utf-8");
|
|
2516
|
-
const ncxXml = Epub.xmlParser.parse(ncxContent);
|
|
2517
|
-
const ncxElement = Epub.findXmlChildByName("ncx", ncxXml);
|
|
2518
|
-
if (!ncxElement) return [];
|
|
2519
|
-
const ncxChildren = Epub.getXmlChildren(ncxElement);
|
|
2520
|
-
const navMap = Epub.findXmlChildByName("navMap", ncxChildren) ?? Epub.findXmlChildByName("navmap", ncxChildren);
|
|
2521
|
-
if (!navMap) return [];
|
|
2522
|
-
return this.parseNavPoints(Epub.getXmlChildren(navMap), ncxItem.href);
|
|
2523
|
-
}
|
|
2524
|
-
async parseNavPoints(nodes, ncxHref) {
|
|
2525
|
-
var _a;
|
|
2526
|
-
const entries = [];
|
|
2527
|
-
for (const node of nodes) {
|
|
2528
|
-
if (Epub.isXmlTextNode(node)) continue;
|
|
2529
|
-
const name = Epub.getXmlElementName(node);
|
|
2530
|
-
const isNavPoint = name === "navPoint" || name === "navpoint";
|
|
2531
|
-
if (!isNavPoint) continue;
|
|
2532
|
-
const children = Epub.getXmlChildren(node);
|
|
2533
|
-
const navLabel = Epub.findXmlChildByName("navLabel", children) ?? Epub.findXmlChildByName("navlabel", children);
|
|
2534
|
-
let title = null;
|
|
2535
|
-
if (navLabel) {
|
|
2536
|
-
const textEl = Epub.findXmlChildByName(
|
|
2537
|
-
"text",
|
|
2538
|
-
Epub.getXmlChildren(navLabel)
|
|
2539
|
-
);
|
|
2540
|
-
if (textEl) {
|
|
2541
|
-
title = Epub.getXhtmlTextContent(Epub.getXmlChildren(textEl)).trim() || null;
|
|
2542
|
-
}
|
|
2543
|
-
}
|
|
2544
|
-
const contentEl = Epub.findXmlChildByName("content", children);
|
|
2545
|
-
const src = (_a = contentEl == null ? void 0 : contentEl[":@"]) == null ? void 0 : _a["@_src"];
|
|
2546
|
-
const href = src ? await this.resolveHref(src, ncxHref) : null;
|
|
2547
|
-
const childEntries = await this.parseNavPoints(children, ncxHref);
|
|
2548
|
-
entries.push({
|
|
2549
|
-
title: title ?? `${entries.length}`,
|
|
2550
|
-
...href && { href },
|
|
2551
|
-
children: childEntries
|
|
2552
|
-
});
|
|
2553
|
-
}
|
|
2554
|
-
return entries;
|
|
2555
|
-
}
|
|
2556
|
-
/**
|
|
2557
|
-
* Retrieve the guide entries from the package document.
|
|
2558
|
-
*
|
|
2559
|
-
* The guide element is deprecated in EPUB 3 in favor of
|
|
2560
|
-
* the landmarks nav, but many publications still include it.
|
|
2561
|
-
*/
|
|
2562
|
-
async getGuideEntries() {
|
|
2563
|
-
const packageElement = await this.getPackageElement();
|
|
2564
|
-
const guide = Epub.findXmlChildByName(
|
|
2565
|
-
"guide",
|
|
2566
|
-
Epub.getXmlChildren(packageElement)
|
|
2567
|
-
);
|
|
2568
|
-
if (!guide) return [];
|
|
2569
|
-
return Epub.getXmlChildren(guide).filter(
|
|
2570
|
-
(node) => !Epub.isXmlTextNode(node) && "reference" in node
|
|
2571
|
-
).map((ref) => {
|
|
2572
|
-
var _a, _b, _c;
|
|
2573
|
-
return {
|
|
2574
|
-
href: ((_a = ref[":@"]) == null ? void 0 : _a["@_href"]) ?? "",
|
|
2575
|
-
title: ((_b = ref[":@"]) == null ? void 0 : _b["@_title"]) ?? "",
|
|
2576
|
-
type: (((_c = ref[":@"]) == null ? void 0 : _c["@_type"]) ?? "").toLowerCase()
|
|
2577
|
-
};
|
|
2578
|
-
}).filter((entry) => entry.href);
|
|
2579
|
-
}
|
|
2580
|
-
discardAndClose() {
|
|
2581
|
-
this.rootfile = null;
|
|
2582
|
-
this.manifest = null;
|
|
2583
|
-
this.spine = null;
|
|
2584
|
-
void this.adapter.dispose();
|
|
2585
|
-
}
|
|
2586
|
-
/**
|
|
2587
|
-
* Write the current contents of the Epub to a new
|
|
2588
|
-
* EPUB archive on disk.
|
|
2589
|
-
*
|
|
2590
|
-
* When this method is called, the "dcterms:modified"
|
|
2591
|
-
* meta tag is automatically updated to the current UTC
|
|
2592
|
-
* timestamp.
|
|
2593
|
-
*/
|
|
2594
|
-
async saveAndClose() {
|
|
2595
|
-
this.assertWritable();
|
|
2596
|
-
if (!this.inputPath) {
|
|
2597
|
-
throw new Error("In-memory EPUB files cannot be saved to disk");
|
|
2598
|
-
}
|
|
2599
|
-
if (!this.adapter.serialize) {
|
|
2600
|
-
throw new Error(
|
|
2601
|
-
`adapter ${this.adapterClass.kind} does not support serialization`
|
|
2602
|
-
);
|
|
2603
|
-
}
|
|
2604
|
-
await this.replaceMetadata(
|
|
2605
|
-
(entry) => entry.properties["property"] === "dcterms:modified",
|
|
2606
|
-
{
|
|
2607
|
-
type: "meta",
|
|
2608
|
-
properties: { property: "dcterms:modified" },
|
|
2609
|
-
// We need UTC with integer seconds, but toISOString gives UTC with ms
|
|
2610
|
-
value: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+/, "")
|
|
2611
|
-
}
|
|
2612
|
-
);
|
|
2613
|
-
await this.adapter.serialize(this.inputPath);
|
|
2614
|
-
}
|
|
2615
|
-
/**
|
|
2616
|
-
* Upgrade an EPUB 2 publication to EPUB 3 in place, returning a new,
|
|
2617
|
-
* valid Epub 3 instance. Equivalent to
|
|
2618
|
-
* `Epub.using(TmpFsAdapter).upgrade(...)`.
|
|
2619
|
-
*/
|
|
2620
|
-
static async upgrade(path, options = {}) {
|
|
2621
|
-
return Epub.using(import_tmpfs.TmpFsAdapter).upgrade(path, options);
|
|
2622
|
-
}
|
|
2623
|
-
[Symbol.dispose]() {
|
|
2624
|
-
this.discardAndClose();
|
|
2625
|
-
}
|
|
2626
|
-
}
|
|
2627
|
-
class EpubFactory {
|
|
2628
|
-
constructor(adapterClass) {
|
|
2629
|
-
this.adapterClass = adapterClass;
|
|
2630
|
-
}
|
|
2631
|
-
async from(source, options = {}) {
|
|
2632
|
-
const adapter = await this.adapterClass.init(
|
|
2633
|
-
source,
|
|
2634
|
-
options
|
|
2635
|
-
);
|
|
2636
|
-
const inputPath = typeof source === "string" ? source : void 0;
|
|
2637
|
-
const readonlyOverride = options.readonly === true;
|
|
2638
|
-
const epub = new Epub(
|
|
2639
|
-
this.adapterClass,
|
|
2640
|
-
adapter,
|
|
2641
|
-
inputPath,
|
|
2642
|
-
readonlyOverride
|
|
2643
|
-
);
|
|
2644
|
-
try {
|
|
2645
|
-
await epub.getPackageElement();
|
|
2646
|
-
} catch (e) {
|
|
2647
|
-
epub.discardAndClose();
|
|
2648
|
-
console.error(e);
|
|
2649
|
-
throw new Error(
|
|
2650
|
-
"This is not a valid EPUB publication. Could not read the package document."
|
|
2651
|
-
);
|
|
2652
|
-
}
|
|
2653
|
-
try {
|
|
2654
|
-
await Epub.assertEpub3(epub);
|
|
2655
|
-
} catch (error) {
|
|
2656
|
-
epub.discardAndClose();
|
|
2657
|
-
throw error;
|
|
2658
|
-
}
|
|
2659
|
-
return epub;
|
|
2660
|
-
}
|
|
2661
|
-
/**
|
|
2662
|
-
* Construct a new EPUB on this factory's adapter, optionally seeded
|
|
2663
|
-
* with the provided metadata. Requires a writable adapter that
|
|
2664
|
-
* implements `initEmpty` (today: {@link TmpFsAdapter}).
|
|
2665
|
-
*
|
|
2666
|
-
* @throws when the adapter is read-only or does not implement initEmpty
|
|
2667
|
-
*/
|
|
2668
|
-
async create(path, {
|
|
2669
|
-
title,
|
|
2670
|
-
language,
|
|
2671
|
-
identifier,
|
|
2672
|
-
date,
|
|
2673
|
-
subjects,
|
|
2674
|
-
type,
|
|
2675
|
-
creators,
|
|
2676
|
-
contributors
|
|
2677
|
-
}, additionalMetadata = []) {
|
|
2678
|
-
if (!this.adapterClass.capabilities.writable) {
|
|
2679
|
-
throw new EpubReadOnlyError(
|
|
2680
|
-
`adapter ${this.adapterClass.kind} is read-only; cannot create`
|
|
2681
|
-
);
|
|
2682
|
-
}
|
|
2683
|
-
if (!this.adapterClass.initEmpty) {
|
|
2684
|
-
throw new Error(
|
|
2685
|
-
`adapter ${this.adapterClass.kind} does not support create() (missing initEmpty)`
|
|
2686
|
-
);
|
|
2687
|
-
}
|
|
2688
|
-
const adapter = await this.adapterClass.initEmpty();
|
|
2689
|
-
if (!adapter.write) {
|
|
2690
|
-
throw new Error(
|
|
2691
|
-
`adapter ${this.adapterClass.kind} declared writable but did not implement write()`
|
|
2692
|
-
);
|
|
2693
|
-
}
|
|
2694
|
-
const encoder = new TextEncoder();
|
|
2695
|
-
const container = encoder.encode(`<?xml version="1.0"?>
|
|
2696
|
-
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
|
2697
|
-
<rootfiles>
|
|
2698
|
-
<rootfile media-type="${import_media_types.MediaType.OPF.mime}" full-path="OEBPS/content.opf"/>
|
|
2699
|
-
</rootfiles>
|
|
2700
|
-
</container>
|
|
2701
|
-
`);
|
|
2702
|
-
await adapter.write("META-INF/container.xml", container);
|
|
2703
|
-
const packageDocument = encoder.encode(`<?xml version="1.0"?>
|
|
2704
|
-
<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/">
|
|
2705
|
-
<metadata>
|
|
2706
|
-
</metadata>
|
|
2707
|
-
<manifest>
|
|
2708
|
-
</manifest>
|
|
2709
|
-
<spine>
|
|
2710
|
-
</spine>
|
|
2711
|
-
</package>
|
|
2712
|
-
`);
|
|
2713
|
-
await adapter.write("OEBPS/content.opf", packageDocument);
|
|
2714
|
-
const epub = new Epub(this.adapterClass, adapter, path);
|
|
2715
|
-
const metadata = [
|
|
2716
|
-
{
|
|
2717
|
-
id: "pub-id",
|
|
2718
|
-
type: "dc:identifier",
|
|
2719
|
-
properties: {},
|
|
2720
|
-
value: identifier
|
|
2721
|
-
},
|
|
2722
|
-
...additionalMetadata
|
|
2723
|
-
];
|
|
2724
|
-
await Promise.all(metadata.map((entry) => epub.addMetadata(entry)));
|
|
2725
|
-
await epub.setTitle(title);
|
|
2726
|
-
await epub.setLanguage(language);
|
|
2727
|
-
if (date) await epub.setPublicationDate(date);
|
|
2728
|
-
if (type) await epub.setType(type);
|
|
2729
|
-
if (subjects) {
|
|
2730
|
-
await Promise.all(subjects.map((subject) => epub.addSubject(subject)));
|
|
2731
|
-
}
|
|
2732
|
-
if (creators) {
|
|
2733
|
-
await Promise.all(creators.map((creator) => epub.addCreator(creator)));
|
|
2734
|
-
}
|
|
2735
|
-
if (contributors) {
|
|
2736
|
-
await Promise.all(
|
|
2737
|
-
contributors.map((contributor) => epub.addCreator(contributor))
|
|
2738
|
-
);
|
|
2739
|
-
}
|
|
2740
|
-
return epub;
|
|
2741
|
-
}
|
|
2742
|
-
/**
|
|
2743
|
-
* Upgrade an EPUB 2 publication to EPUB 3 in place using this
|
|
2744
|
-
* factory's adapter, returning a new, valid Epub 3 instance.
|
|
2745
|
-
*
|
|
2746
|
-
* Performs the following transformations:
|
|
2747
|
-
* - upgrades OPF metadata to EPUB 3 conventions
|
|
2748
|
-
* - scans XHTML documents and adds manifest item properties
|
|
2749
|
-
* - parses the NCX into a TOC tree and generates a nav.xhtml
|
|
2750
|
-
* - removes the NCX file and the guide element (configurable)
|
|
2751
|
-
* - fixes common font MIME types
|
|
2752
|
-
* - bumps the package version to 3.0
|
|
2753
|
-
* - goes over each xhtml item and rewrites it using XMLParser to make sure the output is valid XHTML
|
|
2754
|
-
*
|
|
2755
|
-
* Requires a writable adapter. When {@link Upgrade.Epub2UpgradeOptions.outputPath}
|
|
2756
|
-
* is set, the source file is copied to that path on disk first; this
|
|
2757
|
-
* only makes sense for adapters whose `source` is a real fs path.
|
|
2758
|
-
*
|
|
2759
|
-
* @throws when the adapter is read-only
|
|
2760
|
-
*/
|
|
2761
|
-
async upgrade(path, options = {}) {
|
|
2762
|
-
if (!this.adapterClass.capabilities.writable) {
|
|
2763
|
-
throw new EpubReadOnlyError(
|
|
2764
|
-
`adapter ${this.adapterClass.kind} is read-only; cannot upgrade`
|
|
2765
|
-
);
|
|
2766
|
-
}
|
|
2767
|
-
const { removeNcx = false, outputPath } = options;
|
|
2768
|
-
if (outputPath) {
|
|
2769
|
-
await (0, import_promises.mkdir)((0, import_path.dirname)(outputPath), { recursive: true });
|
|
2770
|
-
await (0, import_promises.cp)(path, outputPath, { force: true });
|
|
2771
|
-
}
|
|
2772
|
-
const source = outputPath ?? path;
|
|
2773
|
-
const adapter = await this.adapterClass.init(
|
|
2774
|
-
source,
|
|
2775
|
-
options
|
|
2776
|
-
);
|
|
2777
|
-
const epub = new Epub(this.adapterClass, adapter, source);
|
|
2778
|
-
try {
|
|
2779
|
-
await epub.getPackageElement();
|
|
2780
|
-
} catch (e) {
|
|
2781
|
-
epub.discardAndClose();
|
|
2782
|
-
console.error(e);
|
|
2783
|
-
throw new Error(
|
|
2784
|
-
"This is not a valid EPUB publication. Could not read the package document."
|
|
2785
|
-
);
|
|
2786
|
-
}
|
|
2787
|
-
try {
|
|
2788
|
-
const version = await epub.getVersion();
|
|
2789
|
-
if (version.startsWith("3.")) {
|
|
2790
|
-
return epub;
|
|
2791
|
-
}
|
|
2792
|
-
const tocEntries = await epub.getNcxTableOfContents();
|
|
2793
|
-
let landmarks = [];
|
|
2794
|
-
await epub.withPackage((pkg) => {
|
|
2795
|
-
landmarks = Upgrade.extractGuideLandmarks(pkg);
|
|
2796
|
-
Upgrade.upgradePackageMetadata(pkg);
|
|
2797
|
-
Upgrade.fixFontMimeTypes(pkg);
|
|
2798
|
-
Upgrade.removeGuide(pkg);
|
|
2799
|
-
if (removeNcx) {
|
|
2800
|
-
Upgrade.removeSpineTocRef(pkg);
|
|
2801
|
-
}
|
|
2802
|
-
Upgrade.setPackageVersion(pkg, "3.0");
|
|
2803
|
-
});
|
|
2804
|
-
await Upgrade.collectManifestProperties(epub);
|
|
2805
|
-
if (removeNcx) {
|
|
2806
|
-
await Upgrade.removeNcx(epub);
|
|
2807
|
-
}
|
|
2808
|
-
const navHref = await Upgrade.chooseNavHref(epub);
|
|
2809
|
-
const navContent = await Upgrade.buildNavDocument(
|
|
2810
|
-
epub,
|
|
2811
|
-
tocEntries,
|
|
2812
|
-
landmarks
|
|
2813
|
-
);
|
|
2814
|
-
await epub.addManifestItem(
|
|
2815
|
-
{
|
|
2816
|
-
id: "nav",
|
|
2817
|
-
href: navHref,
|
|
2818
|
-
mediaType: import_media_types.MediaType.XHTML.mime,
|
|
2819
|
-
properties: ["nav"]
|
|
2820
|
-
},
|
|
2821
|
-
navContent,
|
|
2822
|
-
"utf-8"
|
|
2823
|
-
);
|
|
2824
|
-
const manifest = await epub.getManifest();
|
|
2825
|
-
for (const item of Object.values(manifest)) {
|
|
2826
|
-
if (import_media_types.MediaType.fromMime(item.mediaType ?? "") !== import_media_types.MediaType.XHTML)
|
|
2827
|
-
continue;
|
|
2828
|
-
const contents = await epub.readXhtmlItemContents(item.id);
|
|
2829
|
-
await epub.writeXhtmlItemContents(item.id, contents);
|
|
2830
|
-
}
|
|
2831
|
-
return epub;
|
|
2832
|
-
} catch (error) {
|
|
2833
|
-
epub.discardAndClose();
|
|
2834
|
-
throw error;
|
|
2835
|
-
}
|
|
2836
|
-
}
|
|
2837
|
-
}
|
|
2838
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
2839
|
-
0 && (module.exports = {
|
|
2840
|
-
Epub,
|
|
2841
|
-
EpubFactory,
|
|
2842
|
-
EpubReadOnlyError,
|
|
2843
|
-
EpubVersionError,
|
|
2844
|
-
MemoryAdapter,
|
|
2845
|
-
TmpFsAdapter
|
|
2846
|
-
});
|