UNPKG

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) 10.9 kB
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; }; export { makeFB2 };