vue-book-reader
Version:
<div align="center"> <img width=250 src="https://raw.githubusercontent.com/jinhuan138/vue--book-reader/master/public/logo.png" /> <h1>VueReader</h1> </div>
1,049 lines (1,048 loc) • 40.6 kB
JavaScript
;
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const view = require("./view-BWKNxnVD.cjs");
const NS = {
CONTAINER: "urn:oasis:names:tc:opendocument:xmlns:container",
XHTML: "http://www.w3.org/1999/xhtml",
OPF: "http://www.idpf.org/2007/opf",
EPUB: "http://www.idpf.org/2007/ops",
DC: "http://purl.org/dc/elements/1.1/",
DCTERMS: "http://purl.org/dc/terms/",
ENC: "http://www.w3.org/2001/04/xmlenc#",
NCX: "http://www.daisy.org/z3986/2005/ncx/",
XLINK: "http://www.w3.org/1999/xlink",
SMIL: "http://www.w3.org/ns/SMIL"
};
const MIME = {
XML: "application/xml",
NCX: "application/x-dtbncx+xml",
XHTML: "application/xhtml+xml",
HTML: "text/html",
CSS: "text/css",
SVG: "image/svg+xml",
JS: /\/(x-)?(javascript|ecmascript)/
};
const PREFIX = {
a11y: "http://www.idpf.org/epub/vocab/package/a11y/#",
dcterms: "http://purl.org/dc/terms/",
marc: "http://id.loc.gov/vocabulary/",
media: "http://www.idpf.org/epub/vocab/overlays/#",
onix: "http://www.editeur.org/ONIX/book/codelists/current.html#",
rendition: "http://www.idpf.org/vocab/rendition/#",
schema: "http://schema.org/",
xsd: "http://www.w3.org/2001/XMLSchema#",
msv: "http://www.idpf.org/epub/vocab/structure/magazine/#",
prism: "http://www.prismstandard.org/specifications/3.0/PRISM_CV_Spec_3.0.htm#"
};
const RELATORS = {
art: "artist",
aut: "author",
clr: "colorist",
edt: "editor",
ill: "illustrator",
nrt: "narrator",
trl: "translator",
pbl: "publisher"
};
const ONIX5 = {
"02": "isbn",
"06": "doi",
"15": "isbn",
"26": "doi",
"34": "issn"
};
const camel = (x) => x.toLowerCase().replace(/[-:](.)/g, (_, g) => g.toUpperCase());
const normalizeWhitespace = (str) => str ? str.replace(/[\t\n\f\r ]+/g, " ").replace(/^[\t\n\f\r ]+/, "").replace(/[\t\n\f\r ]+$/, "") : "";
const filterAttribute = (attr, value, isList) => isList ? (el) => {
var _a, _b;
return (_b = (_a = el.getAttribute(attr)) == null ? void 0 : _a.split(/\s/)) == null ? void 0 : _b.includes(value);
} : typeof value === "function" ? (el) => value(el.getAttribute(attr)) : (el) => el.getAttribute(attr) === value;
const getAttributes = (...xs) => (el) => el ? Object.fromEntries(xs.map((x) => [camel(x), el.getAttribute(x)])) : null;
const getElementText = (el) => normalizeWhitespace(el == null ? void 0 : el.textContent);
const childGetter = (doc, ns) => {
const useNS = doc.lookupNamespaceURI(null) === ns || doc.lookupPrefix(ns);
const f = useNS ? (el, name) => (el2) => el2.namespaceURI === ns && el2.localName === name : (el, name) => (el2) => el2.localName === name;
return {
$: (el, name) => [...el.children].find(f(el, name)),
$$: (el, name) => [...el.children].filter(f(el, name)),
$$$: useNS ? (el, name) => [...el.getElementsByTagNameNS(ns, name)] : (el, name) => [...el.getElementsByTagName(name)]
};
};
const resolveURL = (url, relativeTo) => {
try {
if (relativeTo.includes(":"))
return new URL(url, relativeTo);
const root = "https://invalid.invalid/";
const obj = new URL(url, root + relativeTo);
obj.search = "";
return decodeURI(obj.href.replace(root, ""));
} catch (e) {
console.warn(e);
return url;
}
};
const isExternal = (uri) => /^(?!blob)\w+:/i.test(uri);
const pathRelative = (from, to) => {
if (!from)
return to;
const as = from.replace(/\/$/, "").split("/");
const bs = to.replace(/\/$/, "").split("/");
const i = (as.length > bs.length ? as : bs).findIndex((_, i2) => as[i2] !== bs[i2]);
return i < 0 ? "" : Array(as.length - i).fill("..").concat(bs.slice(i)).join("/");
};
const pathDirname = (str) => str.slice(0, str.lastIndexOf("/") + 1);
const replaceSeries = async (str, regex, f) => {
const matches = [];
str.replace(regex, (...args) => (matches.push(args), null));
const results = [];
for (const args of matches)
results.push(await f(...args));
return str.replace(regex, () => results.shift());
};
const regexEscape = (str) => str.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
const tidy = (obj) => {
for (const [key, val] of Object.entries(obj))
if (val == null)
delete obj[key];
else if (Array.isArray(val)) {
obj[key] = val.filter((x) => x).map((x) => typeof x === "object" && !Array.isArray(x) ? tidy(x) : x);
if (!obj[key].length)
delete obj[key];
else if (obj[key].length === 1)
obj[key] = obj[key][0];
} else if (typeof val === "object") {
obj[key] = tidy(val);
if (!Object.keys(val).length)
delete obj[key];
}
const keys = Object.keys(obj);
if (keys.length === 1 && keys[0] === "name")
return obj[keys[0]];
return obj;
};
const getPrefixes = (doc) => {
const map = new Map(Object.entries(PREFIX));
const value = doc.documentElement.getAttributeNS(NS.EPUB, "prefix") || doc.documentElement.getAttribute("prefix");
if (value)
for (const [, prefix, url] of value.matchAll(/(.+): +(.+)[ \t\r\n]*/g))
map.set(prefix, url);
return map;
};
const getPropertyURL = (value, prefixes) => {
if (!value)
return null;
const [a, b] = value.split(":");
const prefix = b ? a : null;
const reference = b ? b : a;
const baseURL = prefixes.get(prefix);
return baseURL ? baseURL + reference : null;
};
const getMetadata = (opf) => {
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w;
const { $ } = childGetter(opf, NS.OPF);
const $metadata = $(opf.documentElement, "metadata");
const els = Object.groupBy($metadata.children, (el) => el.namespaceURI === NS.DC ? "dc" : el.namespaceURI === NS.OPF && el.localName === "meta" ? el.hasAttribute("name") ? "legacyMeta" : "meta" : "");
const baseLang = $metadata.getAttribute("xml:lang") ?? opf.documentElement.getAttribute("xml:lang") ?? "und";
const prefixes = getPrefixes(opf);
const parse = (el) => {
const property = el.getAttribute("property");
const scheme = el.getAttribute("scheme");
return {
property: getPropertyURL(property, prefixes) ?? property,
scheme: getPropertyURL(scheme, prefixes) ?? scheme,
lang: el.getAttribute("xml:lang"),
value: getElementText(el),
props: getProperties(el),
// `opf:` attributes from EPUB 2 & EPUB 3.1 (removed in EPUB 3.2)
attrs: Object.fromEntries(Array.from(el.attributes).filter((attr) => attr.namespaceURI === NS.OPF).map((attr) => [attr.localName, attr.value]))
};
};
const refines = Map.groupBy(els.meta ?? [], (el) => el.getAttribute("refines"));
const getProperties = (el) => {
const els2 = refines.get(el ? "#" + el.getAttribute("id") : null);
if (!els2)
return null;
return Object.groupBy(els2.map(parse), (x) => x.property);
};
const dc = Object.fromEntries(Object.entries(Object.groupBy(els.dc, (el) => el.localName)).map(([name, els2]) => [name, els2.map(parse)]));
const properties = getProperties() ?? {};
const legacyMeta = Object.fromEntries(((_a = els.legacyMeta) == null ? void 0 : _a.map((el) => [el.getAttribute("name"), el.getAttribute("content")])) ?? []);
const one = (x) => {
var _a2;
return (_a2 = x == null ? void 0 : x[0]) == null ? void 0 : _a2.value;
};
const prop = (x, p) => {
var _a2;
return one((_a2 = x == null ? void 0 : x.props) == null ? void 0 : _a2[p]);
};
const makeLanguageMap = (x) => {
var _a2;
if (!x)
return null;
const alts = ((_a2 = x.props) == null ? void 0 : _a2["alternate-script"]) ?? [];
const altRep = x.attrs["alt-rep"];
if (!alts.length && (!x.lang || x.lang === baseLang) && !altRep)
return x.value;
const map = { [x.lang ?? baseLang]: x.value };
if (altRep)
map[x.attrs["alt-rep-lang"]] = altRep;
for (const y of alts)
map[y.lang] ??= y.value;
return map;
};
const makeContributor = (x) => {
var _a2, _b2, _c2, _d2, _e2;
return x ? {
name: makeLanguageMap(x),
sortAs: makeLanguageMap((_b2 = (_a2 = x.props) == null ? void 0 : _a2["file-as"]) == null ? void 0 : _b2[0]) ?? x.attrs["file-as"],
role: ((_e2 = (_d2 = (_c2 = x.props) == null ? void 0 : _c2.role) == null ? void 0 : _d2.filter((x2) => x2.scheme === PREFIX.marc + "relators")) == null ? void 0 : _e2.map((x2) => x2.value)) ?? [x.attrs.role],
code: prop(x, "term") ?? x.attrs.term,
scheme: prop(x, "authority") ?? x.attrs.authority
} : null;
};
const makeCollection = (x) => {
var _a2;
return {
name: makeLanguageMap(x),
// NOTE: webpub requires number but EPUB allows values like "2.2.1"
position: one((_a2 = x.props) == null ? void 0 : _a2["group-position"])
};
};
const makeAltIdentifier = (x) => {
var _a2;
const { value } = x;
if (/^urn:/i.test(value))
return value;
if (/^doi:/i.test(value))
return `urn:${value}`;
const type = (_a2 = x.props) == null ? void 0 : _a2["identifier-type"];
if (!type) {
const scheme = x.attrs.scheme;
if (!scheme)
return value;
if (/^(doi|isbn|uuid)$/i.test(scheme))
return `urn:${scheme}:${value}`;
return { scheme, value };
}
if (type.scheme === PREFIX.onix + "codelist5") {
const nid = ONIX5[type.value];
if (nid)
return `urn:${nid}:${value}`;
}
return value;
};
const belongsTo = Object.groupBy(
properties["belongs-to-collection"] ?? [],
(x) => prop(x, "collection-type") === "series" ? "series" : "collection"
);
const mainTitle = ((_b = dc.title) == null ? void 0 : _b.find((x) => prop(x, "title-type") === "main")) ?? ((_c = dc.title) == null ? void 0 : _c[0]);
const metadata = {
identifier: getIdentifier(opf),
title: makeLanguageMap(mainTitle),
sortAs: makeLanguageMap((_e = (_d = mainTitle == null ? void 0 : mainTitle.props) == null ? void 0 : _d["file-as"]) == null ? void 0 : _e[0]) ?? ((_f = mainTitle == null ? void 0 : mainTitle.attrs) == null ? void 0 : _f["file-as"]) ?? (legacyMeta == null ? void 0 : legacyMeta["calibre:title_sort"]),
subtitle: (_h = (_g = dc.title) == null ? void 0 : _g.find((x) => prop(x, "title-type") === "subtitle")) == null ? void 0 : _h.value,
language: (_i = dc.language) == null ? void 0 : _i.map((x) => x.value),
description: one(dc.description),
publisher: makeContributor((_j = dc.publisher) == null ? void 0 : _j[0]),
published: ((_l = (_k = dc.date) == null ? void 0 : _k.find((x) => x.attrs.event === "publication")) == null ? void 0 : _l.value) ?? one(dc.date),
modified: one(properties[PREFIX.dcterms + "modified"]) ?? ((_n = (_m = dc.date) == null ? void 0 : _m.find((x) => x.attrs.event === "modification")) == null ? void 0 : _n.value),
subject: (_o = dc.subject) == null ? void 0 : _o.map(makeContributor),
belongsTo: {
collection: (_p = belongsTo.collection) == null ? void 0 : _p.map(makeCollection),
series: ((_q = belongsTo.series) == null ? void 0 : _q.map(makeCollection)) ?? (legacyMeta == null ? void 0 : legacyMeta["calibre:series"]) ? {
name: legacyMeta == null ? void 0 : legacyMeta["calibre:series"],
position: parseFloat(legacyMeta == null ? void 0 : legacyMeta["calibre:series_index"])
} : null
},
altIdentifier: (_r = dc.identifier) == null ? void 0 : _r.map(makeAltIdentifier),
source: (_s = dc.source) == null ? void 0 : _s.map(makeAltIdentifier),
// NOTE: not in webpub schema
rights: one(dc.rights)
// NOTE: not in webpub schema
};
const remapContributor = (defaultKey) => (x) => {
var _a2;
const keys = new Set((_a2 = x.role) == null ? void 0 : _a2.map((role) => RELATORS[role] ?? defaultKey));
return [keys.size ? keys : [defaultKey], x];
};
for (const [keys, val] of [].concat(
((_u = (_t = dc.creator) == null ? void 0 : _t.map(makeContributor)) == null ? void 0 : _u.map(remapContributor("author"))) ?? [],
((_w = (_v = dc.contributor) == null ? void 0 : _v.map(makeContributor)) == null ? void 0 : _w.map(remapContributor("contributor"))) ?? []
))
for (const key of keys)
if (metadata[key])
metadata[key].push(val);
else
metadata[key] = [val];
tidy(metadata);
if (metadata.altIdentifier === metadata.identifier)
delete metadata.altIdentifier;
const rendition = {};
const media = {};
for (const [key, val] of Object.entries(properties)) {
if (key.startsWith(PREFIX.rendition))
rendition[camel(key.replace(PREFIX.rendition, ""))] = one(val);
else if (key.startsWith(PREFIX.media))
media[camel(key.replace(PREFIX.media, ""))] = one(val);
}
if (media.duration)
media.duration = parseClock(media.duration);
return { metadata, rendition, media };
};
const parseNav = (doc, resolve = (f) => f) => {
var _a;
const { $, $$, $$$ } = childGetter(doc, NS.XHTML);
const resolveHref = (href) => href ? decodeURI(resolve(href)) : null;
const parseLI = (getType) => ($li) => {
var _a2;
const $a = $($li, "a") ?? $($li, "span");
const $ol = $($li, "ol");
const href = resolveHref($a == null ? void 0 : $a.getAttribute("href"));
const label = getElementText($a) || ($a == null ? void 0 : $a.getAttribute("title"));
const result = { label, href, subitems: parseOL($ol) };
if (getType)
result.type = (_a2 = $a == null ? void 0 : $a.getAttributeNS(NS.EPUB, "type")) == null ? void 0 : _a2.split(/\s/);
return result;
};
const parseOL = ($ol, getType) => $ol ? $$($ol, "li").map(parseLI(getType)) : null;
const parseNav2 = ($nav, getType) => parseOL($($nav, "ol"), getType);
const $$nav = $$$(doc, "nav");
let toc = null, pageList = null, landmarks = null, others = [];
for (const $nav of $$nav) {
const type = ((_a = $nav.getAttributeNS(NS.EPUB, "type")) == null ? void 0 : _a.split(/\s/)) ?? [];
if (type.includes("toc"))
toc ??= parseNav2($nav);
else if (type.includes("page-list"))
pageList ??= parseNav2($nav);
else if (type.includes("landmarks"))
landmarks ??= parseNav2($nav, true);
else
others.push({
label: getElementText($nav.firstElementChild),
type,
list: parseNav2($nav)
});
}
return { toc, pageList, landmarks, others };
};
const parseNCX = (doc, resolve = (f) => f) => {
const { $, $$ } = childGetter(doc, NS.NCX);
const resolveHref = (href) => href ? decodeURI(resolve(href)) : null;
const parseItem = (el) => {
const $label = $(el, "navLabel");
const $content = $(el, "content");
const label = getElementText($label);
const href = resolveHref($content.getAttribute("src"));
if (el.localName === "navPoint") {
const els = $$(el, "navPoint");
return { label, href, subitems: els.length ? els.map(parseItem) : null };
}
return { label, href };
};
const parseList = (el, itemName) => $$(el, itemName).map(parseItem);
const getSingle = (container, itemName) => {
const $container = $(doc.documentElement, container);
return $container ? parseList($container, itemName) : null;
};
return {
toc: getSingle("navMap", "navPoint"),
pageList: getSingle("pageList", "pageTarget"),
others: $$(doc.documentElement, "navList").map((el) => ({
label: getElementText($(el, "navLabel")),
list: parseList(el, "navTarget")
}))
};
};
const parseClock = (str) => {
if (!str)
return;
const parts = str.split(":").map((x2) => parseFloat(x2));
if (parts.length === 3) {
const [h, m, s] = parts;
return h * 60 * 60 + m * 60 + s;
}
if (parts.length === 2) {
const [m, s] = parts;
return m * 60 + s;
}
const [x, unit] = str.split(/(?=[^\d.])/);
const n = parseFloat(x);
const f = unit === "h" ? 60 * 60 : unit === "min" ? 60 : unit === "ms" ? 1e-3 : 1;
return n * f;
};
class MediaOverlay extends EventTarget {
#entries;
#lastMediaOverlayItem;
#sectionIndex;
#audioIndex;
#itemIndex;
#audio;
#volume = 1;
#rate = 1;
#state;
constructor(book, loadXML) {
super();
this.book = book;
this.loadXML = loadXML;
}
async #loadSMIL(item) {
if (this.#lastMediaOverlayItem === item)
return;
const doc = await this.loadXML(item.href);
const resolve = (href) => href ? resolveURL(href, item.href) : null;
const { $, $$$ } = childGetter(doc, NS.SMIL);
this.#audioIndex = -1;
this.#itemIndex = -1;
this.#entries = $$$(doc, "par").reduce((arr, $par) => {
var _a;
const text = resolve((_a = $($par, "text")) == null ? void 0 : _a.getAttribute("src"));
const $audio = $($par, "audio");
if (!text || !$audio)
return arr;
const src = resolve($audio.getAttribute("src"));
const begin = parseClock($audio.getAttribute("clipBegin"));
const end = parseClock($audio.getAttribute("clipEnd"));
const last = arr.at(-1);
if ((last == null ? void 0 : last.src) === src)
last.items.push({ text, begin, end });
else
arr.push({ src, items: [{ text, begin, end }] });
return arr;
}, []);
this.#lastMediaOverlayItem = item;
}
get #activeAudio() {
return this.#entries[this.#audioIndex];
}
get #activeItem() {
var _a, _b;
return (_b = (_a = this.#activeAudio) == null ? void 0 : _a.items) == null ? void 0 : _b[this.#itemIndex];
}
#error(e) {
console.error(e);
this.dispatchEvent(new CustomEvent("error", { detail: e }));
}
#highlight() {
this.dispatchEvent(new CustomEvent("highlight", { detail: this.#activeItem }));
}
#unhighlight() {
this.dispatchEvent(new CustomEvent("unhighlight", { detail: this.#activeItem }));
}
async #play(audioIndex, itemIndex) {
var _a;
this.#stop();
this.#audioIndex = audioIndex;
this.#itemIndex = itemIndex;
const src = (_a = this.#activeAudio) == null ? void 0 : _a.src;
if (!src || !this.#activeItem)
return this.start(this.#sectionIndex + 1);
const url = URL.createObjectURL(await this.book.loadBlob(src));
const audio = new Audio(url);
this.#audio = audio;
audio.volume = this.#volume;
audio.playbackRate = this.#rate;
audio.addEventListener("timeupdate", () => {
var _a2, _b;
if (audio.paused)
return;
const t = audio.currentTime;
const { items } = this.#activeAudio;
if (t > ((_a2 = this.#activeItem) == null ? void 0 : _a2.end)) {
this.#unhighlight();
if (this.#itemIndex === items.length - 1) {
this.#play(this.#audioIndex + 1, 0).catch((e) => this.#error(e));
return;
}
}
const oldIndex = this.#itemIndex;
while (((_b = items[this.#itemIndex + 1]) == null ? void 0 : _b.begin) <= t)
this.#itemIndex++;
if (this.#itemIndex !== oldIndex)
this.#highlight();
});
audio.addEventListener("error", () => this.#error(new Error(`Failed to load ${src}`)));
audio.addEventListener("playing", () => this.#highlight());
audio.addEventListener("ended", () => {
this.#unhighlight();
URL.revokeObjectURL(url);
this.#audio = null;
this.#play(audioIndex + 1, 0).catch((e) => this.#error(e));
});
if (this.#state === "paused") {
this.#highlight();
audio.currentTime = this.#activeItem.begin ?? 0;
} else
audio.addEventListener("canplaythrough", () => {
audio.currentTime = this.#activeItem.begin ?? 0;
this.#state = "playing";
audio.play().catch((e) => this.#error(e));
}, { once: true });
}
async start(sectionIndex, filter = () => true) {
var _a;
(_a = this.#audio) == null ? void 0 : _a.pause();
const section = this.book.sections[sectionIndex];
const href = section == null ? void 0 : section.id;
if (!href)
return;
const { mediaOverlay } = section;
if (!mediaOverlay)
return this.start(sectionIndex + 1);
this.#sectionIndex = sectionIndex;
await this.#loadSMIL(mediaOverlay);
for (let i = 0; i < this.#entries.length; i++) {
const { items } = this.#entries[i];
for (let j = 0; j < items.length; j++) {
if (items[j].text.split("#")[0] === href && filter(items[j], j, items))
return this.#play(i, j).catch((e) => this.#error(e));
}
}
}
pause() {
var _a;
this.#state = "paused";
(_a = this.#audio) == null ? void 0 : _a.pause();
}
resume() {
var _a;
this.#state = "playing";
(_a = this.#audio) == null ? void 0 : _a.play().catch((e) => this.#error(e));
}
#stop() {
if (this.#audio) {
this.#audio.pause();
URL.revokeObjectURL(this.#audio.src);
this.#audio = null;
this.#unhighlight();
}
}
stop() {
this.#state = "stopped";
this.#stop();
}
prev() {
if (this.#itemIndex > 0)
this.#play(this.#audioIndex, this.#itemIndex - 1);
else if (this.#audioIndex > 0)
this.#play(
this.#audioIndex - 1,
this.#entries[this.#audioIndex - 1].items.length - 1
);
else if (this.#sectionIndex > 0)
this.start(this.#sectionIndex - 1, (_, i, items) => i === items.length - 1);
}
next() {
this.#play(this.#audioIndex, this.#itemIndex + 1);
}
setVolume(volume) {
this.#volume = volume;
if (this.#audio)
this.#audio.volume = volume;
}
setRate(rate) {
this.#rate = rate;
if (this.#audio)
this.#audio.playbackRate = rate;
}
}
const isUUID = /([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})/;
const getUUID = (opf) => {
for (const el of opf.getElementsByTagNameNS(NS.DC, "identifier")) {
const [id] = getElementText(el).split(":").slice(-1);
if (isUUID.test(id))
return id;
}
return "";
};
const getIdentifier = (opf) => getElementText(
opf.getElementById(opf.documentElement.getAttribute("unique-identifier")) ?? opf.getElementsByTagNameNS(NS.DC, "identifier")[0]
);
const deobfuscate = async (key, length, blob) => {
const array = new Uint8Array(await blob.slice(0, length).arrayBuffer());
length = Math.min(length, array.length);
for (var i = 0; i < length; i++)
array[i] = array[i] ^ key[i % key.length];
return new Blob([array, blob.slice(length)], { type: blob.type });
};
const WebCryptoSHA1 = async (str) => {
const data = new TextEncoder().encode(str);
const buffer = await globalThis.crypto.subtle.digest("SHA-1", data);
return new Uint8Array(buffer);
};
const deobfuscators = (sha1 = WebCryptoSHA1) => ({
"http://www.idpf.org/2008/embedding": {
key: (opf) => sha1(getIdentifier(opf).replaceAll(/[\u0020\u0009\u000d\u000a]/g, "")),
decode: (key, blob) => deobfuscate(key, 1040, blob)
},
"http://ns.adobe.com/pdf/enc#RC": {
key: (opf) => {
const uuid = getUUID(opf).replaceAll("-", "");
return Uint8Array.from({ length: 16 }, (_, i) => parseInt(uuid.slice(i * 2, i * 2 + 2), 16));
},
decode: (key, blob) => deobfuscate(key, 1024, blob)
}
});
class Encryption {
#uris = /* @__PURE__ */ new Map();
#decoders = /* @__PURE__ */ new Map();
#algorithms;
constructor(algorithms) {
this.#algorithms = algorithms;
}
async init(encryption, opf) {
if (!encryption)
return;
const data = Array.from(
encryption.getElementsByTagNameNS(NS.ENC, "EncryptedData"),
(el) => {
var _a, _b;
return {
algorithm: (_a = el.getElementsByTagNameNS(NS.ENC, "EncryptionMethod")[0]) == null ? void 0 : _a.getAttribute("Algorithm"),
uri: (_b = el.getElementsByTagNameNS(NS.ENC, "CipherReference")[0]) == null ? void 0 : _b.getAttribute("URI")
};
}
);
for (const { algorithm, uri } of data) {
if (!this.#decoders.has(algorithm)) {
const algo = this.#algorithms[algorithm];
if (!algo) {
console.warn("Unknown encryption algorithm");
continue;
}
const key = await algo.key(opf);
this.#decoders.set(algorithm, (blob) => algo.decode(key, blob));
}
this.#uris.set(uri, algorithm);
}
}
getDecoder(uri) {
return this.#decoders.get(this.#uris.get(uri)) ?? ((x) => x);
}
}
class Resources {
constructor({ opf, resolveHref }) {
var _a, _b, _c, _d, _e;
this.opf = opf;
const { $, $$, $$$ } = childGetter(opf, NS.OPF);
const $manifest = $(opf.documentElement, "manifest");
const $spine = $(opf.documentElement, "spine");
const $$itemref = $$($spine, "itemref");
this.manifest = $$($manifest, "item").map(getAttributes("href", "id", "media-type", "properties", "media-overlay")).map((item) => {
var _a2;
item.href = resolveHref(item.href);
item.properties = (_a2 = item.properties) == null ? void 0 : _a2.split(/\s/);
return item;
});
this.manifestById = new Map(this.manifest.map((item) => [item.id, item]));
this.spine = $$itemref.map(getAttributes("idref", "id", "linear", "properties")).map((item) => {
var _a2;
return item.properties = (_a2 = item.properties) == null ? void 0 : _a2.split(/\s/), item;
});
this.pageProgressionDirection = $spine.getAttribute("page-progression-direction");
this.navPath = (_a = this.getItemByProperty("nav")) == null ? void 0 : _a.href;
this.ncxPath = (_b = this.getItemByID($spine.getAttribute("toc")) ?? this.manifest.find((item) => item.mediaType === MIME.NCX)) == null ? void 0 : _b.href;
const $guide = $(opf.documentElement, "guide");
if ($guide)
this.guide = $$($guide, "reference").map(getAttributes("type", "title", "href")).map(({ type, title, href }) => ({
label: title,
type: type.split(/\s/),
href: resolveHref(href)
}));
this.cover = this.getItemByProperty("cover-image") ?? this.getItemByID((_c = $$$(opf, "meta").find(filterAttribute("name", "cover"))) == null ? void 0 : _c.getAttribute("content")) ?? this.getItemByHref((_e = (_d = this.guide) == null ? void 0 : _d.find((ref) => ref.type.includes("cover"))) == null ? void 0 : _e.href);
this.cfis = view.fromElements($$itemref);
}
getItemByID(id) {
return this.manifestById.get(id);
}
getItemByHref(href) {
return this.manifest.find((item) => item.href === href);
}
getItemByProperty(prop) {
return this.manifest.find((item) => {
var _a;
return (_a = item.properties) == null ? void 0 : _a.includes(prop);
});
}
resolveCFI(cfi) {
const parts = view.parse(cfi);
const top = (parts.parent ?? parts).shift();
let $itemref = view.toElement(this.opf, top);
if ($itemref && $itemref.nodeName !== "idref") {
top.at(-1).id = null;
$itemref = view.toElement(this.opf, top);
}
const idref = $itemref == null ? void 0 : $itemref.getAttribute("idref");
const index = this.spine.findIndex((item) => item.idref === idref);
const anchor = (doc) => view.toRange(doc, parts);
return { index, anchor };
}
}
class Loader {
#cache = /* @__PURE__ */ new Map();
#children = /* @__PURE__ */ new Map();
#refCount = /* @__PURE__ */ new Map();
eventTarget = new EventTarget();
constructor({ loadText, loadBlob, resources }) {
this.loadText = loadText;
this.loadBlob = loadBlob;
this.manifest = resources.manifest;
this.assets = resources.manifest;
}
async createURL(href, data, type, parent) {
if (!data)
return "";
const detail = { data, type };
Object.defineProperty(detail, "name", { value: href });
const event = new CustomEvent("data", { detail });
this.eventTarget.dispatchEvent(event);
const newData = await event.detail.data;
const newType = await event.detail.type;
const url = URL.createObjectURL(new Blob([newData], { type: newType }));
this.#cache.set(href, url);
this.#refCount.set(href, 1);
if (parent) {
const childList = this.#children.get(parent);
if (childList)
childList.push(href);
else
this.#children.set(parent, [href]);
}
return url;
}
ref(href, parent) {
const childList = this.#children.get(parent);
if (!(childList == null ? void 0 : childList.includes(href))) {
this.#refCount.set(href, this.#refCount.get(href) + 1);
if (childList)
childList.push(href);
else
this.#children.set(parent, [href]);
}
return this.#cache.get(href);
}
unref(href) {
if (!this.#refCount.has(href))
return;
const count = this.#refCount.get(href) - 1;
if (count < 1) {
URL.revokeObjectURL(this.#cache.get(href));
this.#cache.delete(href);
this.#refCount.delete(href);
const childList = this.#children.get(href);
if (childList)
while (childList.length)
this.unref(childList.pop());
this.#children.delete(href);
} else
this.#refCount.set(href, count);
}
// load manifest item, recursively loading all resources as needed
async loadItem(item, parents = []) {
if (!item)
return null;
const { href, mediaType } = item;
const isScript = MIME.JS.test(item.mediaType);
const detail = { type: mediaType, isScript, allow: true };
const event = new CustomEvent("load", { detail });
this.eventTarget.dispatchEvent(event);
const allow = await event.detail.allow;
if (!allow)
return null;
const parent = parents.at(-1);
if (this.#cache.has(href))
return this.ref(href, parent);
const shouldReplace = (isScript || [MIME.XHTML, MIME.HTML, MIME.CSS, MIME.SVG].includes(mediaType)) && parents.every((p) => p !== href);
if (shouldReplace)
return this.loadReplaced(item, parents);
const tryLoadBlob = Promise.resolve().then(() => this.loadBlob(href));
return this.createURL(href, tryLoadBlob, mediaType, parent);
}
async loadHref(href, base, parents = []) {
if (isExternal(href))
return href;
const path = resolveURL(href, base);
const item = this.manifest.find((item2) => item2.href === path);
if (!item)
return href;
return this.loadItem(item, parents.concat(base));
}
async loadReplaced(item, parents = []) {
var _a, _b;
const { href, mediaType } = item;
const parent = parents.at(-1);
let str = "";
try {
str = await this.loadText(href);
} catch (e) {
return this.createURL(href, Promise.reject(e), mediaType, parent);
}
if (!str)
return null;
if ([MIME.XHTML, MIME.HTML, MIME.SVG].includes(mediaType)) {
let doc = new DOMParser().parseFromString(str, mediaType);
if (mediaType === MIME.XHTML && (doc.querySelector("parsererror") || !((_a = doc.documentElement) == null ? void 0 : _a.namespaceURI))) {
console.warn(((_b = doc.querySelector("parsererror")) == null ? void 0 : _b.innerText) ?? "Invalid XHTML");
item.mediaType = MIME.HTML;
doc = new DOMParser().parseFromString(str, item.mediaType);
}
if ([MIME.XHTML, MIME.SVG].includes(item.mediaType)) {
let child = doc.firstChild;
while (child instanceof ProcessingInstruction) {
if (child.data) {
const replacedData = await replaceSeries(
child.data,
/(?:^|\s*)(href\s*=\s*['"])([^'"]*)(['"])/i,
(_, p1, p2, p3) => this.loadHref(p2, href, parents).then((p22) => `${p1}${p22}${p3}`)
);
child.replaceWith(doc.createProcessingInstruction(
child.target,
replacedData
));
}
child = child.nextSibling;
}
}
const replace = async (el, attr) => el.setAttribute(
attr,
await this.loadHref(el.getAttribute(attr), href, parents)
);
for (const el of doc.querySelectorAll("link[href]"))
await replace(el, "href");
for (const el of doc.querySelectorAll("[src]"))
await replace(el, "src");
for (const el of doc.querySelectorAll("[poster]"))
await replace(el, "poster");
for (const el of doc.querySelectorAll("object[data]"))
await replace(el, "data");
for (const el of doc.querySelectorAll("[*|href]:not([href])"))
el.setAttributeNS(NS.XLINK, "href", await this.loadHref(
el.getAttributeNS(NS.XLINK, "href"),
href,
parents
));
for (const el of doc.querySelectorAll("style"))
if (el.textContent)
el.textContent = await this.replaceCSS(el.textContent, href, parents);
for (const el of doc.querySelectorAll("[style]"))
el.setAttribute(
"style",
await this.replaceCSS(el.getAttribute("style"), href, parents)
);
const result2 = new XMLSerializer().serializeToString(doc);
return this.createURL(href, result2, item.mediaType, parent);
}
const result = mediaType === MIME.CSS ? await this.replaceCSS(str, href, parents) : await this.replaceString(str, href, parents);
return this.createURL(href, result, mediaType, parent);
}
async replaceCSS(str, href, parents = []) {
const replacedUrls = await replaceSeries(
str,
/url\(\s*["']?([^'"\n]*?)\s*["']?\s*\)/gi,
(_, url) => this.loadHref(url, href, parents).then((url2) => `url("${url2}")`)
);
return replaceSeries(
replacedUrls,
/@import\s*["']([^"'\n]*?)["']/gi,
(_, url) => this.loadHref(url, href, parents).then((url2) => `@import "${url2}"`)
);
}
// find & replace all possible relative paths for all assets without parsing
replaceString(str, href, parents = []) {
const assetMap = /* @__PURE__ */ new Map();
const urls = this.assets.map((asset) => {
if (asset.href === href)
return;
const relative = pathRelative(pathDirname(href), asset.href);
const relativeEnc = encodeURI(relative);
const rootRelative = "/" + asset.href;
const rootRelativeEnc = encodeURI(rootRelative);
const set = /* @__PURE__ */ new Set([relative, relativeEnc, rootRelative, rootRelativeEnc]);
for (const url of set)
assetMap.set(url, asset);
return Array.from(set);
}).flat().filter((x) => x);
if (!urls.length)
return str;
const regex = new RegExp(urls.map(regexEscape).join("|"), "g");
return replaceSeries(str, regex, async (match) => this.loadItem(
assetMap.get(match.replace(/^\//, "")),
parents.concat(href)
));
}
unloadItem(item) {
this.unref(item == null ? void 0 : item.href);
}
destroy() {
for (const url of this.#cache.values())
URL.revokeObjectURL(url);
}
}
const getHTMLFragment = (doc, id) => doc.getElementById(id) ?? doc.querySelector(`[name="${CSS.escape(id)}"]`);
const getPageSpread = (properties) => {
for (const p of properties) {
if (p === "page-spread-left" || p === "rendition:page-spread-left")
return "left";
if (p === "page-spread-right" || p === "rendition:page-spread-right")
return "right";
if (p === "rendition:page-spread-center")
return "center";
}
};
const getDisplayOptions = (doc) => {
if (!doc)
return null;
return {
fixedLayout: getElementText(doc.querySelector('option[name="fixed-layout"]')),
openToSpread: getElementText(doc.querySelector('option[name="open-to-spread"]'))
};
};
class EPUB {
parser = new DOMParser();
#loader;
#encryption;
constructor({ loadText, loadBlob, getSize, sha1 }) {
this.loadText = loadText;
this.loadBlob = loadBlob;
this.getSize = getSize;
this.#encryption = new Encryption(deobfuscators(sha1));
}
async #loadXML(uri) {
const str = await this.loadText(uri);
if (!str)
return null;
const doc = this.parser.parseFromString(str, MIME.XML);
if (doc.querySelector("parsererror"))
throw new Error(`XML parsing error: ${uri}
${doc.querySelector("parsererror").innerText}`);
return doc;
}
async init() {
const $container = await this.#loadXML("META-INF/container.xml");
if (!$container)
throw new Error("Failed to load container file");
const opfs = Array.from(
$container.getElementsByTagNameNS(NS.CONTAINER, "rootfile"),
getAttributes("full-path", "media-type")
).filter((file) => file.mediaType === "application/oebps-package+xml");
if (!opfs.length)
throw new Error("No package document defined in container");
const opfPath = opfs[0].fullPath;
const opf = await this.#loadXML(opfPath);
if (!opf)
throw new Error("Failed to load package document");
const $encryption = await this.#loadXML("META-INF/encryption.xml");
await this.#encryption.init($encryption, opf);
this.resources = new Resources({
opf,
resolveHref: (url) => resolveURL(url, opfPath)
});
this.#loader = new Loader({
loadText: this.loadText,
loadBlob: (uri) => Promise.resolve(this.loadBlob(uri)).then(this.#encryption.getDecoder(uri)),
resources: this.resources
});
this.transformTarget = this.#loader.eventTarget;
this.sections = this.resources.spine.map((spineItem, index) => {
const { idref, linear, properties = [] } = spineItem;
const item = this.resources.getItemByID(idref);
if (!item) {
console.warn(`Could not find item with ID "${idref}" in manifest`);
return null;
}
return {
id: item.href,
load: () => this.#loader.loadItem(item),
unload: () => this.#loader.unloadItem(item),
createDocument: () => this.loadDocument(item),
size: this.getSize(item.href),
cfi: this.resources.cfis[index],
linear,
pageSpread: getPageSpread(properties),
resolveHref: (href) => resolveURL(href, item.href),
mediaOverlay: item.mediaOverlay ? this.resources.getItemByID(item.mediaOverlay) : null
};
}).filter((s) => s);
const { navPath, ncxPath } = this.resources;
if (navPath)
try {
const resolve = (url) => resolveURL(url, navPath);
const nav = parseNav(await this.#loadXML(navPath), resolve);
this.toc = nav.toc;
this.pageList = nav.pageList;
this.landmarks = nav.landmarks;
} catch (e) {
console.warn(e);
}
if (!this.toc && ncxPath)
try {
const resolve = (url) => resolveURL(url, ncxPath);
const ncx = parseNCX(await this.#loadXML(ncxPath), resolve);
this.toc = ncx.toc;
this.pageList = ncx.pageList;
} catch (e) {
console.warn(e);
}
this.landmarks ??= this.resources.guide;
const { metadata, rendition, media } = getMetadata(opf);
this.metadata = metadata;
this.rendition = rendition;
this.media = media;
this.dir = this.resources.pageProgressionDirection;
const displayOptions = getDisplayOptions(
await this.#loadXML("META-INF/com.apple.ibooks.display-options.xml") ?? await this.#loadXML("META-INF/com.kobobooks.display-options.xml")
);
if (displayOptions) {
if (displayOptions.fixedLayout === "true")
this.rendition.layout ??= "pre-paginated";
if (displayOptions.openToSpread === "false")
this.sections.find((section) => section.linear !== "no").pageSpread ??= this.dir === "rtl" ? "left" : "right";
}
return this;
}
async loadDocument(item) {
const str = await this.loadText(item.href);
return this.parser.parseFromString(str, item.mediaType);
}
getMediaOverlay() {
return new MediaOverlay(this, this.#loadXML.bind(this));
}
resolveCFI(cfi) {
return this.resources.resolveCFI(cfi);
}
resolveHref(href) {
const [path, hash] = href.split("#");
const item = this.resources.getItemByHref(decodeURI(path));
if (!item)
return null;
const index = this.resources.spine.findIndex(({ idref }) => idref === item.id);
const anchor = hash ? (doc) => getHTMLFragment(doc, hash) : () => 0;
return { index, anchor };
}
splitTOCHref(href) {
return (href == null ? void 0 : href.split("#")) ?? [];
}
getTOCFragment(doc, id) {
return doc.getElementById(id) ?? doc.querySelector(`[name="${CSS.escape(id)}"]`);
}
isExternal(uri) {
return isExternal(uri);
}
async getCover() {
var _a;
const cover = (_a = this.resources) == null ? void 0 : _a.cover;
return (cover == null ? void 0 : cover.href) ? new Blob([await this.loadBlob(cover.href)], { type: cover.mediaType }) : null;
}
async getCalibreBookmarks() {
const txt = await this.loadText("META-INF/calibre_bookmarks.txt");
const magic = "encoding=json+base64:";
if (txt == null ? void 0 : txt.startsWith(magic)) {
const json = atob(txt.slice(magic.length));
return JSON.parse(json);
}
}
destroy() {
var _a;
(_a = this.#loader) == null ? void 0 : _a.destroy();
}
}
exports.EPUB = EPUB;