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,246 lines (1,245 loc) • 44.1 kB
JavaScript
const findIndices = (arr, f) => arr.map((x, i, a) => f(x, i, a) ? i : null).filter((x) => x != null);
const splitAt = (arr, is) => [-1, ...is, arr.length].reduce(({ xs, a }, b) => ({ xs: (xs == null ? void 0 : xs.concat([arr.slice(a + 1, b)])) ?? [], a: b }), {}).xs;
const concatArrays = (a, b) => a.slice(0, -1).concat([a[a.length - 1].concat(b[0])]).concat(b.slice(1));
const isNumber = /\d/;
const isCFI = /^epubcfi\((.*)\)$/;
const escapeCFI = (str) => str.replace(/[\^[\](),;=]/g, "^$&");
const wrap = (x) => isCFI.test(x) ? x : `epubcfi(${x})`;
const unwrap = (x) => {
var _a;
return ((_a = x.match(isCFI)) == null ? void 0 : _a[1]) ?? x;
};
const lift = (f) => (...xs) => `epubcfi(${f(...xs.map((x) => {
var _a;
return ((_a = x.match(isCFI)) == null ? void 0 : _a[1]) ?? x;
}))})`;
const joinIndir = lift((...xs) => xs.join("!"));
const tokenizer = (str) => {
const tokens = [];
let state, escape, value = "";
const push = (x) => (tokens.push(x), state = null, value = "");
const cat = (x) => (value += x, escape = false);
for (const char of Array.from(str.trim()).concat("")) {
if (char === "^" && !escape) {
escape = true;
continue;
}
if (state === "!")
push(["!"]);
else if (state === ",")
push([","]);
else if (state === "/" || state === ":") {
if (isNumber.test(char)) {
cat(char);
continue;
} else
push([state, parseInt(value)]);
} else if (state === "~") {
if (isNumber.test(char) || char === ".") {
cat(char);
continue;
} else
push(["~", parseFloat(value)]);
} else if (state === "@") {
if (char === ":") {
push(["@", parseFloat(value)]);
state = "@";
continue;
}
if (isNumber.test(char) || char === ".") {
cat(char);
continue;
} else
push(["@", parseFloat(value)]);
} else if (state === "[") {
if (char === ";" && !escape) {
push(["[", value]);
state = ";";
} else if (char === "," && !escape) {
push(["[", value]);
state = "[";
} else if (char === "]" && !escape)
push(["[", value]);
else
cat(char);
continue;
} else if (state == null ? void 0 : state.startsWith(";")) {
if (char === "=" && !escape) {
state = `;${value}`;
value = "";
} else if (char === ";" && !escape) {
push([state, value]);
state = ";";
} else if (char === "]" && !escape)
push([state, value]);
else
cat(char);
continue;
}
if (char === "/" || char === ":" || char === "~" || char === "@" || char === "[" || char === "!" || char === ",")
state = char;
}
return tokens;
};
const findTokens = (tokens, x) => findIndices(tokens, ([t]) => t === x);
const parser = (tokens) => {
const parts = [];
let state;
for (const [type, val] of tokens) {
if (type === "/")
parts.push({ index: val });
else {
const last = parts[parts.length - 1];
if (type === ":")
last.offset = val;
else if (type === "~")
last.temporal = val;
else if (type === "@")
last.spatial = (last.spatial ?? []).concat(val);
else if (type === ";s")
last.side = val;
else if (type === "[") {
if (state === "/" && val)
last.id = val;
else {
last.text = (last.text ?? []).concat(val);
continue;
}
}
}
state = type;
}
return parts;
};
const parserIndir = (tokens) => splitAt(tokens, findTokens(tokens, "!")).map(parser);
const parse = (cfi) => {
const tokens = tokenizer(unwrap(cfi));
const commas = findTokens(tokens, ",");
if (!commas.length)
return parserIndir(tokens);
const [parent, start, end] = splitAt(tokens, commas).map(parserIndir);
return { parent, start, end };
};
const partToString = ({ index, id, offset, temporal, spatial, text, side }) => {
var _a;
const param = side ? `;s=${side}` : "";
return `/${index}` + (id ? `[${escapeCFI(id)}${param}]` : "") + (offset != null && index % 2 ? `:${offset}` : "") + (temporal ? `~${temporal}` : "") + (spatial ? `@${spatial.join(":")}` : "") + (text || !id && side ? "[" + (((_a = text == null ? void 0 : text.map(escapeCFI)) == null ? void 0 : _a.join(",")) ?? "") + param + "]" : "");
};
const toInnerString = (parsed) => parsed.parent ? [parsed.parent, parsed.start, parsed.end].map(toInnerString).join(",") : parsed.map((parts) => parts.map(partToString).join("")).join("!");
const toString = (parsed) => wrap(toInnerString(parsed));
const collapse = (x, toEnd) => typeof x === "string" ? toString(collapse(parse(x), toEnd)) : x.parent ? concatArrays(x.parent, x[toEnd ? "end" : "start"]) : x;
const buildRange = (from, to) => {
if (typeof from === "string")
from = parse(from);
if (typeof to === "string")
to = parse(to);
from = collapse(from);
to = collapse(to, true);
const localFrom = from[from.length - 1], localTo = to[to.length - 1];
const localParent = [], localStart = [], localEnd = [];
let pushToParent = true;
const len = Math.max(localFrom.length, localTo.length);
for (let i = 0; i < len; i++) {
const a = localFrom[i], b = localTo[i];
pushToParent &&= (a == null ? void 0 : a.index) === (b == null ? void 0 : b.index) && !(a == null ? void 0 : a.offset) && !(b == null ? void 0 : b.offset);
if (pushToParent)
localParent.push(a);
else {
if (a)
localStart.push(a);
if (b)
localEnd.push(b);
}
}
const parent = from.slice(0, -1).concat([localParent]);
return toString({ parent, start: [localStart], end: [localEnd] });
};
const isTextNode = ({ nodeType }) => nodeType === 3 || nodeType === 4;
const isElementNode = ({ nodeType }) => nodeType === 1;
const getChildNodes = (node, filter2) => {
const nodes = Array.from(node.childNodes).filter((node2) => isTextNode(node2) || isElementNode(node2));
return filter2 ? nodes.map((node2) => {
const accept = filter2(node2);
if (accept === NodeFilter.FILTER_REJECT)
return null;
else if (accept === NodeFilter.FILTER_SKIP)
return getChildNodes(node2, filter2);
else
return node2;
}).flat().filter((x) => x) : nodes;
};
const indexChildNodes = (node, filter2) => {
const nodes = getChildNodes(node, filter2).reduce((arr, node2) => {
let last = arr[arr.length - 1];
if (!last)
arr.push(node2);
else if (isTextNode(node2)) {
if (Array.isArray(last))
last.push(node2);
else if (isTextNode(last))
arr[arr.length - 1] = [last, node2];
else
arr.push(node2);
} else {
if (isElementNode(last))
arr.push(null, node2);
else
arr.push(node2);
}
return arr;
}, []);
if (isElementNode(nodes[0]))
nodes.unshift("first");
if (isElementNode(nodes[nodes.length - 1]))
nodes.push("last");
nodes.unshift("before");
nodes.push("after");
return nodes;
};
const partsToNode = (node, parts, filter2) => {
const { id } = parts[parts.length - 1];
if (id) {
const el = node.ownerDocument.getElementById(id);
if (el)
return { node: el, offset: 0 };
}
for (const { index } of parts) {
const newNode = node ? indexChildNodes(node, filter2)[index] : null;
if (newNode === "first")
return { node: node.firstChild ?? node };
if (newNode === "last")
return { node: node.lastChild ?? node };
if (newNode === "before")
return { node, before: true };
if (newNode === "after")
return { node, after: true };
node = newNode;
}
const { offset } = parts[parts.length - 1];
if (!Array.isArray(node))
return { node, offset };
let sum = 0;
for (const n of node) {
const { length } = n.nodeValue;
if (sum + length >= offset)
return { node: n, offset: offset - sum };
sum += length;
}
};
const nodeToParts = (node, offset, filter2) => {
const { parentNode, id } = node;
const indexed = indexChildNodes(parentNode, filter2);
const index = indexed.findIndex((x) => Array.isArray(x) ? x.some((x2) => x2 === node) : x === node);
const chunk = indexed[index];
if (Array.isArray(chunk)) {
let sum = 0;
for (const x of chunk) {
if (x === node) {
sum += offset;
break;
} else
sum += x.nodeValue.length;
}
offset = sum;
}
const part = { id, index, offset };
return (parentNode !== node.ownerDocument.documentElement ? nodeToParts(parentNode, null, filter2).concat(part) : [part]).filter((x) => x.index !== -1);
};
const fromRange = (range, filter2) => {
const { startContainer, startOffset, endContainer, endOffset } = range;
const start = nodeToParts(startContainer, startOffset, filter2);
if (range.collapsed)
return toString([start]);
const end = nodeToParts(endContainer, endOffset, filter2);
return buildRange([start], [end]);
};
const toRange = (doc, parts, filter2) => {
const startParts = collapse(parts);
const endParts = collapse(parts, true);
const root = doc.documentElement;
const start = partsToNode(root, startParts[0], filter2);
const end = partsToNode(root, endParts[0], filter2);
const range = doc.createRange();
if (start.before)
range.setStartBefore(start.node);
else if (start.after)
range.setStartAfter(start.node);
else
range.setStart(start.node, start.offset);
if (end.before)
range.setEndBefore(end.node);
else if (end.after)
range.setEndAfter(end.node);
else
range.setEnd(end.node, end.offset);
return range;
};
const fromElements = (elements) => {
const results = [];
const { parentNode } = elements[0];
const parts = nodeToParts(parentNode);
for (const [index, node] of indexChildNodes(parentNode).entries()) {
const el = elements[results.length];
if (node === el)
results.push(toString([parts.concat({ id: el.id, index })]));
}
return results;
};
const toElement = (doc, parts) => partsToNode(doc.documentElement, collapse(parts)).node;
const fake = {
fromIndex: (index) => wrap(`/6/${(index + 1) * 2}`),
toIndex: (parts) => (parts == null ? void 0 : parts.at(-1).index) / 2 - 1
};
const assignIDs = (toc) => {
let id = 0;
const assignID = (item) => {
item.id = id++;
if (item.subitems)
for (const subitem of item.subitems)
assignID(subitem);
};
for (const item of toc)
assignID(item);
return toc;
};
const flatten = (items) => items.map((item) => {
var _a;
return ((_a = item.subitems) == null ? void 0 : _a.length) ? [item, flatten(item.subitems)].flat() : item;
}).flat();
class TOCProgress {
async init({ toc, ids, splitHref, getFragment }) {
assignIDs(toc);
const items = flatten(toc);
const grouped = /* @__PURE__ */ new Map();
for (const [i, item] of items.entries()) {
const [id, fragment] = await splitHref(item == null ? void 0 : item.href) ?? [];
const value = { fragment, item };
if (grouped.has(id))
grouped.get(id).items.push(value);
else
grouped.set(id, { prev: items[i - 1], items: [value] });
}
const map = /* @__PURE__ */ new Map();
for (const [i, id] of ids.entries()) {
if (grouped.has(id))
map.set(id, grouped.get(id));
else
map.set(id, map.get(ids[i - 1]));
}
this.ids = ids;
this.map = map;
this.getFragment = getFragment;
}
getProgress(index, range) {
var _a;
if (!this.ids)
return;
const id = this.ids[index];
const obj = this.map.get(id);
if (!obj)
return null;
const { prev, items } = obj;
if (!items)
return prev;
if (!range || items.length === 1 && !items[0].fragment)
return items[0].item;
const doc = range.startContainer.getRootNode();
for (const [i, { fragment }] of items.entries()) {
const el = this.getFragment(doc, fragment);
if (!el)
continue;
if (range.comparePoint(el, 0) > 0)
return ((_a = items[i - 1]) == null ? void 0 : _a.item) ?? prev;
}
return items[items.length - 1].item;
}
}
class SectionProgress {
constructor(sections, sizePerLoc, sizePerTimeUnit) {
this.sizes = sections.map((s) => s.linear != "no" && s.size > 0 ? s.size : 0);
this.sizePerLoc = sizePerLoc;
this.sizePerTimeUnit = sizePerTimeUnit;
this.sizeTotal = this.sizes.reduce((a, b) => a + b, 0);
this.sectionFractions = this.#getSectionFractions();
}
#getSectionFractions() {
const { sizeTotal } = this;
const results = [0];
let sum = 0;
for (const size of this.sizes)
results.push((sum += size) / sizeTotal);
return results;
}
// get progress given index of and fractions within a section
getProgress(index, fractionInSection, pageFraction = 0) {
const { sizes, sizePerLoc, sizePerTimeUnit, sizeTotal } = this;
const sizeInSection = sizes[index] ?? 0;
const sizeBefore = sizes.slice(0, index).reduce((a, b) => a + b, 0);
const size = sizeBefore + fractionInSection * sizeInSection;
const nextSize = size + pageFraction * sizeInSection;
const remainingTotal = sizeTotal - size;
const remainingSection = (1 - fractionInSection) * sizeInSection;
return {
fraction: nextSize / sizeTotal,
section: {
current: index,
total: sizes.length
},
location: {
current: Math.floor(size / sizePerLoc),
next: Math.floor(nextSize / sizePerLoc),
total: Math.ceil(sizeTotal / sizePerLoc)
},
time: {
section: remainingSection / sizePerTimeUnit,
total: remainingTotal / sizePerTimeUnit
}
};
}
// the inverse of `getProgress`
// get index of and fraction in section based on total fraction
getSection(fraction) {
if (fraction <= 0)
return [0, 0];
if (fraction >= 1)
return [this.sizes.length - 1, 1];
fraction = fraction + Number.EPSILON;
const { sizeTotal } = this;
let index = this.sectionFractions.findIndex((x) => x > fraction) - 1;
if (index < 0)
return [0, 0];
while (!this.sizes[index])
index++;
const fractionInSection = (fraction - this.sectionFractions[index]) / (this.sizes[index] / sizeTotal);
return [index, fractionInSection];
}
}
const createSVGElement = (tag) => document.createElementNS("http://www.w3.org/2000/svg", tag);
class Overlayer {
#svg = createSVGElement("svg");
#map = /* @__PURE__ */ new Map();
constructor() {
Object.assign(this.#svg.style, {
position: "absolute",
top: "0",
left: "0",
width: "100%",
height: "100%",
pointerEvents: "none"
});
}
get element() {
return this.#svg;
}
add(key, range, draw, options) {
if (this.#map.has(key))
this.remove(key);
if (typeof range === "function")
range = range(this.#svg.getRootNode());
const rects = range.getClientRects();
const element = draw(rects, options);
this.#svg.append(element);
this.#map.set(key, { range, draw, options, element, rects });
}
remove(key) {
if (!this.#map.has(key))
return;
this.#svg.removeChild(this.#map.get(key).element);
this.#map.delete(key);
}
redraw() {
for (const obj of this.#map.values()) {
const { range, draw, options, element } = obj;
this.#svg.removeChild(element);
const rects = range.getClientRects();
const el = draw(rects, options);
this.#svg.append(el);
obj.element = el;
obj.rects = rects;
}
}
hitTest({ x, y }) {
const arr = Array.from(this.#map.entries());
for (let i = arr.length - 1; i >= 0; i--) {
const [key, obj] = arr[i];
for (const { left, top, right, bottom } of obj.rects)
if (top <= y && left <= x && bottom > y && right > x)
return [key, obj.range];
}
return [];
}
static underline(rects, options = {}) {
const { color = "red", width: strokeWidth = 2, writingMode } = options;
const g = createSVGElement("g");
g.setAttribute("fill", color);
if (writingMode === "vertical-rl" || writingMode === "vertical-lr")
for (const { right, top, height } of rects) {
const el = createSVGElement("rect");
el.setAttribute("x", right - strokeWidth);
el.setAttribute("y", top);
el.setAttribute("height", height);
el.setAttribute("width", strokeWidth);
g.append(el);
}
else
for (const { left, bottom, width } of rects) {
const el = createSVGElement("rect");
el.setAttribute("x", left);
el.setAttribute("y", bottom - strokeWidth);
el.setAttribute("height", strokeWidth);
el.setAttribute("width", width);
g.append(el);
}
return g;
}
static strikethrough(rects, options = {}) {
const { color = "red", width: strokeWidth = 2, writingMode } = options;
const g = createSVGElement("g");
g.setAttribute("fill", color);
if (writingMode === "vertical-rl" || writingMode === "vertical-lr")
for (const { right, left, top, height } of rects) {
const el = createSVGElement("rect");
el.setAttribute("x", (right + left) / 2);
el.setAttribute("y", top);
el.setAttribute("height", height);
el.setAttribute("width", strokeWidth);
g.append(el);
}
else
for (const { left, top, bottom, width } of rects) {
const el = createSVGElement("rect");
el.setAttribute("x", left);
el.setAttribute("y", (top + bottom) / 2);
el.setAttribute("height", strokeWidth);
el.setAttribute("width", width);
g.append(el);
}
return g;
}
static squiggly(rects, options = {}) {
const { color = "red", width: strokeWidth = 2, writingMode } = options;
const g = createSVGElement("g");
g.setAttribute("fill", "none");
g.setAttribute("stroke", color);
g.setAttribute("stroke-width", strokeWidth);
const block = strokeWidth * 1.5;
if (writingMode === "vertical-rl" || writingMode === "vertical-lr")
for (const { right, top, height } of rects) {
const el = createSVGElement("path");
const n = Math.round(height / block / 1.5);
const inline = height / n;
const ls = Array.from(
{ length: n },
(_, i) => `l${i % 2 ? -block : block} ${inline}`
).join("");
el.setAttribute("d", `M${right} ${top}${ls}`);
g.append(el);
}
else
for (const { left, bottom, width } of rects) {
const el = createSVGElement("path");
const n = Math.round(width / block / 1.5);
const inline = width / n;
const ls = Array.from(
{ length: n },
(_, i) => `l${inline} ${i % 2 ? block : -block}`
).join("");
el.setAttribute("d", `M${left} ${bottom}${ls}`);
g.append(el);
}
return g;
}
static highlight(rects, options = {}) {
const { color = "red" } = options;
const g = createSVGElement("g");
g.setAttribute("fill", color);
g.style.opacity = "var(--overlayer-highlight-opacity, .3)";
g.style.mixBlendMode = "var(--overlayer-highlight-blend-mode, normal)";
for (const { left, top, height, width } of rects) {
const el = createSVGElement("rect");
el.setAttribute("x", left);
el.setAttribute("y", top);
el.setAttribute("height", height);
el.setAttribute("width", width);
g.append(el);
}
return g;
}
static outline(rects, options = {}) {
const { color = "red", width: strokeWidth = 3, radius = 3 } = options;
const g = createSVGElement("g");
g.setAttribute("fill", "none");
g.setAttribute("stroke", color);
g.setAttribute("stroke-width", strokeWidth);
for (const { left, top, height, width } of rects) {
const el = createSVGElement("rect");
el.setAttribute("x", left);
el.setAttribute("y", top);
el.setAttribute("height", height);
el.setAttribute("width", width);
el.setAttribute("rx", radius);
g.append(el);
}
return g;
}
// make an exact copy of an image in the overlay
// one can then apply filters to the entire element, without affecting them;
// it's a bit silly and probably better to just invert images twice
// (though the color will be off in that case if you do heu-rotate)
static copyImage([rect], options = {}) {
const { src } = options;
const image = createSVGElement("image");
const { left, top, height, width } = rect;
image.setAttribute("href", src);
image.setAttribute("x", left);
image.setAttribute("y", top);
image.setAttribute("height", height);
image.setAttribute("width", width);
return image;
}
}
const walkRange = (range, walker) => {
const nodes = [];
for (let node = walker.currentNode; node; node = walker.nextNode()) {
const compare = range.comparePoint(node, 0);
if (compare === 0)
nodes.push(node);
else if (compare > 0)
break;
}
return nodes;
};
const walkDocument = (_, walker) => {
const nodes = [];
for (let node = walker.nextNode(); node; node = walker.nextNode())
nodes.push(node);
return nodes;
};
const filter = NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_CDATA_SECTION;
const acceptNode = (node) => {
if (node.nodeType === 1) {
const name = node.tagName.toLowerCase();
if (name === "script" || name === "style")
return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_SKIP;
}
return NodeFilter.FILTER_ACCEPT;
};
const textWalker = function* (x, func, filterFunc) {
const root = x.commonAncestorContainer ?? x.body ?? x;
const walker = document.createTreeWalker(root, filter, { acceptNode: filterFunc || acceptNode });
const walk = x.commonAncestorContainer ? walkRange : walkDocument;
const nodes = walk(x, walker);
const strs = nodes.map((node) => node.nodeValue);
const makeRange = (startIndex, startOffset, endIndex, endOffset) => {
const range = document.createRange();
range.setStart(nodes[startIndex], startOffset);
range.setEnd(nodes[endIndex], endOffset);
return range;
};
for (const match of func(strs, makeRange))
yield match;
};
console.log("textWalker", textWalker);
const SEARCH_PREFIX = "foliate-search:";
const isZip = async (file) => {
const arr = new Uint8Array(await file.slice(0, 4).arrayBuffer());
return arr[0] === 80 && arr[1] === 75 && arr[2] === 3 && arr[3] === 4;
};
const isPDF = async (file) => {
const arr = new Uint8Array(await file.slice(0, 5).arrayBuffer());
return arr[0] === 37 && arr[1] === 80 && arr[2] === 68 && arr[3] === 70 && arr[4] === 45;
};
const isCBZ = ({ name, type }) => type === "application/vnd.comicbook+zip" || name.endsWith(".cbz");
const isFB2 = ({ name, type }) => type === "application/x-fictionbook+xml" || name.endsWith(".fb2");
const isFBZ = ({ name, type }) => type === "application/x-zip-compressed-fb2" || name.endsWith(".fb2.zip") || name.endsWith(".fbz");
const makeZipLoader = async (file) => {
const { configure, ZipReader, BlobReader, TextWriter, BlobWriter } = await import("./zip-24umRHzb.js");
configure({ useWebWorkers: false });
const reader = new ZipReader(new BlobReader(file));
const entries = await reader.getEntries();
const map = new Map(entries.map((entry) => [entry.filename, entry]));
const load = (f) => (name, ...args) => map.has(name) ? f(map.get(name), ...args) : null;
const loadText = load((entry) => entry.getData(new TextWriter()));
const loadBlob = load((entry, type) => entry.getData(new BlobWriter(type)));
const getSize = (name) => {
var _a;
return ((_a = map.get(name)) == null ? void 0 : _a.uncompressedSize) ?? 0;
};
return { entries, loadText, loadBlob, getSize };
};
const getFileEntries = async (entry) => entry.isFile ? entry : (await Promise.all(Array.from(
await new Promise((resolve, reject) => entry.createReader().readEntries((entries) => resolve(entries), (error) => reject(error))),
getFileEntries
))).flat();
const makeDirectoryLoader = async (entry) => {
const entries = await getFileEntries(entry);
const files = await Promise.all(
entries.map((entry2) => new Promise((resolve, reject) => entry2.file(
(file) => resolve([file, entry2.fullPath]),
(error) => reject(error)
)))
);
const map = new Map(files.map(([file, path]) => [path.replace(entry.fullPath + "/", ""), file]));
const decoder = new TextDecoder();
const decode = (x) => x ? decoder.decode(x) : null;
const getBuffer = (name) => {
var _a;
return ((_a = map.get(name)) == null ? void 0 : _a.arrayBuffer()) ?? null;
};
const loadText = async (name) => decode(await getBuffer(name));
const loadBlob = (name) => map.get(name);
const getSize = (name) => {
var _a;
return ((_a = map.get(name)) == null ? void 0 : _a.size) ?? 0;
};
return { loadText, loadBlob, getSize };
};
class ResponseError extends Error {
}
class NotFoundError extends Error {
}
class UnsupportedTypeError extends Error {
}
const fetchFile = async (url) => {
const res = await fetch(url);
if (!res.ok)
throw new ResponseError(
`${res.status} ${res.statusText}`,
{ cause: res }
);
return new File([await res.blob()], new URL(res.url).pathname);
};
const makeBook = async (file) => {
if (typeof file === "string")
file = await fetchFile(file);
let book;
if (file.isDirectory) {
const loader = await makeDirectoryLoader(file);
const { EPUB } = await import("./epub-ByUtcnCJ.js");
book = await new EPUB(loader).init();
} else if (!file.size)
throw new NotFoundError("File not found");
else if (await isZip(file)) {
const loader = await makeZipLoader(file);
if (isCBZ(file)) {
const { makeComicBook } = await import("./comic-book-OguLtdSd.js");
book = makeComicBook(loader, file);
} else if (isFBZ(file)) {
const { makeFB2 } = await import("./fb2-DNkqwccS.js");
const { entries } = loader;
const entry = entries.find((entry2) => entry2.filename.endsWith(".fb2"));
const blob = await loader.loadBlob((entry ?? entries[0]).filename);
book = await makeFB2(blob);
} else {
const { EPUB } = await import("./epub-ByUtcnCJ.js");
book = await new EPUB(loader).init();
}
} else if (await isPDF(file)) {
const { makePDF } = await import("./pdf-D__QPVJx.js");
book = await makePDF(file);
} else {
const { isMOBI, MOBI } = await import("./mobi-BqPNzr7I.js");
if (await isMOBI(file)) {
const fflate = await import("./fflate-Bs35_BDF.js");
book = await new MOBI({ unzlib: fflate.unzlibSync }).open(file);
} else if (isFB2(file)) {
const { makeFB2 } = await import("./fb2-DNkqwccS.js");
book = await makeFB2(file);
}
}
if (!book)
throw new UnsupportedTypeError("File type not supported");
return book;
};
class CursorAutohider {
#timeout;
#el;
#check;
#state;
constructor(el, check, state = {}) {
this.#el = el;
this.#check = check;
this.#state = state;
if (this.#state.hidden)
this.hide();
this.#el.addEventListener("mousemove", ({ screenX, screenY }) => {
if (screenX === this.#state.x && screenY === this.#state.y)
return;
this.#state.x = screenX, this.#state.y = screenY;
this.show();
if (this.#timeout)
clearTimeout(this.#timeout);
if (check())
this.#timeout = setTimeout(this.hide.bind(this), 1e3);
}, false);
}
cloneFor(el) {
return new CursorAutohider(el, this.#check, this.#state);
}
hide() {
this.#el.style.cursor = "none";
this.#state.hidden = true;
}
show() {
this.#el.style.removeProperty("cursor");
this.#state.hidden = false;
}
}
class History extends EventTarget {
#arr = [];
#index = -1;
pushState(x) {
const last = this.#arr[this.#index];
if (last === x || (last == null ? void 0 : last.fraction) && last.fraction === x.fraction)
return;
this.#arr[++this.#index] = x;
this.#arr.length = this.#index + 1;
this.dispatchEvent(new Event("index-change"));
}
replaceState(x) {
const index = this.#index;
this.#arr[index] = x;
}
back() {
const index = this.#index;
if (index <= 0)
return;
const detail = { state: this.#arr[index - 1] };
this.#index = index - 1;
this.dispatchEvent(new CustomEvent("popstate", { detail }));
this.dispatchEvent(new Event("index-change"));
}
forward() {
const index = this.#index;
if (index >= this.#arr.length - 1)
return;
const detail = { state: this.#arr[index + 1] };
this.#index = index + 1;
this.dispatchEvent(new CustomEvent("popstate", { detail }));
this.dispatchEvent(new Event("index-change"));
}
get canGoBack() {
return this.#index > 0;
}
get canGoForward() {
return this.#index < this.#arr.length - 1;
}
clear() {
this.#arr = [];
this.#index = -1;
}
}
const languageInfo = (lang) => {
var _a, _b;
if (!lang)
return {};
try {
const canonical = Intl.getCanonicalLocales(lang)[0];
const locale = new Intl.Locale(canonical);
const isCJK = ["zh", "ja", "kr"].includes(locale.language);
const direction = (_b = ((_a = locale.getTextInfo) == null ? void 0 : _a.call(locale)) ?? locale.textInfo) == null ? void 0 : _b.direction;
return { canonical, locale, isCJK, direction };
} catch (e) {
console.warn(e);
return {};
}
};
class View extends HTMLElement {
#root = this.attachShadow({ mode: "closed" });
#sectionProgress;
#tocProgress;
#pageProgress;
#searchResults = /* @__PURE__ */ new Map();
#cursorAutohider = new CursorAutohider(this, () => this.hasAttribute("autohide-cursor"));
isFixedLayout = false;
lastLocation;
history = new History();
constructor() {
super();
this.history.addEventListener("popstate", ({ detail }) => {
const resolved = this.resolveNavigation(detail.state);
this.renderer.goTo(resolved);
});
}
async open(book) {
var _a, _b;
if (typeof book === "string" || typeof book.arrayBuffer === "function" || book.isDirectory)
book = await makeBook(book);
this.book = book;
this.language = languageInfo((_a = book.metadata) == null ? void 0 : _a.language);
if (book.splitTOCHref && book.getTOCFragment) {
const ids = book.sections.map((s) => s.id);
this.#sectionProgress = new SectionProgress(book.sections, 1500, 1600);
const splitHref = book.splitTOCHref.bind(book);
const getFragment = book.getTOCFragment.bind(book);
this.#tocProgress = new TOCProgress();
await this.#tocProgress.init({
toc: book.toc ?? [],
ids,
splitHref,
getFragment
});
this.#pageProgress = new TOCProgress();
await this.#pageProgress.init({
toc: book.pageList ?? [],
ids,
splitHref,
getFragment
});
}
this.isFixedLayout = ((_b = this.book.rendition) == null ? void 0 : _b.layout) === "pre-paginated";
if (this.isFixedLayout) {
await import("./fixed-layout-CKHLslVQ.js");
this.renderer = document.createElement("foliate-fxl");
} else {
await import("./paginator-DAQM1rJW.js");
this.renderer = document.createElement("foliate-paginator");
}
this.renderer.setAttribute("exportparts", "head,foot,filter");
this.renderer.addEventListener("load", (e) => this.#onLoad(e.detail));
this.renderer.addEventListener("relocate", (e) => this.#onRelocate(e.detail));
this.renderer.addEventListener("create-overlayer", (e) => e.detail.attach(this.#createOverlayer(e.detail)));
this.renderer.open(book);
this.#root.append(this.renderer);
if (book.sections.some((section) => section.mediaOverlay)) {
const activeClass = book.media.activeClass;
const playbackActiveClass = book.media.playbackActiveClass;
this.mediaOverlay = book.getMediaOverlay();
let lastActive;
this.mediaOverlay.addEventListener("highlight", (e) => {
const resolved = this.resolveNavigation(e.detail.text);
this.renderer.goTo(resolved).then(() => {
const { doc } = this.renderer.getContents().find((x) => x.index = resolved.index);
const el = resolved.anchor(doc);
el.classList.add(activeClass);
if (playbackActiveClass)
el.ownerDocument.documentElement.classList.add(playbackActiveClass);
lastActive = new WeakRef(el);
});
});
this.mediaOverlay.addEventListener("unhighlight", () => {
const el = lastActive == null ? void 0 : lastActive.deref();
if (el) {
el.classList.remove(activeClass);
if (playbackActiveClass)
el.ownerDocument.documentElement.classList.remove(playbackActiveClass);
}
});
}
}
close() {
var _a, _b;
(_a = this.renderer) == null ? void 0 : _a.destroy();
(_b = this.renderer) == null ? void 0 : _b.remove();
this.#sectionProgress = null;
this.#tocProgress = null;
this.#pageProgress = null;
this.#searchResults = /* @__PURE__ */ new Map();
this.lastLocation = null;
this.history.clear();
this.tts = null;
this.mediaOverlay = null;
}
goToTextStart() {
var _a, _b;
return this.goTo(((_b = (_a = this.book.landmarks) == null ? void 0 : _a.find((m) => m.type.includes("bodymatter") || m.type.includes("text"))) == null ? void 0 : _b.href) ?? this.book.sections.findIndex((s) => s.linear !== "no"));
}
async init({ lastLocation, showTextStart }) {
const resolved = lastLocation ? this.resolveNavigation(lastLocation) : null;
if (resolved) {
await this.renderer.goTo(resolved);
this.history.pushState(lastLocation);
} else if (showTextStart)
await this.goToTextStart();
else {
this.history.pushState(0);
await this.next();
}
}
#emit(name, detail, cancelable) {
return this.dispatchEvent(new CustomEvent(name, { detail, cancelable }));
}
#onRelocate({ reason, range, index, fraction, size }) {
var _a, _b, _c;
const progress = ((_a = this.#sectionProgress) == null ? void 0 : _a.getProgress(index, fraction, size)) ?? {};
const tocItem = (_b = this.#tocProgress) == null ? void 0 : _b.getProgress(index, range);
const pageItem = (_c = this.#pageProgress) == null ? void 0 : _c.getProgress(index, range);
const cfi = this.getCFI(index, range);
this.lastLocation = { ...progress, tocItem, pageItem, cfi, range };
if (reason === "snap" || reason === "page" || reason === "scroll")
this.history.replaceState(cfi);
this.#emit("relocate", this.lastLocation);
}
#onLoad({ doc, index }) {
doc.documentElement.lang ||= this.language.canonical ?? "";
if (!this.language.isCJK)
doc.documentElement.dir ||= this.language.direction ?? "";
this.#handleLinks(doc, index);
this.#cursorAutohider.cloneFor(doc.documentElement);
this.#emit("load", { doc, index });
}
#handleLinks(doc, index) {
const { book } = this;
const section = book.sections[index];
doc.addEventListener("click", (e) => {
var _a, _b;
const a = e.target.closest("a[href]");
if (!a)
return;
e.preventDefault();
const href_ = a.getAttribute("href");
const href = ((_a = section == null ? void 0 : section.resolveHref) == null ? void 0 : _a.call(section, href_)) ?? href_;
if ((_b = book == null ? void 0 : book.isExternal) == null ? void 0 : _b.call(book, href))
Promise.resolve(this.#emit("external-link", { a, href }, true)).then((x) => x ? globalThis.open(href, "_blank") : null).catch((e2) => console.error(e2));
else
Promise.resolve(this.#emit("link", { a, href }, true)).then((x) => x ? this.goTo(href) : null).catch((e2) => console.error(e2));
});
}
async addAnnotation(annotation, remove) {
var _a;
const { value } = annotation;
if (value.startsWith(SEARCH_PREFIX)) {
const cfi = value.replace(SEARCH_PREFIX, "");
const { index: index2, anchor: anchor2 } = await this.resolveNavigation(cfi);
const obj2 = this.#getOverlayer(index2);
if (obj2) {
const { overlayer, doc } = obj2;
if (remove) {
overlayer.remove(value);
return;
}
const range = doc ? anchor2(doc) : anchor2;
overlayer.add(value, range, Overlayer.outline);
}
return;
}
const { index, anchor } = await this.resolveNavigation(value);
const obj = this.#getOverlayer(index);
if (obj) {
const { overlayer, doc } = obj;
overlayer.remove(value);
if (!remove) {
const range = doc ? anchor(doc) : anchor;
const draw = (func, opts) => overlayer.add(value, range, func, opts);
this.#emit("draw-annotation", { draw, annotation, doc, range });
}
}
const label = ((_a = this.#tocProgress.getProgress(index)) == null ? void 0 : _a.label) ?? "";
return { index, label };
}
deleteAnnotation(annotation) {
return this.addAnnotation(annotation, true);
}
#getOverlayer(index) {
return this.renderer.getContents().find((x) => x.index === index && x.overlayer);
}
#createOverlayer({ doc, index }) {
const overlayer = new Overlayer();
doc.addEventListener("click", (e) => {
const [value, range] = overlayer.hitTest(e);
if (value && !value.startsWith(SEARCH_PREFIX)) {
this.#emit("show-annotation", { value, index, range });
}
}, false);
const list = this.#searchResults.get(index);
if (list)
for (const item of list)
this.addAnnotation(item);
this.#emit("create-overlay", { index });
return overlayer;
}
async showAnnotation(annotation) {
const { value } = annotation;
const resolved = await this.goTo(value);
if (resolved) {
const { index, anchor } = resolved;
const { doc } = this.#getOverlayer(index);
const range = anchor(doc);
this.#emit("show-annotation", { value, index, range });
}
}
getCFI(index, range) {
const baseCFI = this.book.sections[index].cfi ?? fake.fromIndex(index);
if (!range)
return baseCFI;
return joinIndir(baseCFI, fromRange(range));
}
resolveCFI(cfi) {
if (this.book.resolveCFI)
return this.book.resolveCFI(cfi);
else {
const parts = parse(cfi);
const index = fake.toIndex((parts.parent ?? parts).shift());
const anchor = (doc) => toRange(doc, parts);
return { index, anchor };
}
}
resolveNavigation(target) {
try {
if (typeof target === "number")
return { index: target };
if (typeof target.fraction === "number") {
const [index, anchor] = this.#sectionProgress.getSection(target.fraction);
return { index, anchor };
}
if (isCFI.test(target))
return this.resolveCFI(target);
return this.book.resolveHref(target);
} catch (e) {
console.error(e);
console.error(`Could not resolve target ${target}`);
}
}
async goTo(target) {
const resolved = this.resolveNavigation(target);
try {
await this.renderer.goTo(resolved);
this.history.pushState(target);
return resolved;
} catch (e) {
console.error(e);
console.error(`Could not go to ${target}`);
}
}
async goToFraction(frac) {
const [index, anchor] = this.#sectionProgress.getSection(frac);
await this.renderer.goTo({ index, anchor });
this.history.pushState({ fraction: frac });
}
async select(target) {
try {
const obj = await this.resolveNavigation(target);
await this.renderer.goTo({ ...obj, select: true });
this.history.pushState(target);
} catch (e) {
console.error(e);
console.error(`Could not go to ${target}`);
}
}
deselect() {
for (const { doc } of this.renderer.getContents())
doc.defaultView.getSelection().removeAllRanges();
}
getSectionFractions() {
var _a;
return (((_a = this.#sectionProgress) == null ? void 0 : _a.sectionFractions) ?? []).map((x) => x + Number.EPSILON);
}
getProgressOf(index, range) {
var _a, _b;
const tocItem = (_a = this.#tocProgress) == null ? void 0 : _a.getProgress(index, range);
const pageItem = (_b = this.#pageProgress) == null ? void 0 : _b.getProgress(index, range);
return { tocItem, pageItem };
}
async getTOCItemOf(target) {
try {
const { index, anchor } = await this.resolveNavigation(target);
const doc = await this.book.sections[index].createDocument();
const frag = anchor(doc);
const isRange = frag instanceof Range;
const range = isRange ? frag : doc.createRange();
if (!isRange)
range.selectNodeContents(frag);
return this.#tocProgress.getProgress(index, range);
} catch (e) {
console.error(e);
console.error(`Could not get ${target}`);
}
}
async prev(distance) {
await this.renderer.prev(distance);
}
async next(distance) {
await this.renderer.next(distance);
}
goLeft() {
return this.book.dir === "rtl" ? this.next() : this.prev();
}
goRight() {
return this.book.dir === "rtl" ? this.prev() : this.next();
}
async *#searchSection(matcher, query, index) {
const doc = await this.book.sections[index].createDocument();
for (const { range, excerpt } of matcher(doc, query))
yield { cfi: this.getCFI(index, range), excerpt };
}
async *#searchBook(matcher, query) {
const { sections } = this.book;
for (const [index, { createDocument }] of sections.entries()) {
if (!createDocument)
continue;
const doc = await createDocument();
const subitems = Array.from(matcher(doc, query), ({ range, excerpt }) => ({ cfi: this.getCFI(index, range), excerpt }));
const progress = (index + 1) / sections.length;
yield { progress };
if (subitems.length)
yield { index, subitems };
}
}
async *search(opts) {
var _a;
this.clearSearch();
const { searchMatcher } = await import("./search-CRYLa_WN.js");
const { query, index } = opts;
const matcher = searchMatcher(
textWalker,
{ defaultLocale: this.language, ...opts }
);
const iter = index != null ? this.#searchSection(matcher, query, index) : this.#searchBook(matcher, query);
const list = [];
this.#searchResults.set(index, list);
for await (const result of iter) {
if (result.subitems) {
const list2 = result.subitems.map(({ cfi }) => ({ value: SEARCH_PREFIX + cfi }));
this.#searchResults.set(result.index, list2);
for (const item of list2)
this.addAnnotation(item);
yield {
label: ((_a = this.#tocProgress.getProgress(result.index)) == null ? void 0 : _a.label) ?? "",
subitems: result.subitems
};
} else {
if (result.cfi) {
const item = { value: SEARCH_PREFIX + result.cfi };
list.push(item);
this.addAnnotation(item);
}
yield result;
}
}
yield "done";
}
clearSearch() {
for (const list of this.#searchResults.values())
for (const item of list)
this.deleteAnnotation(item);
this.#searchResults.clear();
}
async initTTS(granularity = "word", highlight) {
const doc = this.renderer.getContents()[0].doc;
if (this.tts && this.tts.doc === doc)
return;
const { TTS } = await import("./tts-CWBNTWh7.js");
this.tts = new TTS(doc, textWalker, highlight || ((range) => this.renderer.scrollToAnchor(range, true)), granularity);
}
startMediaOverlay() {
const { index } = this.renderer.getContents()[0];
return this.mediaOverlay.start(index);
}
}
customElements.define("foliate-view", View);
const view = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
NotFoundError,
ResponseError,
UnsupportedTypeError,
View,
makeBook
}, Symbol.toStringTag, { value: "Module" }));
export {
toRange as a,
fromElements as f,
parse as p,
toElement as t,
view as v
};