@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/index.js
CHANGED
|
@@ -3,14 +3,9 @@ import { cp, mkdir } from "node:fs/promises";
|
|
|
3
3
|
import { Mutex } from "async-mutex";
|
|
4
4
|
import { XMLBuilder, XMLParser } from "fast-xml-parser";
|
|
5
5
|
import memoize from "mem";
|
|
6
|
-
import { lookup } from "mime-types";
|
|
7
6
|
import { nanoid } from "nanoid";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
hrefToPlatformPath,
|
|
11
|
-
join,
|
|
12
|
-
resolve
|
|
13
|
-
} from "@storyteller-platform/path";
|
|
7
|
+
import { MediaType } from "@storyteller-platform/media-types";
|
|
8
|
+
import { dirname } from "@storyteller-platform/path";
|
|
14
9
|
import { TmpFsAdapter } from "./adapters/tmpfs.js";
|
|
15
10
|
import * as Upgrade from "./upgrade.js";
|
|
16
11
|
import { MemoryAdapter } from "./adapters/memory.js";
|
|
@@ -19,6 +14,61 @@ class EpubVersionError extends Error {
|
|
|
19
14
|
}
|
|
20
15
|
class EpubReadOnlyError extends Error {
|
|
21
16
|
}
|
|
17
|
+
const OPF_NAMESPACE = "http://www.idpf.org/2007/opf";
|
|
18
|
+
const ENTRY_ORIGIN = "epub:///";
|
|
19
|
+
function entryToUrlPath(entry) {
|
|
20
|
+
return entry.split("/").map(encodeURIComponent).join("/");
|
|
21
|
+
}
|
|
22
|
+
function resolveEntryUrl(from, href) {
|
|
23
|
+
const base = new URL(entryToUrlPath(from), ENTRY_ORIGIN);
|
|
24
|
+
if (!URL.canParse(href, base.toString())) return null;
|
|
25
|
+
const url = new URL(href, base);
|
|
26
|
+
return url.href.startsWith(ENTRY_ORIGIN) ? url : null;
|
|
27
|
+
}
|
|
28
|
+
function renameOpfAttributes(attrs, prefix) {
|
|
29
|
+
const attrPrefix = `@_${prefix}:`;
|
|
30
|
+
const renamed = {};
|
|
31
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
32
|
+
if (key === `@_xmlns:${prefix}`) continue;
|
|
33
|
+
if (key.startsWith(attrPrefix)) {
|
|
34
|
+
renamed[`@_opf:${key.slice(attrPrefix.length)}`] = value;
|
|
35
|
+
} else {
|
|
36
|
+
renamed[key] = value;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return renamed;
|
|
40
|
+
}
|
|
41
|
+
function stripOpfPrefix(node, prefix) {
|
|
42
|
+
if (Epub.isXmlTextNode(node)) return node;
|
|
43
|
+
const name = Epub.getXmlElementName(node);
|
|
44
|
+
const bareName = name.startsWith(`${prefix}:`) ? name.slice(prefix.length + 1) : name;
|
|
45
|
+
const element = {
|
|
46
|
+
[bareName]: Epub.getXmlChildren(node).map(
|
|
47
|
+
(child) => stripOpfPrefix(child, prefix)
|
|
48
|
+
)
|
|
49
|
+
};
|
|
50
|
+
if (node[":@"]) {
|
|
51
|
+
element[":@"] = renameOpfAttributes(node[":@"], prefix);
|
|
52
|
+
}
|
|
53
|
+
return element;
|
|
54
|
+
}
|
|
55
|
+
function normalizeOpfNamespacePrefixes(doc) {
|
|
56
|
+
var _a;
|
|
57
|
+
const root = doc.find(
|
|
58
|
+
(node) => !Epub.isXmlTextNode(node) && Epub.getXmlElementName(node).endsWith(":package")
|
|
59
|
+
);
|
|
60
|
+
if (!root) return doc;
|
|
61
|
+
const prefix = Epub.getXmlElementName(root).slice(0, -":package".length);
|
|
62
|
+
if (((_a = root[":@"]) == null ? void 0 : _a[`@_xmlns:${prefix}`]) !== OPF_NAMESPACE) return doc;
|
|
63
|
+
const stripped = stripOpfPrefix(root, prefix);
|
|
64
|
+
stripped[":@"] = {
|
|
65
|
+
...stripped[":@"],
|
|
66
|
+
"@_xmlns": OPF_NAMESPACE,
|
|
67
|
+
"@_xmlns:opf": OPF_NAMESPACE
|
|
68
|
+
};
|
|
69
|
+
doc[doc.indexOf(root)] = stripped;
|
|
70
|
+
return doc;
|
|
71
|
+
}
|
|
22
72
|
class Epub {
|
|
23
73
|
/**
|
|
24
74
|
* Prefer the static factories ({@link Epub.using}, {@link Epub.from},
|
|
@@ -299,14 +349,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
299
349
|
);
|
|
300
350
|
}
|
|
301
351
|
const rootfile = await this.getRootfile();
|
|
302
|
-
|
|
303
|
-
await this.adapter.remove(filename);
|
|
304
|
-
}
|
|
305
|
-
async getFileData(path, encoding) {
|
|
306
|
-
if (encoding) {
|
|
307
|
-
return this.adapter.read(path, encoding);
|
|
308
|
-
}
|
|
309
|
-
return this.adapter.read(path);
|
|
352
|
+
await this.adapter.remove(await this.resolveEntry(rootfile, href));
|
|
310
353
|
}
|
|
311
354
|
/**
|
|
312
355
|
* Length of the underlying archive entry for a manifest item, in bytes
|
|
@@ -319,14 +362,15 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
319
362
|
const manifestItem = manifest[id];
|
|
320
363
|
if (!manifestItem)
|
|
321
364
|
throw new Error(`Could not find item with id "${id}" in manifest`);
|
|
322
|
-
|
|
323
|
-
|
|
365
|
+
return this.adapter.archiveLength(
|
|
366
|
+
await this.resolveEntry(rootfile, manifestItem.href)
|
|
367
|
+
);
|
|
324
368
|
}
|
|
325
369
|
async getRootfile() {
|
|
326
370
|
var _a;
|
|
327
371
|
if (this.rootfile !== null) return this.rootfile;
|
|
328
|
-
const containerString = await this.
|
|
329
|
-
|
|
372
|
+
const containerString = await this.adapter.read(
|
|
373
|
+
"META-INF/container.xml",
|
|
330
374
|
"utf-8"
|
|
331
375
|
);
|
|
332
376
|
if (!containerString)
|
|
@@ -350,7 +394,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
350
394
|
Epub.getXmlChildren(rootfiles),
|
|
351
395
|
(node) => {
|
|
352
396
|
var _a2;
|
|
353
|
-
return !Epub.isXmlTextNode(node) && ((_a2 = node[":@"]) == null ? void 0 : _a2["@_media-type"]) ===
|
|
397
|
+
return !Epub.isXmlTextNode(node) && ((_a2 = node[":@"]) == null ? void 0 : _a2["@_media-type"]) === MediaType.OPF.mime;
|
|
354
398
|
}
|
|
355
399
|
);
|
|
356
400
|
const fullPath = (_a = rootfile == null ? void 0 : rootfile[":@"]) == null ? void 0 : _a["@_full-path"];
|
|
@@ -358,12 +402,12 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
358
402
|
throw new Error(
|
|
359
403
|
"Failed to parse EPUB container.xml: Found no rootfile element"
|
|
360
404
|
);
|
|
361
|
-
this.rootfile =
|
|
405
|
+
this.rootfile = await this.resolveEntry("", fullPath);
|
|
362
406
|
return this.rootfile;
|
|
363
407
|
}
|
|
364
408
|
async getPackageDocument() {
|
|
365
409
|
const rootfile = await this.getRootfile();
|
|
366
|
-
const packageDocumentString = await this.
|
|
410
|
+
const packageDocumentString = await this.adapter.read(rootfile, "utf-8");
|
|
367
411
|
if (!packageDocumentString)
|
|
368
412
|
throw new Error(
|
|
369
413
|
`Failed to parse EPUB: could not find package document at ${rootfile}`
|
|
@@ -371,7 +415,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
371
415
|
const packageDocument = Epub.xmlParser.parse(
|
|
372
416
|
packageDocumentString
|
|
373
417
|
);
|
|
374
|
-
return packageDocument;
|
|
418
|
+
return normalizeOpfNamespacePrefixes(packageDocument);
|
|
375
419
|
}
|
|
376
420
|
async getPackageElement() {
|
|
377
421
|
const packageDocument = await this.getPackageDocument();
|
|
@@ -1043,11 +1087,12 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1043
1087
|
* the provided href within the publication.
|
|
1044
1088
|
*/
|
|
1045
1089
|
async setCoverImage(href, data) {
|
|
1090
|
+
var _a;
|
|
1046
1091
|
const coverImageItem = await this.getCoverImageItem();
|
|
1047
1092
|
if (coverImageItem) {
|
|
1048
1093
|
await this.removeManifestItem(coverImageItem.id);
|
|
1049
1094
|
}
|
|
1050
|
-
const mediaType =
|
|
1095
|
+
const mediaType = (_a = MediaType.fromPath(href)) == null ? void 0 : _a.mime;
|
|
1051
1096
|
if (!mediaType)
|
|
1052
1097
|
throw new Error(`Invalid file extension for cover image: ${href}`);
|
|
1053
1098
|
await this.addManifestItem(
|
|
@@ -2026,35 +2071,81 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
2026
2071
|
return this.getNavigation("page-list", { resolveToRoot });
|
|
2027
2072
|
}
|
|
2028
2073
|
/**
|
|
2029
|
-
*
|
|
2074
|
+
* Name the entry a resolved URL addresses.
|
|
2075
|
+
*
|
|
2076
|
+
* A correctly authored publication percent-encodes its hrefs, so the
|
|
2077
|
+
* decoded reading is the one the spec calls for and the one we prefer.
|
|
2078
|
+
* Some publications instead write the entry name verbatim, so when the two
|
|
2079
|
+
* readings differ we ask the archive which one it holds. A verbatim name
|
|
2080
|
+
* carrying a bare `%`, as `100%.xhtml` does, is not a valid encoding at all
|
|
2081
|
+
* and can only be its own answer.
|
|
2030
2082
|
*/
|
|
2031
|
-
|
|
2032
|
-
const
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2083
|
+
async entryForUrl(url) {
|
|
2084
|
+
const asWritten = url.pathname.slice(1);
|
|
2085
|
+
let decoded;
|
|
2086
|
+
try {
|
|
2087
|
+
decoded = decodeURIComponent(asWritten);
|
|
2088
|
+
} catch {
|
|
2089
|
+
return asWritten;
|
|
2090
|
+
}
|
|
2091
|
+
if (decoded === asWritten) {
|
|
2092
|
+
return decoded;
|
|
2093
|
+
}
|
|
2094
|
+
if (await this.adapter.exists(decoded)) {
|
|
2095
|
+
return decoded;
|
|
2096
|
+
}
|
|
2097
|
+
return await this.adapter.exists(asWritten) ? asWritten : decoded;
|
|
2098
|
+
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Resolve an href to the name of the archive entry it addresses.
|
|
2101
|
+
*
|
|
2102
|
+
* @param from The entry the href appears in; it resolves against that
|
|
2103
|
+
* entry's directory
|
|
2104
|
+
* @throws when the href addresses something outside the publication
|
|
2105
|
+
*/
|
|
2106
|
+
async resolveEntry(from, href) {
|
|
2107
|
+
const url = resolveEntryUrl(from, href);
|
|
2108
|
+
if (!url) {
|
|
2109
|
+
throw new Error(
|
|
2110
|
+
`href does not address an entry in this publication: ${href}`
|
|
2111
|
+
);
|
|
2112
|
+
}
|
|
2113
|
+
return this.entryForUrl(url);
|
|
2038
2114
|
}
|
|
2039
2115
|
/**
|
|
2040
2116
|
* Returns a path-relative-scheme-less URL, relative to the
|
|
2041
2117
|
* container root.
|
|
2042
2118
|
*
|
|
2119
|
+
* An href that points outside the publication, such as an external link in
|
|
2120
|
+
* a navigation document, is handed back unchanged.
|
|
2121
|
+
*
|
|
2043
2122
|
* @param href The href to resolve
|
|
2044
2123
|
* @param [relativeTo] Optional - The href to resolve this href relative to.
|
|
2045
2124
|
Use if resolving a relative href from a file other than the package document.
|
|
2046
2125
|
*/
|
|
2047
2126
|
async resolveHref(href, relativeTo, { toRoot } = {}) {
|
|
2127
|
+
if (href.startsWith("#")) return href;
|
|
2048
2128
|
const rootfile = await this.getRootfile();
|
|
2049
|
-
const from = relativeTo ? this.
|
|
2050
|
-
const
|
|
2051
|
-
|
|
2129
|
+
const from = relativeTo ? await this.resolveEntry(rootfile, relativeTo) : rootfile;
|
|
2130
|
+
const url = resolveEntryUrl(from, href);
|
|
2131
|
+
if (!url) return href;
|
|
2132
|
+
const entry = (await this.entryForUrl(url)).split("/");
|
|
2133
|
+
const base = toRoot ? [] : rootfile.split("/").slice(0, -1);
|
|
2134
|
+
let shared = 0;
|
|
2135
|
+
while (shared < base.length && base[shared] === entry[shared]) {
|
|
2136
|
+
shared++;
|
|
2137
|
+
}
|
|
2138
|
+
const relative = [
|
|
2139
|
+
...Array(base.length - shared).fill(".."),
|
|
2140
|
+
...entry.slice(shared).map(encodeURIComponent)
|
|
2141
|
+
];
|
|
2142
|
+
return relative.join("/") + url.hash;
|
|
2052
2143
|
}
|
|
2053
2144
|
async readFileContents(href, relativeTo, encoding) {
|
|
2054
2145
|
const rootfile = await this.getRootfile();
|
|
2055
|
-
const from = relativeTo ? this.
|
|
2056
|
-
const
|
|
2057
|
-
const itemEntry = encoding ? await this.
|
|
2146
|
+
const from = relativeTo ? await this.resolveEntry(rootfile, relativeTo) : rootfile;
|
|
2147
|
+
const entry = await this.resolveEntry(from, href);
|
|
2148
|
+
const itemEntry = encoding ? await this.adapter.read(entry, encoding) : await this.adapter.read(entry);
|
|
2058
2149
|
return itemEntry;
|
|
2059
2150
|
}
|
|
2060
2151
|
async readItemContents(id, encoding) {
|
|
@@ -2063,8 +2154,8 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
2063
2154
|
const manifestItem = manifest[id];
|
|
2064
2155
|
if (!manifestItem)
|
|
2065
2156
|
throw new Error(`Could not find item with id "${id}" in manifest`);
|
|
2066
|
-
const
|
|
2067
|
-
const itemEntry = encoding ? await this.
|
|
2157
|
+
const entry = await this.resolveEntry(rootfile, manifestItem.href);
|
|
2158
|
+
const itemEntry = encoding ? await this.adapter.read(entry, encoding) : await this.adapter.read(entry);
|
|
2068
2159
|
return itemEntry;
|
|
2069
2160
|
}
|
|
2070
2161
|
/**
|
|
@@ -2122,11 +2213,11 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
2122
2213
|
if (!manifestItem)
|
|
2123
2214
|
throw new Error(`Could not find item with id "${id}" in manifest`);
|
|
2124
2215
|
memoize.clear(this.readXhtmlItemContents);
|
|
2125
|
-
const
|
|
2216
|
+
const entry = await this.resolveEntry(rootfile, manifestItem.href);
|
|
2126
2217
|
if (encoding === "utf-8") {
|
|
2127
|
-
await this.writeEntryContents(
|
|
2218
|
+
await this.writeEntryContents(entry, contents, encoding);
|
|
2128
2219
|
} else {
|
|
2129
|
-
await this.writeEntryContents(
|
|
2220
|
+
await this.writeEntryContents(entry, contents);
|
|
2130
2221
|
}
|
|
2131
2222
|
}
|
|
2132
2223
|
/**
|
|
@@ -2197,7 +2288,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
2197
2288
|
});
|
|
2198
2289
|
this.manifest = null;
|
|
2199
2290
|
const rootfile = await this.getRootfile();
|
|
2200
|
-
const filename = this.
|
|
2291
|
+
const filename = await this.resolveEntry(rootfile, item.href);
|
|
2201
2292
|
const data = encoding === "utf-8" || encoding === "xml" ? new TextEncoder().encode(
|
|
2202
2293
|
encoding === "utf-8" ? contents : await Epub.xmlBuilder.build(
|
|
2203
2294
|
contents
|
|
@@ -2381,10 +2472,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
2381
2472
|
);
|
|
2382
2473
|
const spineTocId = (_a = spine == null ? void 0 : spine[":@"]) == null ? void 0 : _a["@_toc"];
|
|
2383
2474
|
const ncxItem = spineTocId ? manifest[spineTocId] : Object.values(manifest).find(
|
|
2384
|
-
(item) =>
|
|
2385
|
-
var _a2;
|
|
2386
|
-
return ((_a2 = item.mediaType) == null ? void 0 : _a2.toLowerCase()) === "application/x-dtbncx+xml";
|
|
2387
|
-
}
|
|
2475
|
+
(item) => MediaType.fromMime(item.mediaType ?? "") === MediaType.NCX
|
|
2388
2476
|
);
|
|
2389
2477
|
if (!ncxItem) return [];
|
|
2390
2478
|
const ncxContent = await this.readItemContents(ncxItem.id, "utf-8");
|
|
@@ -2525,7 +2613,12 @@ class EpubFactory {
|
|
|
2525
2613
|
"This is not a valid EPUB publication. Could not read the package document."
|
|
2526
2614
|
);
|
|
2527
2615
|
}
|
|
2528
|
-
|
|
2616
|
+
try {
|
|
2617
|
+
await Epub.assertEpub3(epub);
|
|
2618
|
+
} catch (error) {
|
|
2619
|
+
epub.discardAndClose();
|
|
2620
|
+
throw error;
|
|
2621
|
+
}
|
|
2529
2622
|
return epub;
|
|
2530
2623
|
}
|
|
2531
2624
|
/**
|
|
@@ -2565,14 +2658,11 @@ class EpubFactory {
|
|
|
2565
2658
|
const container = encoder.encode(`<?xml version="1.0"?>
|
|
2566
2659
|
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
|
2567
2660
|
<rootfiles>
|
|
2568
|
-
<rootfile media-type="
|
|
2661
|
+
<rootfile media-type="${MediaType.OPF.mime}" full-path="OEBPS/content.opf"/>
|
|
2569
2662
|
</rootfiles>
|
|
2570
2663
|
</container>
|
|
2571
2664
|
`);
|
|
2572
|
-
await adapter.write(
|
|
2573
|
-
join(adapter.rootPath, "META-INF", "container.xml"),
|
|
2574
|
-
container
|
|
2575
|
-
);
|
|
2665
|
+
await adapter.write("META-INF/container.xml", container);
|
|
2576
2666
|
const packageDocument = encoder.encode(`<?xml version="1.0"?>
|
|
2577
2667
|
<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/">
|
|
2578
2668
|
<metadata>
|
|
@@ -2583,10 +2673,7 @@ class EpubFactory {
|
|
|
2583
2673
|
</spine>
|
|
2584
2674
|
</package>
|
|
2585
2675
|
`);
|
|
2586
|
-
await adapter.write(
|
|
2587
|
-
join(adapter.rootPath, "OEBPS", "content.opf"),
|
|
2588
|
-
packageDocument
|
|
2589
|
-
);
|
|
2676
|
+
await adapter.write("OEBPS/content.opf", packageDocument);
|
|
2590
2677
|
const epub = new Epub(this.adapterClass, adapter, path);
|
|
2591
2678
|
const metadata = [
|
|
2592
2679
|
{
|
|
@@ -2635,7 +2722,6 @@ class EpubFactory {
|
|
|
2635
2722
|
* @throws when the adapter is read-only
|
|
2636
2723
|
*/
|
|
2637
2724
|
async upgrade(path, options = {}) {
|
|
2638
|
-
var _a;
|
|
2639
2725
|
if (!this.adapterClass.capabilities.writable) {
|
|
2640
2726
|
throw new EpubReadOnlyError(
|
|
2641
2727
|
`adapter ${this.adapterClass.kind} is read-only; cannot upgrade`
|
|
@@ -2661,49 +2747,55 @@ class EpubFactory {
|
|
|
2661
2747
|
"This is not a valid EPUB publication. Could not read the package document."
|
|
2662
2748
|
);
|
|
2663
2749
|
}
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2750
|
+
try {
|
|
2751
|
+
const version = await epub.getVersion();
|
|
2752
|
+
if (version.startsWith("3.")) {
|
|
2753
|
+
return epub;
|
|
2754
|
+
}
|
|
2755
|
+
const tocEntries = await epub.getNcxTableOfContents();
|
|
2756
|
+
let landmarks = [];
|
|
2757
|
+
await epub.withPackage((pkg) => {
|
|
2758
|
+
landmarks = Upgrade.extractGuideLandmarks(pkg);
|
|
2759
|
+
Upgrade.upgradePackageMetadata(pkg);
|
|
2760
|
+
Upgrade.fixFontMimeTypes(pkg);
|
|
2761
|
+
Upgrade.removeGuide(pkg);
|
|
2762
|
+
if (removeNcx) {
|
|
2763
|
+
Upgrade.removeSpineTocRef(pkg);
|
|
2764
|
+
}
|
|
2765
|
+
Upgrade.setPackageVersion(pkg, "3.0");
|
|
2766
|
+
});
|
|
2767
|
+
await Upgrade.collectManifestProperties(epub);
|
|
2675
2768
|
if (removeNcx) {
|
|
2676
|
-
Upgrade.
|
|
2769
|
+
await Upgrade.removeNcx(epub);
|
|
2677
2770
|
}
|
|
2678
|
-
Upgrade.
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2771
|
+
const navHref = await Upgrade.chooseNavHref(epub);
|
|
2772
|
+
const navContent = await Upgrade.buildNavDocument(
|
|
2773
|
+
epub,
|
|
2774
|
+
tocEntries,
|
|
2775
|
+
landmarks
|
|
2776
|
+
);
|
|
2777
|
+
await epub.addManifestItem(
|
|
2778
|
+
{
|
|
2779
|
+
id: "nav",
|
|
2780
|
+
href: navHref,
|
|
2781
|
+
mediaType: MediaType.XHTML.mime,
|
|
2782
|
+
properties: ["nav"]
|
|
2783
|
+
},
|
|
2784
|
+
navContent,
|
|
2785
|
+
"utf-8"
|
|
2786
|
+
);
|
|
2787
|
+
const manifest = await epub.getManifest();
|
|
2788
|
+
for (const item of Object.values(manifest)) {
|
|
2789
|
+
if (MediaType.fromMime(item.mediaType ?? "") !== MediaType.XHTML)
|
|
2790
|
+
continue;
|
|
2791
|
+
const contents = await epub.readXhtmlItemContents(item.id);
|
|
2792
|
+
await epub.writeXhtmlItemContents(item.id, contents);
|
|
2793
|
+
}
|
|
2794
|
+
return epub;
|
|
2795
|
+
} catch (error) {
|
|
2796
|
+
epub.discardAndClose();
|
|
2797
|
+
throw error;
|
|
2705
2798
|
}
|
|
2706
|
-
return epub;
|
|
2707
2799
|
}
|
|
2708
2800
|
}
|
|
2709
2801
|
export {
|
package/dist/upgrade.cjs
CHANGED
|
@@ -41,6 +41,7 @@ __export(upgrade_exports, {
|
|
|
41
41
|
});
|
|
42
42
|
module.exports = __toCommonJS(upgrade_exports);
|
|
43
43
|
var import_nanoid = require("nanoid");
|
|
44
|
+
var import_media_types = require("@storyteller-platform/media-types");
|
|
44
45
|
var import_index = require("./index.cjs");
|
|
45
46
|
const GUIDE_TO_EPUBTYPE = {
|
|
46
47
|
acknowledgements: "acknowledgments",
|
|
@@ -88,31 +89,7 @@ const GUIDE_TO_EPUBTYPE = {
|
|
|
88
89
|
"other.volume": "volume",
|
|
89
90
|
"other.warning": "warning"
|
|
90
91
|
};
|
|
91
|
-
const XHTML_MEDIA_TYPES = /* @__PURE__ */ new Set([
|
|
92
|
-
"application/xhtml+xml",
|
|
93
|
-
"application/vnd.adobe-page-template+xml",
|
|
94
|
-
"text/html"
|
|
95
|
-
]);
|
|
96
|
-
const OEB_FONTS = /* @__PURE__ */ new Set([
|
|
97
|
-
"application/vnd.ms-opentype",
|
|
98
|
-
"application/x-font-ttf",
|
|
99
|
-
"application/x-font-otf",
|
|
100
|
-
"application/x-font-truetype",
|
|
101
|
-
"application/font-sfnt",
|
|
102
|
-
"application/font-woff",
|
|
103
|
-
"application/font-woff2",
|
|
104
|
-
"font/woff",
|
|
105
|
-
"font/woff2",
|
|
106
|
-
"font/otf",
|
|
107
|
-
"font/ttf",
|
|
108
|
-
"font/sfnt"
|
|
109
|
-
]);
|
|
110
|
-
const FONT_MIME_BY_EXT = {
|
|
111
|
-
".ttf": "application/font-sfnt",
|
|
112
|
-
".otf": "application/font-sfnt",
|
|
113
|
-
".woff": "application/font-woff",
|
|
114
|
-
".woff2": "font/woff2"
|
|
115
|
-
};
|
|
92
|
+
const XHTML_MEDIA_TYPES = /* @__PURE__ */ new Set([import_media_types.MediaType.XHTML, import_media_types.MediaType.HTML]);
|
|
116
93
|
function getMetadataElement(pkg) {
|
|
117
94
|
return import_index.Epub.findXmlChildByName("metadata", import_index.Epub.getXmlChildren(pkg));
|
|
118
95
|
}
|
|
@@ -389,11 +366,10 @@ function removeSpineTocRef(pkg) {
|
|
|
389
366
|
delete spine[":@"]["@_toc"];
|
|
390
367
|
}
|
|
391
368
|
async function collectManifestProperties(epub) {
|
|
392
|
-
var _a;
|
|
393
369
|
const manifest = await epub.getManifest();
|
|
394
370
|
for (const item of Object.values(manifest)) {
|
|
395
|
-
const mediaType = (
|
|
396
|
-
if (!XHTML_MEDIA_TYPES.has(mediaType)) continue;
|
|
371
|
+
const mediaType = import_media_types.MediaType.fromMime(item.mediaType ?? "");
|
|
372
|
+
if (!mediaType || !XHTML_MEDIA_TYPES.has(mediaType)) continue;
|
|
397
373
|
let xml;
|
|
398
374
|
try {
|
|
399
375
|
xml = await epub.readXhtmlItemContents(item.id);
|
|
@@ -414,21 +390,16 @@ async function collectManifestProperties(epub) {
|
|
|
414
390
|
}
|
|
415
391
|
}
|
|
416
392
|
function fixFontMimeTypes(pkg) {
|
|
417
|
-
var _a, _b;
|
|
393
|
+
var _a, _b, _c;
|
|
418
394
|
const manifest = getManifestElement(pkg);
|
|
419
395
|
if (!manifest) return;
|
|
420
396
|
for (const item of findAllByName("item", manifest.manifest)) {
|
|
421
|
-
const
|
|
422
|
-
if (!
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const corrected = FONT_MIME_BY_EXT[ext];
|
|
428
|
-
if (corrected && corrected !== mt) {
|
|
429
|
-
item[":@"] ??= {};
|
|
430
|
-
item[":@"]["@_media-type"] = corrected;
|
|
431
|
-
}
|
|
397
|
+
const declared = ((_a = item[":@"]) == null ? void 0 : _a["@_media-type"]) ?? "";
|
|
398
|
+
if (!((_b = import_media_types.MediaType.fromMime(declared)) == null ? void 0 : _b.is("font"))) continue;
|
|
399
|
+
const corrected = import_media_types.MediaType.fromPath(((_c = item[":@"]) == null ? void 0 : _c["@_href"]) ?? "");
|
|
400
|
+
if (!(corrected == null ? void 0 : corrected.is("font")) || corrected.mime === declared) continue;
|
|
401
|
+
item[":@"] ??= {};
|
|
402
|
+
item[":@"]["@_media-type"] = corrected.mime;
|
|
432
403
|
}
|
|
433
404
|
}
|
|
434
405
|
function buildTocOl(entries) {
|
|
@@ -521,10 +492,7 @@ async function chooseNavHref(epub) {
|
|
|
521
492
|
async function removeNcx(epub) {
|
|
522
493
|
const manifest = await epub.getManifest();
|
|
523
494
|
const ncxItem = Object.values(manifest).find(
|
|
524
|
-
(item) =>
|
|
525
|
-
var _a;
|
|
526
|
-
return ((_a = item.mediaType) == null ? void 0 : _a.toLowerCase()) === "application/x-dtbncx+xml";
|
|
527
|
-
}
|
|
495
|
+
(item) => import_media_types.MediaType.fromMime(item.mediaType ?? "") === import_media_types.MediaType.NCX
|
|
528
496
|
);
|
|
529
497
|
if (ncxItem) {
|
|
530
498
|
await epub.removeManifestItem(ncxItem.id);
|
package/dist/upgrade.d.cts
CHANGED
|
@@ -339,10 +339,6 @@ declare class Epub {
|
|
|
339
339
|
static assertEpub3(epub: Epub): Promise<void>;
|
|
340
340
|
copy(path?: string): Promise<Epub>;
|
|
341
341
|
private removeEntry;
|
|
342
|
-
/**
|
|
343
|
-
* Read raw bytes (or utf-8 text) from the underlying adapter
|
|
344
|
-
*/
|
|
345
|
-
private getFileData;
|
|
346
342
|
/**
|
|
347
343
|
* Length of the underlying archive entry for a manifest item, in bytes
|
|
348
344
|
* Necessary to compute the readium page count which is for COMPRESSED content
|
|
@@ -391,7 +387,7 @@ declare class Epub {
|
|
|
391
387
|
*/
|
|
392
388
|
findMetadataItem(predicate: (entry: MetadataEntry) => boolean): Promise<{
|
|
393
389
|
id: string | undefined;
|
|
394
|
-
type:
|
|
390
|
+
type: `a${string}` | `b${string}` | `c${string}` | `d${string}` | `e${string}` | `f${string}` | `g${string}` | `h${string}` | `i${string}` | `j${string}` | `k${string}` | `l${string}` | `m${string}` | `n${string}` | `o${string}` | `p${string}` | `q${string}` | `r${string}` | `s${string}` | `t${string}` | `u${string}` | `v${string}` | `w${string}` | `x${string}` | `y${string}` | `z${string}` | `A${string}` | `B${string}` | `C${string}` | `D${string}` | `E${string}` | `F${string}` | `G${string}` | `H${string}` | `I${string}` | `J${string}` | `K${string}` | `L${string}` | `M${string}` | `N${string}` | `O${string}` | `P${string}` | `Q${string}` | `R${string}` | `S${string}` | `T${string}` | `U${string}` | `V${string}` | `W${string}` | `X${string}` | `Y${string}` | `Z${string}` | `?${string}`;
|
|
395
391
|
properties: {
|
|
396
392
|
[k: string]: string;
|
|
397
393
|
};
|
|
@@ -403,7 +399,7 @@ declare class Epub {
|
|
|
403
399
|
*/
|
|
404
400
|
findAllMetadataItems(predicate: (entry: MetadataEntry) => boolean): Promise<{
|
|
405
401
|
id: string | undefined;
|
|
406
|
-
type:
|
|
402
|
+
type: `a${string}` | `b${string}` | `c${string}` | `d${string}` | `e${string}` | `f${string}` | `g${string}` | `h${string}` | `i${string}` | `j${string}` | `k${string}` | `l${string}` | `m${string}` | `n${string}` | `o${string}` | `p${string}` | `q${string}` | `r${string}` | `s${string}` | `t${string}` | `u${string}` | `v${string}` | `w${string}` | `x${string}` | `y${string}` | `z${string}` | `A${string}` | `B${string}` | `C${string}` | `D${string}` | `E${string}` | `F${string}` | `G${string}` | `H${string}` | `I${string}` | `J${string}` | `K${string}` | `L${string}` | `M${string}` | `N${string}` | `O${string}` | `P${string}` | `Q${string}` | `R${string}` | `S${string}` | `T${string}` | `U${string}` | `V${string}` | `W${string}` | `X${string}` | `Y${string}` | `Z${string}` | `?${string}`;
|
|
407
403
|
properties: {
|
|
408
404
|
[k: string]: string;
|
|
409
405
|
};
|
|
@@ -922,13 +918,31 @@ declare class Epub {
|
|
|
922
918
|
resolveToRoot?: boolean;
|
|
923
919
|
}): Promise<Navigation | null>;
|
|
924
920
|
/**
|
|
925
|
-
*
|
|
921
|
+
* Name the entry a resolved URL addresses.
|
|
922
|
+
*
|
|
923
|
+
* A correctly authored publication percent-encodes its hrefs, so the
|
|
924
|
+
* decoded reading is the one the spec calls for and the one we prefer.
|
|
925
|
+
* Some publications instead write the entry name verbatim, so when the two
|
|
926
|
+
* readings differ we ask the archive which one it holds. A verbatim name
|
|
927
|
+
* carrying a bare `%`, as `100%.xhtml` does, is not a valid encoding at all
|
|
928
|
+
* and can only be its own answer.
|
|
926
929
|
*/
|
|
927
|
-
private
|
|
930
|
+
private entryForUrl;
|
|
931
|
+
/**
|
|
932
|
+
* Resolve an href to the name of the archive entry it addresses.
|
|
933
|
+
*
|
|
934
|
+
* @param from The entry the href appears in; it resolves against that
|
|
935
|
+
* entry's directory
|
|
936
|
+
* @throws when the href addresses something outside the publication
|
|
937
|
+
*/
|
|
938
|
+
private resolveEntry;
|
|
928
939
|
/**
|
|
929
940
|
* Returns a path-relative-scheme-less URL, relative to the
|
|
930
941
|
* container root.
|
|
931
942
|
*
|
|
943
|
+
* An href that points outside the publication, such as an external link in
|
|
944
|
+
* a navigation document, is handed back unchanged.
|
|
945
|
+
*
|
|
932
946
|
* @param href The href to resolve
|
|
933
947
|
* @param [relativeTo] Optional - The href to resolve this href relative to.
|
|
934
948
|
Use if resolving a relative href from a file other than the package document.
|