您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
This is a library for converting HTML to FB2.
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/468831/1206302/HTML2FB2Lib.js
// ==UserScript== // @name HTML2FB2Lib // @namespace 90h.yy.zz // @version 0.1.0 // @author Ox90 // @match http://* // @match https://* // @description This is a library for converting HTML to FB2. // @run-at document-start // @license MIT // ==/UserScript== class FB2Parser { constructor() { this._stop = null; } async parse(htmlNode, fromNode) { const that = this; async function _parse(node, from, fb2el, depth) { let n = from || node.firstChild; while (n) { if (that.startNode(n, depth)) { const f = that.processElement(FB2Element.fromHTML(n, false), depth); if (f) { if (fb2el) fb2el.children.push(f); _parse(n, null, f, depth + 1); } that.endNode(n, depth); } if (that._stop) break; n = n.nextSibling; } } await _parse(htmlNode, fromNode, null, 0); return this._stop; } startNode(node, depth) { return true; } processElement(fb2el, depth) { return fb2el; } endNode(node, depth) { } } class FB2Document { constructor() { this.binaries = []; this.bookAuthors = []; this.annotation = null; this.genres = []; this.chapters = []; this.xmldoc = null; } toString() { this._ensureXMLDocument(); const root = this.xmldoc.documentElement; this._markBinaries(); root.appendChild(this._makeDescriptionElement()); root.appendChild(this._makeBodyElement()); this._makeBinaryElements().forEach(el => root.appendChild(el)); const res = (new XMLSerializer()).serializeToString(this.xmldoc); this.xmldoc = null; return res; } createElement(name) { this._ensureXMLDocument(); return this.xmldoc.createElementNS(this.xmldoc.documentElement.namespaceURI, name); } createTextNode(value) { this._ensureXMLDocument(); return this.xmldoc.createTextNode(value); } _ensureXMLDocument() { if (!this.xmldoc) { this.xmldoc = new DOMParser().parseFromString( '<?xml version="1.0" encoding="UTF-8"?><FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0"/>', "application/xml" ); this.xmldoc.documentElement.setAttribute("xmlns:l", "http://www.w3.org/1999/xlink"); } } _makeDescriptionElement() { const desc = this.createElement("description"); // title-info const t_info = this.createElement("title-info"); desc.appendChild(t_info); this.genres.forEach(g => t_info.appendChild(g.xml(this))); (this.bookAuthors.length ? this.bookAuthors : [ new FB2Author("Неизвестный автор") ]).forEach(a => { t_info.appendChild(a.xml(this)); }); t_info.appendChild((new FB2Element("book-title", this.bookTitle)).xml(this)); t_info.appendChild(this.annotation.xml(this)); if (this.keywords) t_info.appendChild(this.keywords.xml(this)); if (this.bookDate) { const el = this.createElement("date"); el.setAttribute("value", this.bookDate.toAtomDate()); el.textContent = this.bookDate.getFullYear(); t_info.appendChild(el); } if (this.coverpage) { const el = this.createElement("coverpage"); el.appendChild(this.coverpage.xml(this)); t_info.appendChild(el); } const lang = this.createElement("lang"); lang.textContent = "ru"; t_info.appendChild(lang); if (this.sequence) { const el = this.createElement("sequence"); el.setAttribute("name", this.sequence.name); if (this.sequence.number) el.setAttribute("number", this.sequence.number); t_info.appendChild(el); } // document-info const d_info = this.createElement("document-info"); desc.appendChild(d_info); d_info.appendChild((new FB2Author("Ox90")).xml(this)); d_info.appendChild((new FB2Element("program-used", PROGRAM_NAME + " v" + GM_info.script.version)).xml(this)); d_info.appendChild((() => { const f_time = new Date(); const el = this.createElement("date"); el.setAttribute("value", f_time.toAtomDate()); el.textContent = f_time.toUTCString(); return el; })()); if (this.sourceURL) { d_info.appendChild((new FB2Element("src-url", this.sourceURL)).xml(this)); } d_info.appendChild((new FB2Element("id", this._genBookId())).xml(this)); d_info.appendChild((new FB2Element("version", "1.0")).xml(this)); return desc; } _makeBodyElement() { const body = this.createElement("body"); const title = this.createElement("title"); body.appendChild(title); if (this.bookAuthors.length) title.appendChild((new FB2Paragraph(this.bookAuthors.join(", "))).xml(this)); title.appendChild((new FB2Paragraph(this.bookTitle)).xml(this)); this.chapters.forEach(ch => body.appendChild(ch.xml(this))); return body; } _markBinaries() { let idx = 0; this.binaries.forEach(img => { if (!img.id) img.id = "image" + (++idx) + img.suffix(); }); } _makeBinaryElements() { return this.binaries.reduce((list, img) => { if (img.value) list.push(img.xmlBinary(this)); return list; }, []); } _genBookId() { let str = this.sourceURL; let hash = 0; const slen = str.length; for (let i = 0; i < slen; ++i) { const ch = str.charCodeAt(i); hash = ((hash << 5) - hash) + ch; hash = hash & hash; // Convert to 32bit integer } return "rbe_" + Math.abs(hash).toString() + (hash > 0 ? "1" : ""); } } class FB2Element { constructor(name, value) { this.name = name; this.value = value !== undefined ? value : null; this.children = []; } static fromHTML(node, recursive) { let fb2el = null; const names = new Map([ [ "U", "emphasis" ], [ "EM", "emphasis" ], [ "EMPHASIS", "emphasis" ], [ "I", "emphasis" ], [ "S", "strike" ], [ "DEL", "strike" ], [ "STRIKE", "strike" ], [ "STRONG", "strong" ], [ "BLOCKQUOTE", "cite" ], [ "#comment", null ] ]); const node_name = node.nodeName; if (names.has(node_name)) { const name = names.get(node_name); if (!name) return null; fb2el = new FB2Element(names.get(node_name)); } else { switch (node_name) { case "#text": return new FB2Text(node.textContent); case "P": fb2el = new FB2Paragraph(); break; case "SUBTITLE": fb2el = new FB2Subtitle(); break; case "A": fb2el = new FB2Link(node.href || node.getAttribute("l:href")); break; case "BR": return new FB2EmptyLine(); case "HR": return new FB2Paragraph("---"); case "IMG": return new FB2Image(node.src); default: throw new Error("Неизвестный HTML блок: " + node.nodeName); } } if (recursive) fb2el.appendContentFromHTML(node); return fb2el; } hasValue() { return ((this.value !== undefined && this.value !== null) || !!this.children.length); } setContentFromHTML(data, fb2doc, log) { this.children = []; this.appendContentFromHTML(data, fb2doc, log); } appendContentFromHTML(data, fb2doc, log) { for (const node of data.childNodes) { let fe = FB2Element.fromHTML(node, true); if (fe) this.children.push(fe); } } normalize() { const res_list = [ this ]; let cur_el = this; const children = this.children; this.children = []; children.forEach(el => { if (el instanceof FB2EmptyLine || el instanceof FB2Subtitle) { res_list.push(el); cur_el = new this.constructor(); res_list.push(cur_el); } else { el.normalize().forEach(el => { cur_el.children.push(el); }); } }); return res_list; } xml(doc) { const el = doc.createElement(this.name); if (this.value !== null) el.textContent = this.value; this.children.forEach(ch => el.appendChild(ch.xml(doc))); return el; } } class FB2BlockElement extends FB2Element { normalize() { // Удалить пробельные символы в конце блока while (this.children.length) { const el = this.children[this.children.length - 1]; if (el.name === "text" && typeof(el.value) === "string") { el.value = el.value.trimEnd(); if (!el.value) { this.children.pop(); continue; } } break; } // Удалить пробельные символы в начале блока while (this.children.length) { const el = this.children[0]; if (el.name === "text" && typeof(el.value) === "string") { el.value = el.value.trimStart(); if (!el.value) { this.children.shift(); continue; } } break; } //-- return super.normalize(); } } /** * FB2 элемент верхнего уровня section */ class FB2Chapter extends FB2Element { constructor(title) { super("section"); this.title = title; } normalize() { // Обернуть текстовые ноды в параграфы и удалить пустые элементы this.children = this.children.reduce((list, el) => { if (el instanceof FB2Text) { const pe = new FB2Paragraph(); pe.children.push(el); el = pe; } el.normalize().forEach(el => { if (el.hasValue()) list.push(el); }); return list; }, []); return [ this ]; } xml(doc) { const el = super.xml(doc); if (this.title) { const t_el = doc.createElement("title"); const p_el = doc.createElement("p"); p_el.textContent = this.title; t_el.appendChild(p_el); el.prepend(t_el); } return el; } } /** * FB2 элемент верхнего уровня annotation */ class FB2Annotation extends FB2Element { constructor() { super("annotation"); } normalize() { // Обернуть неформатированный текст, разделенный <br> в параграфы let lp = null; const newParagraph = list => { lp = new FB2Paragraph(); list.push(lp); }; this.children = this.children.reduce((list, el) => { if (el.name === "empty-line") { newParagraph(list); } else if (el instanceof FB2BlockElement) { list.push(el); lp = null; } else { if (!lp) newParagraph(list); lp.children.push(el); } return list; }, []); // Запустить собственную нормализацию дочерних элементов // чтобы предотвратить их дальнейшее всплытие this.children = this.children.reduce((list, el) => { el.normalize().forEach(el => { if (el.hasValue()) list.push(el); }); return list; }, []); } } class FB2Subtitle extends FB2BlockElement { constructor(value) { super("subtitle", value); } } class FB2Paragraph extends FB2BlockElement { constructor(value) { super("p", value); } } class FB2EmptyLine extends FB2Element { constructor() { super("empty-line"); } hasValue() { return true; } } class FB2Text extends FB2Element { constructor(value) { super("text", value); } xml(doc) { return doc.createTextNode(this.value); } } class FB2Link extends FB2Element { constructor(href) { super("a"); this.href = href; } xml(doc) { const el = super.xml(doc); el.setAttribute("l:href", this.href); return el; } } class FB2Author extends FB2Element { constructor(s) { super("author"); const a = s.split(" "); switch (a.length) { case 1: this.nickName = s; break; case 2: this.firstName = a[0]; this.lastName = a[1]; break; default: this.firstName = a[0]; this.middleName = a.slice(1, -1).join(" "); this.lastName = a[a.length - 1]; break; } this.homePage = null; } hasValue() { return (!!this.firstName || !!this.lastName || !!this.middleName); } toString() { if (!this.firstName) return this.nickName; return [ this.firstName, this.middleName, this.lastName ].reduce((list, name) => { if (name) list.push(name); return list; }, []).join(" "); } xml(doc) { let a_el = super.xml(doc); [ [ "first-name", this.firstName ], [ "middle-name", this.middleName ], [ "last-name", this.lastName ], [ "home-page", this.homePage ], [ "nickname", this.nickName ] ].forEach(it => { if (it[1]) { const e = doc.createElement(it[0]); e.textContent = it[1]; a_el.appendChild(e); } }); return a_el; } } class FB2Image extends FB2Element { constructor(value) { super("image"); if (typeof(value) === "string") { this.url = value; } else { this.value = value; } } async load(onprogress) { if (this.url) { const bin = await this._load(this.url, { responseType: "binary", onprogress: onprogress }); this.type = bin.type; this.size = bin.size; return new Promise((resolve, reject) => { const reader = new FileReader(); reader.addEventListener("loadend", (event) => resolve(event.target.result)); reader.readAsDataURL(bin); }).then(base64str => { this.value = base64str.substr(base64str.indexOf(",") + 1); }).catch(err => { throw new Error("Ошибка загрузки изображения"); }); } } xml(doc) { if (this.value) { const el = doc.createElement(this.name); el.setAttribute("l:href", "#" + this.id); return el } const id = this.id || "изображение"; return doc.createTextNode(`[ ${id} ]`); } xmlBinary(doc) { const el = doc.createElement("binary"); el.setAttribute("id", this.id); el.setAttribute("content-type", this.type); el.textContent = this.value return el; } suffix() { switch (this.type) { case "image/png": return ".png"; case "image/jpeg": return ".jpg"; case "image/webp": return ".webp"; } return ""; } async _load(...args) { return FB2Loader.addJob(...args); } } class FB2Loader { static async addJob(url, params) { params ||= {}; const fp = {}; fp.method = params.method || "GET"; fp.credentials = "same-origin"; fp.signal = this._getSignal(); const resp = await fetch(url, fp); if (!resp.ok) throw new Error(`Сервер вернул ошибку (${resp.status})`); const reader = resp.body.getReader(); const type = resp.headers.get("Content-Type"); const total = +resp.headers.get("Content-Length"); let loaded = 0; const chunks = []; const onprogress = (total && typeof(params.onprogress) === "function") ? params.onprogress : null; while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); loaded += value.length; if (onprogress) onprogress(loaded, total); } switch (params.responseType) { case "binary": return new Blob(chunks, { type: type }); default: { let pos = 0; const data = new Uint8Array(loaded); for (let ch of chunks) { data.set(ch, pos); pos += ch.length; } return (new TextDecoder("utf-8")).decode(data); } } } static abortAll() { if (this._controller) { this._controller.abort(); this._controller = null; } } static _getSignal() { let controller = this._controller; if (!controller) this._controller = controller = new AbortController(); return controller.signal; } }