@storyteller-platform/epub 0.7.0 → 0.7.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 +270 -1150
- package/dist/adapters/interface.d.cts +25 -19
- package/dist/adapters/interface.d.ts +25 -19
- package/dist/adapters/memory.cjs +19 -29
- package/dist/adapters/memory.d.cts +4 -5
- package/dist/adapters/memory.d.ts +4 -5
- package/dist/adapters/memory.js +19 -32
- package/dist/adapters/tmpfs.cjs +50 -46
- package/dist/adapters/tmpfs.d.cts +9 -7
- package/dist/adapters/tmpfs.d.ts +9 -7
- package/dist/adapters/tmpfs.js +50 -46
- package/dist/epubcheck.cjs +2 -2
- package/dist/epubcheck.js +2 -2
- package/dist/index.cjs +190 -93
- package/dist/index.js +191 -99
- package/dist/upgrade.cjs +12 -44
- package/dist/upgrade.d.cts +22 -8
- package/dist/upgrade.d.ts +22 -8
- package/dist/upgrade.js +12 -44
- package/dist/zip.cjs +142 -0
- package/dist/zip.d.cts +5 -0
- package/dist/zip.d.ts +5 -0
- package/dist/zip.js +124 -0
- package/package.json +4 -5
package/dist/adapters/tmpfs.js
CHANGED
|
@@ -13,39 +13,23 @@ import {
|
|
|
13
13
|
stat,
|
|
14
14
|
writeFile
|
|
15
15
|
} from "node:fs/promises";
|
|
16
|
-
import { tmpdir } from "node:os";
|
|
17
16
|
import { pipeline } from "node:stream/promises";
|
|
18
|
-
import { fromBuffer, open } from "yauzl-promise";
|
|
19
17
|
import { ZipFile } from "yazl";
|
|
18
|
+
import { scratchPath } from "@storyteller-platform/fs";
|
|
19
|
+
import { MediaType } from "@storyteller-platform/media-types";
|
|
20
20
|
import { dirname, join, sep } from "@storyteller-platform/path";
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const FLAC_FILE_EXTENSIONS = [".flac"];
|
|
29
|
-
const ALAC_FILE_EXTENSIONS = [".alac"];
|
|
30
|
-
const WEBM_FILE_EXTENSIONS = [".weba"];
|
|
31
|
-
const AUDIO_FILE_EXTENSIONS = [
|
|
32
|
-
...MP3_FILE_EXTENSIONS,
|
|
33
|
-
...AAC_FILE_EXTENSIONS,
|
|
34
|
-
...MPEG4_FILE_EXTENSIONS,
|
|
35
|
-
...OPUS_FILE_EXTENSIONS,
|
|
36
|
-
...OGG_FILE_EXTENSIONS,
|
|
37
|
-
...WAVE_FILE_EXTENSIONS,
|
|
38
|
-
...AIFF_FILE_EXTENSIONS,
|
|
39
|
-
...FLAC_FILE_EXTENSIONS,
|
|
40
|
-
...ALAC_FILE_EXTENSIONS,
|
|
41
|
-
...WEBM_FILE_EXTENSIONS
|
|
42
|
-
];
|
|
43
|
-
function isAudioFile(filenameOrExt) {
|
|
44
|
-
const lowered = filenameOrExt.toLowerCase();
|
|
45
|
-
return AUDIO_FILE_EXTENSIONS.some((ext) => lowered.endsWith(ext));
|
|
46
|
-
}
|
|
21
|
+
import { openZip } from "../zip.js";
|
|
22
|
+
const PRECOMPRESSED_KINDS = /* @__PURE__ */ new Set([
|
|
23
|
+
"audio",
|
|
24
|
+
"image",
|
|
25
|
+
"archive",
|
|
26
|
+
"package"
|
|
27
|
+
]);
|
|
47
28
|
function mintRootPath() {
|
|
48
|
-
return
|
|
29
|
+
return scratchPath(`storyteller-platform-epub-${randomUUID()}`);
|
|
30
|
+
}
|
|
31
|
+
function isStorableEntryName(entry) {
|
|
32
|
+
return !entry.split("/").some((segment) => segment === "" || segment === "." || segment === "..");
|
|
49
33
|
}
|
|
50
34
|
class TmpFsAdapter {
|
|
51
35
|
constructor(rootPath) {
|
|
@@ -58,7 +42,7 @@ class TmpFsAdapter {
|
|
|
58
42
|
try {
|
|
59
43
|
var _stack = [];
|
|
60
44
|
try {
|
|
61
|
-
const zipfile =
|
|
45
|
+
const zipfile = await openZip(source);
|
|
62
46
|
const stack = __using(_stack, new AsyncDisposableStack(), true);
|
|
63
47
|
stack.defer(async () => {
|
|
64
48
|
await zipfile.close();
|
|
@@ -67,7 +51,8 @@ class TmpFsAdapter {
|
|
|
67
51
|
if (entry.filename.endsWith("/")) {
|
|
68
52
|
continue;
|
|
69
53
|
}
|
|
70
|
-
|
|
54
|
+
if (!isStorableEntryName(entry.filename)) continue;
|
|
55
|
+
const writePath = join(rootPath, ...entry.filename.split("/"));
|
|
71
56
|
const readStream = await entry.openReadStream();
|
|
72
57
|
await mkdir(dirname(writePath), { recursive: true });
|
|
73
58
|
const writeStream = createWriteStream(writePath);
|
|
@@ -90,19 +75,36 @@ class TmpFsAdapter {
|
|
|
90
75
|
await mkdir(rootPath, { recursive: true });
|
|
91
76
|
return new TmpFsAdapter(rootPath);
|
|
92
77
|
}
|
|
93
|
-
|
|
94
|
-
|
|
78
|
+
diskPath(entry) {
|
|
79
|
+
if (!isStorableEntryName(entry)) {
|
|
80
|
+
const err = new Error(`ENOENT: no such entry in EPUB archive: ${entry}`);
|
|
81
|
+
err.code = "ENOENT";
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
return join(this.rootPath, ...entry.split("/"));
|
|
85
|
+
}
|
|
86
|
+
async read(entry, encoding) {
|
|
87
|
+
return await readFile(this.diskPath(entry), encoding);
|
|
95
88
|
}
|
|
96
|
-
async write(
|
|
89
|
+
async write(entry, data, encoding) {
|
|
90
|
+
const path = this.diskPath(entry);
|
|
97
91
|
await mkdir(dirname(path), { recursive: true });
|
|
98
92
|
await writeFile(path, data, encoding);
|
|
99
93
|
}
|
|
100
|
-
async remove(
|
|
101
|
-
await rm(
|
|
94
|
+
async remove(entry) {
|
|
95
|
+
await rm(this.diskPath(entry));
|
|
96
|
+
}
|
|
97
|
+
async exists(entry) {
|
|
98
|
+
try {
|
|
99
|
+
await stat(this.diskPath(entry));
|
|
100
|
+
return true;
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
102
104
|
}
|
|
103
|
-
async archiveLength(
|
|
105
|
+
async archiveLength(entry) {
|
|
104
106
|
try {
|
|
105
|
-
const st = await stat(
|
|
107
|
+
const st = await stat(this.diskPath(entry));
|
|
106
108
|
return st.size;
|
|
107
109
|
} catch {
|
|
108
110
|
return 0;
|
|
@@ -118,7 +120,7 @@ class TmpFsAdapter {
|
|
|
118
120
|
const absolutePath = join(entry.parentPath, entry.name);
|
|
119
121
|
yield {
|
|
120
122
|
absolutePath,
|
|
121
|
-
relativePath: absolutePath.
|
|
123
|
+
relativePath: absolutePath.slice(this.rootPath.length + 1).split(sep).join("/")
|
|
122
124
|
};
|
|
123
125
|
}
|
|
124
126
|
}
|
|
@@ -133,31 +135,33 @@ class TmpFsAdapter {
|
|
|
133
135
|
return new TmpFsAdapter(newRoot);
|
|
134
136
|
}
|
|
135
137
|
async serialize(targetPath) {
|
|
138
|
+
var _a;
|
|
136
139
|
var _stack = [];
|
|
137
140
|
try {
|
|
138
|
-
const tmpArchivePath =
|
|
139
|
-
|
|
140
|
-
`storyteller-platform-epub-${randomUUID()}`
|
|
141
|
-
);
|
|
142
|
-
const { promise, resolve } = Promise.withResolvers();
|
|
141
|
+
const tmpArchivePath = mintRootPath();
|
|
142
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
143
143
|
const zipfile = new ZipFile();
|
|
144
144
|
const writeStream = createWriteStream(tmpArchivePath);
|
|
145
145
|
writeStream.on("close", () => {
|
|
146
146
|
resolve();
|
|
147
147
|
});
|
|
148
|
+
writeStream.on("error", reject);
|
|
149
|
+
zipfile.outputStream.on("error", reject);
|
|
148
150
|
const stack = __using(_stack, new AsyncDisposableStack(), true);
|
|
149
151
|
stack.defer(async () => {
|
|
150
152
|
writeStream.close();
|
|
151
153
|
await rm(tmpArchivePath, { force: true });
|
|
152
154
|
});
|
|
153
155
|
zipfile.outputStream.pipe(writeStream);
|
|
154
|
-
zipfile.addBuffer(Buffer.from(
|
|
156
|
+
zipfile.addBuffer(Buffer.from(MediaType.EPUB.mime), "mimetype", {
|
|
155
157
|
compress: false
|
|
156
158
|
});
|
|
157
159
|
for await (const entry of this.list()) {
|
|
158
160
|
if (entry.relativePath === "mimetype") continue;
|
|
159
161
|
zipfile.addFile(entry.absolutePath, entry.relativePath, {
|
|
160
|
-
compress: !
|
|
162
|
+
compress: !PRECOMPRESSED_KINDS.has(
|
|
163
|
+
(_a = MediaType.fromPath(entry.absolutePath)) == null ? void 0 : _a.kind
|
|
164
|
+
)
|
|
161
165
|
});
|
|
162
166
|
}
|
|
163
167
|
zipfile.end();
|
package/dist/epubcheck.cjs
CHANGED
|
@@ -24,15 +24,15 @@ module.exports = __toCommonJS(epubcheck_exports);
|
|
|
24
24
|
var import_node_child_process = require("node:child_process");
|
|
25
25
|
var import_node_crypto = require("node:crypto");
|
|
26
26
|
var import_promises = require("node:fs/promises");
|
|
27
|
-
var import_node_os = require("node:os");
|
|
28
27
|
var import_node_path = require("node:path");
|
|
29
28
|
var import_node_url = require("node:url");
|
|
30
29
|
var import_node_util = require("node:util");
|
|
30
|
+
var import_fs = require("@storyteller-platform/fs");
|
|
31
31
|
const import_meta = {};
|
|
32
32
|
const here = (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
33
33
|
const JAR = (0, import_node_path.join)(here, "vendors", "epubcheck-5.3.0", "epubcheck.jar");
|
|
34
34
|
async function epubcheck(epubFile) {
|
|
35
|
-
const output = (0,
|
|
35
|
+
const output = (0, import_fs.scratchPath)(`epubcheck-${(0, import_node_crypto.randomUUID)()}.json`);
|
|
36
36
|
const execFilePromise = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
37
37
|
try {
|
|
38
38
|
await execFilePromise("java", [
|
package/dist/epubcheck.js
CHANGED
|
@@ -2,14 +2,14 @@ import "./chunk-BIEQXUOY.js";
|
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { readFile, unlink } from "node:fs/promises";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
5
|
import { dirname, join } from "node:path";
|
|
7
6
|
import { fileURLToPath } from "node:url";
|
|
8
7
|
import { promisify } from "node:util";
|
|
8
|
+
import { scratchPath } from "@storyteller-platform/fs";
|
|
9
9
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
const JAR = join(here, "vendors", "epubcheck-5.3.0", "epubcheck.jar");
|
|
11
11
|
async function epubcheck(epubFile) {
|
|
12
|
-
const output =
|
|
12
|
+
const output = scratchPath(`epubcheck-${randomUUID()}.json`);
|
|
13
13
|
const execFilePromise = promisify(execFile);
|
|
14
14
|
try {
|
|
15
15
|
await execFilePromise("java", [
|
package/dist/index.cjs
CHANGED
|
@@ -40,8 +40,8 @@ var import_promises = require("node:fs/promises");
|
|
|
40
40
|
var import_async_mutex = require("async-mutex");
|
|
41
41
|
var import_fast_xml_parser = require("fast-xml-parser");
|
|
42
42
|
var import_mem = __toESM(require("mem"), 1);
|
|
43
|
-
var import_mime_types = require("mime-types");
|
|
44
43
|
var import_nanoid = require("nanoid");
|
|
44
|
+
var import_media_types = require("@storyteller-platform/media-types");
|
|
45
45
|
var import_path = require("@storyteller-platform/path");
|
|
46
46
|
var import_tmpfs = require("./adapters/tmpfs.cjs");
|
|
47
47
|
var Upgrade = __toESM(require("./upgrade.ts"), 1);
|
|
@@ -51,6 +51,61 @@ class EpubVersionError extends Error {
|
|
|
51
51
|
}
|
|
52
52
|
class EpubReadOnlyError extends Error {
|
|
53
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
|
+
}
|
|
54
109
|
class Epub {
|
|
55
110
|
/**
|
|
56
111
|
* Prefer the static factories ({@link Epub.using}, {@link Epub.from},
|
|
@@ -331,14 +386,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
331
386
|
);
|
|
332
387
|
}
|
|
333
388
|
const rootfile = await this.getRootfile();
|
|
334
|
-
|
|
335
|
-
await this.adapter.remove(filename);
|
|
336
|
-
}
|
|
337
|
-
async getFileData(path, encoding) {
|
|
338
|
-
if (encoding) {
|
|
339
|
-
return this.adapter.read(path, encoding);
|
|
340
|
-
}
|
|
341
|
-
return this.adapter.read(path);
|
|
389
|
+
await this.adapter.remove(await this.resolveEntry(rootfile, href));
|
|
342
390
|
}
|
|
343
391
|
/**
|
|
344
392
|
* Length of the underlying archive entry for a manifest item, in bytes
|
|
@@ -351,14 +399,15 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
351
399
|
const manifestItem = manifest[id];
|
|
352
400
|
if (!manifestItem)
|
|
353
401
|
throw new Error(`Could not find item with id "${id}" in manifest`);
|
|
354
|
-
|
|
355
|
-
|
|
402
|
+
return this.adapter.archiveLength(
|
|
403
|
+
await this.resolveEntry(rootfile, manifestItem.href)
|
|
404
|
+
);
|
|
356
405
|
}
|
|
357
406
|
async getRootfile() {
|
|
358
407
|
var _a;
|
|
359
408
|
if (this.rootfile !== null) return this.rootfile;
|
|
360
|
-
const containerString = await this.
|
|
361
|
-
|
|
409
|
+
const containerString = await this.adapter.read(
|
|
410
|
+
"META-INF/container.xml",
|
|
362
411
|
"utf-8"
|
|
363
412
|
);
|
|
364
413
|
if (!containerString)
|
|
@@ -382,7 +431,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
382
431
|
Epub.getXmlChildren(rootfiles),
|
|
383
432
|
(node) => {
|
|
384
433
|
var _a2;
|
|
385
|
-
return !Epub.isXmlTextNode(node) && ((_a2 = node[":@"]) == null ? void 0 : _a2["@_media-type"]) ===
|
|
434
|
+
return !Epub.isXmlTextNode(node) && ((_a2 = node[":@"]) == null ? void 0 : _a2["@_media-type"]) === import_media_types.MediaType.OPF.mime;
|
|
386
435
|
}
|
|
387
436
|
);
|
|
388
437
|
const fullPath = (_a = rootfile == null ? void 0 : rootfile[":@"]) == null ? void 0 : _a["@_full-path"];
|
|
@@ -390,12 +439,12 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
390
439
|
throw new Error(
|
|
391
440
|
"Failed to parse EPUB container.xml: Found no rootfile element"
|
|
392
441
|
);
|
|
393
|
-
this.rootfile =
|
|
442
|
+
this.rootfile = await this.resolveEntry("", fullPath);
|
|
394
443
|
return this.rootfile;
|
|
395
444
|
}
|
|
396
445
|
async getPackageDocument() {
|
|
397
446
|
const rootfile = await this.getRootfile();
|
|
398
|
-
const packageDocumentString = await this.
|
|
447
|
+
const packageDocumentString = await this.adapter.read(rootfile, "utf-8");
|
|
399
448
|
if (!packageDocumentString)
|
|
400
449
|
throw new Error(
|
|
401
450
|
`Failed to parse EPUB: could not find package document at ${rootfile}`
|
|
@@ -403,7 +452,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
403
452
|
const packageDocument = Epub.xmlParser.parse(
|
|
404
453
|
packageDocumentString
|
|
405
454
|
);
|
|
406
|
-
return packageDocument;
|
|
455
|
+
return normalizeOpfNamespacePrefixes(packageDocument);
|
|
407
456
|
}
|
|
408
457
|
async getPackageElement() {
|
|
409
458
|
const packageDocument = await this.getPackageDocument();
|
|
@@ -1075,11 +1124,12 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1075
1124
|
* the provided href within the publication.
|
|
1076
1125
|
*/
|
|
1077
1126
|
async setCoverImage(href, data) {
|
|
1127
|
+
var _a;
|
|
1078
1128
|
const coverImageItem = await this.getCoverImageItem();
|
|
1079
1129
|
if (coverImageItem) {
|
|
1080
1130
|
await this.removeManifestItem(coverImageItem.id);
|
|
1081
1131
|
}
|
|
1082
|
-
const mediaType = (
|
|
1132
|
+
const mediaType = (_a = import_media_types.MediaType.fromPath(href)) == null ? void 0 : _a.mime;
|
|
1083
1133
|
if (!mediaType)
|
|
1084
1134
|
throw new Error(`Invalid file extension for cover image: ${href}`);
|
|
1085
1135
|
await this.addManifestItem(
|
|
@@ -2058,35 +2108,81 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
2058
2108
|
return this.getNavigation("page-list", { resolveToRoot });
|
|
2059
2109
|
}
|
|
2060
2110
|
/**
|
|
2061
|
-
*
|
|
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.
|
|
2062
2119
|
*/
|
|
2063
|
-
|
|
2064
|
-
const
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
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);
|
|
2070
2151
|
}
|
|
2071
2152
|
/**
|
|
2072
2153
|
* Returns a path-relative-scheme-less URL, relative to the
|
|
2073
2154
|
* container root.
|
|
2074
2155
|
*
|
|
2156
|
+
* An href that points outside the publication, such as an external link in
|
|
2157
|
+
* a navigation document, is handed back unchanged.
|
|
2158
|
+
*
|
|
2075
2159
|
* @param href The href to resolve
|
|
2076
2160
|
* @param [relativeTo] Optional - The href to resolve this href relative to.
|
|
2077
2161
|
Use if resolving a relative href from a file other than the package document.
|
|
2078
2162
|
*/
|
|
2079
2163
|
async resolveHref(href, relativeTo, { toRoot } = {}) {
|
|
2164
|
+
if (href.startsWith("#")) return href;
|
|
2080
2165
|
const rootfile = await this.getRootfile();
|
|
2081
|
-
const from = relativeTo ? this.
|
|
2082
|
-
const
|
|
2083
|
-
|
|
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;
|
|
2084
2180
|
}
|
|
2085
2181
|
async readFileContents(href, relativeTo, encoding) {
|
|
2086
2182
|
const rootfile = await this.getRootfile();
|
|
2087
|
-
const from = relativeTo ? this.
|
|
2088
|
-
const
|
|
2089
|
-
const itemEntry = encoding ? await this.
|
|
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);
|
|
2090
2186
|
return itemEntry;
|
|
2091
2187
|
}
|
|
2092
2188
|
async readItemContents(id, encoding) {
|
|
@@ -2095,8 +2191,8 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
2095
2191
|
const manifestItem = manifest[id];
|
|
2096
2192
|
if (!manifestItem)
|
|
2097
2193
|
throw new Error(`Could not find item with id "${id}" in manifest`);
|
|
2098
|
-
const
|
|
2099
|
-
const itemEntry = encoding ? await this.
|
|
2194
|
+
const entry = await this.resolveEntry(rootfile, manifestItem.href);
|
|
2195
|
+
const itemEntry = encoding ? await this.adapter.read(entry, encoding) : await this.adapter.read(entry);
|
|
2100
2196
|
return itemEntry;
|
|
2101
2197
|
}
|
|
2102
2198
|
/**
|
|
@@ -2154,11 +2250,11 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
2154
2250
|
if (!manifestItem)
|
|
2155
2251
|
throw new Error(`Could not find item with id "${id}" in manifest`);
|
|
2156
2252
|
import_mem.default.clear(this.readXhtmlItemContents);
|
|
2157
|
-
const
|
|
2253
|
+
const entry = await this.resolveEntry(rootfile, manifestItem.href);
|
|
2158
2254
|
if (encoding === "utf-8") {
|
|
2159
|
-
await this.writeEntryContents(
|
|
2255
|
+
await this.writeEntryContents(entry, contents, encoding);
|
|
2160
2256
|
} else {
|
|
2161
|
-
await this.writeEntryContents(
|
|
2257
|
+
await this.writeEntryContents(entry, contents);
|
|
2162
2258
|
}
|
|
2163
2259
|
}
|
|
2164
2260
|
/**
|
|
@@ -2229,7 +2325,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
2229
2325
|
});
|
|
2230
2326
|
this.manifest = null;
|
|
2231
2327
|
const rootfile = await this.getRootfile();
|
|
2232
|
-
const filename = this.
|
|
2328
|
+
const filename = await this.resolveEntry(rootfile, item.href);
|
|
2233
2329
|
const data = encoding === "utf-8" || encoding === "xml" ? new TextEncoder().encode(
|
|
2234
2330
|
encoding === "utf-8" ? contents : await Epub.xmlBuilder.build(
|
|
2235
2331
|
contents
|
|
@@ -2413,10 +2509,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
2413
2509
|
);
|
|
2414
2510
|
const spineTocId = (_a = spine == null ? void 0 : spine[":@"]) == null ? void 0 : _a["@_toc"];
|
|
2415
2511
|
const ncxItem = spineTocId ? manifest[spineTocId] : Object.values(manifest).find(
|
|
2416
|
-
(item) =>
|
|
2417
|
-
var _a2;
|
|
2418
|
-
return ((_a2 = item.mediaType) == null ? void 0 : _a2.toLowerCase()) === "application/x-dtbncx+xml";
|
|
2419
|
-
}
|
|
2512
|
+
(item) => import_media_types.MediaType.fromMime(item.mediaType ?? "") === import_media_types.MediaType.NCX
|
|
2420
2513
|
);
|
|
2421
2514
|
if (!ncxItem) return [];
|
|
2422
2515
|
const ncxContent = await this.readItemContents(ncxItem.id, "utf-8");
|
|
@@ -2557,7 +2650,12 @@ class EpubFactory {
|
|
|
2557
2650
|
"This is not a valid EPUB publication. Could not read the package document."
|
|
2558
2651
|
);
|
|
2559
2652
|
}
|
|
2560
|
-
|
|
2653
|
+
try {
|
|
2654
|
+
await Epub.assertEpub3(epub);
|
|
2655
|
+
} catch (error) {
|
|
2656
|
+
epub.discardAndClose();
|
|
2657
|
+
throw error;
|
|
2658
|
+
}
|
|
2561
2659
|
return epub;
|
|
2562
2660
|
}
|
|
2563
2661
|
/**
|
|
@@ -2597,14 +2695,11 @@ class EpubFactory {
|
|
|
2597
2695
|
const container = encoder.encode(`<?xml version="1.0"?>
|
|
2598
2696
|
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
|
2599
2697
|
<rootfiles>
|
|
2600
|
-
<rootfile media-type="
|
|
2698
|
+
<rootfile media-type="${import_media_types.MediaType.OPF.mime}" full-path="OEBPS/content.opf"/>
|
|
2601
2699
|
</rootfiles>
|
|
2602
2700
|
</container>
|
|
2603
2701
|
`);
|
|
2604
|
-
await adapter.write(
|
|
2605
|
-
(0, import_path.join)(adapter.rootPath, "META-INF", "container.xml"),
|
|
2606
|
-
container
|
|
2607
|
-
);
|
|
2702
|
+
await adapter.write("META-INF/container.xml", container);
|
|
2608
2703
|
const packageDocument = encoder.encode(`<?xml version="1.0"?>
|
|
2609
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/">
|
|
2610
2705
|
<metadata>
|
|
@@ -2615,10 +2710,7 @@ class EpubFactory {
|
|
|
2615
2710
|
</spine>
|
|
2616
2711
|
</package>
|
|
2617
2712
|
`);
|
|
2618
|
-
await adapter.write(
|
|
2619
|
-
(0, import_path.join)(adapter.rootPath, "OEBPS", "content.opf"),
|
|
2620
|
-
packageDocument
|
|
2621
|
-
);
|
|
2713
|
+
await adapter.write("OEBPS/content.opf", packageDocument);
|
|
2622
2714
|
const epub = new Epub(this.adapterClass, adapter, path);
|
|
2623
2715
|
const metadata = [
|
|
2624
2716
|
{
|
|
@@ -2667,7 +2759,6 @@ class EpubFactory {
|
|
|
2667
2759
|
* @throws when the adapter is read-only
|
|
2668
2760
|
*/
|
|
2669
2761
|
async upgrade(path, options = {}) {
|
|
2670
|
-
var _a;
|
|
2671
2762
|
if (!this.adapterClass.capabilities.writable) {
|
|
2672
2763
|
throw new EpubReadOnlyError(
|
|
2673
2764
|
`adapter ${this.adapterClass.kind} is read-only; cannot upgrade`
|
|
@@ -2693,49 +2784,55 @@ class EpubFactory {
|
|
|
2693
2784
|
"This is not a valid EPUB publication. Could not read the package document."
|
|
2694
2785
|
);
|
|
2695
2786
|
}
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
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);
|
|
2707
2805
|
if (removeNcx) {
|
|
2708
|
-
Upgrade.
|
|
2806
|
+
await Upgrade.removeNcx(epub);
|
|
2709
2807
|
}
|
|
2710
|
-
Upgrade.
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
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;
|
|
2737
2835
|
}
|
|
2738
|
-
return epub;
|
|
2739
2836
|
}
|
|
2740
2837
|
}
|
|
2741
2838
|
// Annotate the CommonJS export names for ESM import in node:
|