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>
341 lines (340 loc) • 11 kB
JavaScript
;
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const normalizeWhitespace = (str) => str ? str.replace(/[\t\n\f\r ]+/g, " ").replace(/^[\t\n\f\r ]+/, "").replace(/[\t\n\f\r ]+$/, "") : "";
const getElementText = (el) => normalizeWhitespace(el == null ? void 0 : el.textContent);
const NS = {
XLINK: "http://www.w3.org/1999/xlink",
EPUB: "http://www.idpf.org/2007/ops"
};
const MIME = {
XML: "application/xml",
XHTML: "application/xhtml+xml"
};
const STYLE = {
"strong": ["strong", "self"],
"emphasis": ["em", "self"],
"style": ["span", "self"],
"a": "anchor",
"strikethrough": ["s", "self"],
"sub": ["sub", "self"],
"sup": ["sup", "self"],
"code": ["code", "self"],
"image": "image"
};
const TABLE = {
"tr": ["tr", {
"th": ["th", STYLE, ["colspan", "rowspan", "align", "valign"]],
"td": ["td", STYLE, ["colspan", "rowspan", "align", "valign"]]
}, ["align"]]
};
const POEM = {
"epigraph": ["blockquote"],
"subtitle": ["h2", STYLE],
"text-author": ["p", STYLE],
"date": ["p", STYLE],
"stanza": "stanza"
};
const SECTION = {
"title": ["header", {
"p": ["h1", STYLE],
"empty-line": ["br"]
}],
"epigraph": ["blockquote", "self"],
"image": "image",
"annotation": ["aside"],
"section": ["section", "self"],
"p": ["p", STYLE],
"poem": ["blockquote", POEM],
"subtitle": ["h2", STYLE],
"cite": ["blockquote", "self"],
"empty-line": ["br"],
"table": ["table", TABLE],
"text-author": ["p", STYLE]
};
POEM["epigraph"].push(SECTION);
const BODY = {
"image": "image",
"title": ["section", {
"p": ["h1", STYLE],
"empty-line": ["br"]
}],
"epigraph": ["section", SECTION],
"section": ["section", SECTION]
};
class FB2Converter {
constructor(fb2) {
this.fb2 = fb2;
this.doc = document.implementation.createDocument(NS.XHTML, "html");
this.bins = new Map(Array.from(
this.fb2.getElementsByTagName("binary"),
(el) => [el.id, el]
));
}
getImageSrc(el) {
const href = el.getAttributeNS(NS.XLINK, "href");
if (!href)
return "data:,";
const [, id] = href.split("#");
if (!id)
return href;
const bin = this.bins.get(id);
return bin ? `data:${bin.getAttribute("content-type")};base64,${bin.textContent}` : href;
}
image(node) {
const el = this.doc.createElement("img");
el.alt = node.getAttribute("alt");
el.title = node.getAttribute("title");
el.setAttribute("src", this.getImageSrc(node));
return el;
}
anchor(node) {
const el = this.convert(node, { "a": ["a", STYLE] });
el.setAttribute("href", node.getAttributeNS(NS.XLINK, "href"));
if (node.getAttribute("type") === "note")
el.setAttributeNS(NS.EPUB, "epub:type", "noteref");
return el;
}
stanza(node) {
const el = this.convert(node, {
"stanza": ["p", {
"title": ["header", {
"p": ["strong", STYLE],
"empty-line": ["br"]
}],
"subtitle": ["p", STYLE]
}]
});
for (const child of node.children)
if (child.nodeName === "v") {
el.append(this.doc.createTextNode(child.textContent));
el.append(this.doc.createElement("br"));
}
return el;
}
convert(node, def) {
if (node.nodeType === 3)
return this.doc.createTextNode(node.textContent);
if (node.nodeType === 4)
return this.doc.createCDATASection(node.textContent);
if (node.nodeType === 8)
return this.doc.createComment(node.textContent);
const d = def == null ? void 0 : def[node.nodeName];
if (!d)
return null;
if (typeof d === "string")
return this[d](node);
const [name, opts, attrs] = d;
const el = this.doc.createElement(name);
if (node.id)
el.id = node.id;
el.classList.add(node.nodeName);
if (Array.isArray(attrs))
for (const attr of attrs) {
const value = node.getAttribute(attr);
if (value)
el.setAttribute(attr, value);
}
const childDef = opts === "self" ? def : opts;
let child = node.firstChild;
while (child) {
const childEl = this.convert(child, childDef);
if (childEl)
el.append(childEl);
child = child.nextSibling;
}
return el;
}
}
const parseXML = async (blob) => {
var _a;
const buffer = await blob.arrayBuffer();
const str = new TextDecoder("utf-8").decode(buffer);
const parser = new DOMParser();
const doc = parser.parseFromString(str, MIME.XML);
const encoding = doc.xmlEncoding || ((_a = str.match(/^<\?xml\s+version\s*=\s*["']1.\d+"\s+encoding\s*=\s*["']([A-Za-z0-9._-]*)["']/)) == null ? void 0 : _a[1]);
if (encoding && encoding.toLowerCase() !== "utf-8") {
const str2 = new TextDecoder(encoding).decode(buffer);
return parser.parseFromString(str2, MIME.XML);
}
return doc;
};
const style = URL.createObjectURL(new Blob([`
@namespace epub "http://www.idpf.org/2007/ops";
body > img, section > img {
display: block;
margin: auto;
}
.title h1 {
text-align: center;
}
body > section > .title, body.notesBodyType > .title {
margin: 3em 0;
}
body.notesBodyType > section .title h1 {
text-align: start;
}
body.notesBodyType > section .title {
margin: 1em 0;
}
p {
text-indent: 1em;
margin: 0;
}
:not(p) + p, p:first-child {
text-indent: 0;
}
.poem p {
text-indent: 0;
margin: 1em 0;
}
.text-author, .date {
text-align: end;
}
.text-author:before {
content: "—";
}
table {
border-collapse: collapse;
}
td, th {
padding: .25em;
}
a[epub|type~="noteref"] {
font-size: .75em;
vertical-align: super;
}
body:not(.notesBodyType) > .title, body:not(.notesBodyType) > .epigraph {
margin: 3em 0;
}
`], { type: "text/css" }));
const template = (html) => `<?xml version="1.0" encoding="utf-8"?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head><link href="${style}" rel="stylesheet" type="text/css"/></head>
<body>${html}</body>
</html>`;
const dataID = "data-foliate-id";
const makeFB2 = async (blob) => {
const book = {};
const doc = await parseXML(blob);
const converter = new FB2Converter(doc);
const $ = (x) => doc.querySelector(x);
const $$ = (x) => [...doc.querySelectorAll(x)];
const getPerson = (el) => {
const nick = getElementText(el.querySelector("nickname"));
if (nick)
return nick;
const first = getElementText(el.querySelector("first-name"));
const middle = getElementText(el.querySelector("middle-name"));
const last = getElementText(el.querySelector("last-name"));
const name = [first, middle, last].filter((x) => x).join(" ");
const sortAs = last ? [last, [first, middle].filter((x) => x).join(" ")].join(", ") : null;
return { name, sortAs };
};
const getDate = (el) => (el == null ? void 0 : el.getAttribute("value")) ?? getElementText(el);
const annotation = $("title-info annotation");
book.metadata = {
title: getElementText($("title-info book-title")),
identifier: getElementText($("document-info id")),
language: getElementText($("title-info lang")),
author: $$("title-info author").map(getPerson),
translator: $$("title-info translator").map(getPerson),
contributor: $$("document-info author").map(getPerson).concat($$("document-info program-used").map(getElementText)).map((x) => Object.assign(
typeof x === "string" ? { name: x } : x,
{ role: "bkp" }
)),
publisher: getElementText($("publish-info publisher")),
published: getDate($("title-info date")),
modified: getDate($("document-info date")),
description: annotation ? converter.convert(
annotation,
{ annotation: ["div", SECTION] }
).innerHTML : null,
subject: $$("title-info genre").map(getElementText)
};
if ($("coverpage image")) {
const src = converter.getImageSrc($("coverpage image"));
book.getCover = () => fetch(src).then((res) => res.blob());
} else
book.getCover = () => null;
const bodyData = Array.from(doc.querySelectorAll("body"), (body) => {
const converted = converter.convert(body, { body: ["body", BODY] });
return [Array.from(converted.children, (el) => {
const ids = [el, ...el.querySelectorAll("[id]")].map((el2) => el2.id);
return { el, ids };
}), converted];
});
const urls = [];
const sectionData = bodyData[0][0].map(({ el, ids }) => {
const titles = Array.from(
el.querySelectorAll(":scope > section > .title"),
(el2, index) => {
el2.setAttribute(dataID, index);
return { title: getElementText(el2), index };
}
);
return { ids, titles, el };
}).concat(bodyData.slice(1).map(([sections, body]) => {
const ids = sections.map((s) => s.ids).flat();
body.classList.add("notesBodyType");
return { ids, el: body, linear: "no" };
})).map(({ ids, titles, el, linear }) => {
var _a;
const str = template(el.outerHTML);
const blob2 = new Blob([str], { type: MIME.XHTML });
const url = URL.createObjectURL(blob2);
urls.push(url);
const title = normalizeWhitespace(
((_a = el.querySelector(".title, .subtitle, p")) == null ? void 0 : _a.textContent) ?? (el.classList.contains("title") ? el.textContent : "")
);
return {
ids,
title,
titles,
load: () => url,
createDocument: () => new DOMParser().parseFromString(str, MIME.XHTML),
// doo't count image data as it'd skew the size too much
size: blob2.size - Array.from(
el.querySelectorAll("[src]"),
(el2) => {
var _a2;
return ((_a2 = el2.getAttribute("src")) == null ? void 0 : _a2.length) ?? 0;
}
).reduce((a, b) => a + b, 0),
linear
};
});
const idMap = /* @__PURE__ */ new Map();
book.sections = sectionData.map((section, index) => {
const { ids, load, createDocument, size, linear } = section;
for (const id of ids)
if (id)
idMap.set(id, index);
return { id: index, load, createDocument, size, linear };
});
book.toc = sectionData.map(({ title, titles }, index) => {
const id = index.toString();
return {
label: title,
href: id,
subitems: (titles == null ? void 0 : titles.length) ? titles.map(({ title: title2, index: index2 }) => ({
label: title2,
href: `${id}#${index2}`
})) : null
};
}).filter((item) => item);
book.resolveHref = (href) => {
const [a, b] = href.split("#");
return a ? { index: Number(a), anchor: (doc2) => doc2.querySelector(`[${dataID}="${b}"]`) } : { index: idMap.get(b), anchor: (doc2) => doc2.getElementById(b) };
};
book.splitTOCHref = (href) => {
var _a;
return ((_a = href == null ? void 0 : href.split("#")) == null ? void 0 : _a.map((x) => Number(x))) ?? [];
};
book.getTOCFragment = (doc2, id) => doc2.querySelector(`[${dataID}="${id}"]`);
book.destroy = () => {
for (const url of urls)
URL.revokeObjectURL(url);
};
return book;
};
exports.makeFB2 = makeFB2;