// ==UserScript== // @name FicbookExtractor // @namespace 90h.yy.zz // @version 0.1.3 // @author Ox90 // @match https://ficbook.net/readfic/*/download // @description The script allows you to download books to an FB2 file without any limits // @description:ru Скрипт позволяет скачивать книги в FB2 файл без ограничений // @require https://greasyfork.org/scripts/468831-html2fb2lib/code/HTML2FB2Lib.js?version=1279138 // @grant GM.xmlHttpRequest // @license MIT // @downloadURL none // ==/UserScript== (function start() { const PROGRAM_NAME = GM_info.script.name; let stage = 0; function init() { try { updatePage(); } catch (err) { console.error(err); } } function updatePage() { const cs = document.querySelector("section.content-section>div.clearfix"); if (!cs) throw new Error("Ошибка идентификации блока download"); if (cs.querySelector(".fbe-download-section")) return; // Для отработки кнопки "Назад" в браузере. let ds = Array.from(cs.querySelectorAll("section.fanfic-download-option")).find(el => { const hdr = el.firstElementChild; return hdr.tagName === "H5" && hdr.textContent.endsWith(" fb2"); }); if (!ds) { ds = makeDownloadSection(); cs.append(ds); } ds.appendChild(makeDownloadButton()).querySelector("button.btn-primary").addEventListener("click", event => { event.preventDefault(); let log = null; let doc = new DocumentEx(); doc.idPrefix = "fbe_"; doc.programName = PROGRAM_NAME + " v" + GM_info.script.version; const dlg = new Dialog({ onsubmit: () => { makeAction(doc, dlg, log); }, onhide: () => { Loader.abortAll(); doc = null; if (dlg.link) { URL.revokeObjectURL(dlg.link.href); dlg.link = null; } } }); dlg.show(); log = new LogElement(dlg.log); dlg.button.textContent = setStage(0); makeAction(doc, dlg, log); }); } function makeDownloadSection() { const sec = document.createElement("section"); sec.classList.add("fanfic-download-option"); sec.innerHTML = "
используется, но в основном как * контейнер для выравнивания строк текста и подзаголовков. * --- * Перед парсингом блоки текста упаковываются в параграфы, разделитель - символ новой строки * Все пустые строки заменяются на empyty-line. Также учитывается вложенность других элементов. */ parse(htmlNode) { const doc = htmlNode.ownerDocument; const newNode = htmlNode.cloneNode(false); let nodeChain = [ doc.createElement("p") ]; newNode.append(nodeChain[0]); function insertText(text, newBlock) { if (newBlock) { if (nodeChain[0].textContent.trim() === "") { newNode.lastChild.remove(); newNode.append(doc.createElement("br")); } let parent = newNode; nodeChain = nodeChain.map(n => { const nn = n.cloneNode(false); parent = parent.appendChild(nn); return nn; }); parent.append(text); } else { nodeChain[nodeChain.length - 1].append(text); } } function rewriteChildNodes(node) { let cn = node.firstChild; while (cn) { if (cn.nodeName === "#text") { const lines = cn.textContent.split("\n"); for (let i = 0; i < lines.length; ++i) insertText(lines[i], i > 0); } else { const nn = cn.cloneNode(false); nodeChain[nodeChain.length - 1].append(nn); nodeChain.push(nn); rewriteChildNodes(cn); nodeChain.pop(); } cn = cn.nextSibling; } } rewriteChildNodes(htmlNode); return super.parse(newNode); } processElement(fb2el, depth) { if (fb2el instanceof FB2UnknownNode) this._unknownNodes.push(fb2el.value); return super.processElement(fb2el, depth); } } class AnnotationParser extends TextParser { run(doc, htmlNode) { this._annotation = new FB2Annotation(); const res = super.run(doc, htmlNode); this._annotation.normalize(); if (this._annotation.children.length) doc.annotation = this._annotation; delete this._annotation; return res; } processElement(fb2el, depth) { if (fb2el && !depth) this._annotation.children.push(fb2el); return super.processElement(fb2el, depth); } } class ChapterParser extends TextParser { run(doc, htmlNode, title, notes) { this._chapter = new FB2Chapter(title); this._noteValues = notes; const res = super.run(doc, htmlNode); this._chapter.normalize(); doc.chapters.push(this._chapter); delete this._chapter; return res; } startNode(node, depth, fb2to) { if (node.nodeName === "SPAN") { if (node.classList.contains("footnote") && node.textContent === "") { // Это заметка if (this._noteValues) { const value = this._noteValues[node.id]; if (value) { const nt = new FB2Note(value, ""); this.processElement(nt, depth); fb2to && fb2to.children.push(nt); } } return null; } } else if (node.nodeName === "P") { if (node.style.textAlign === "center" && [ "•••", "* * *", "***" ].includes(node.textContent.trim())) { // Это подзаголовок const sub = new FB2Subtitle("* * *") this.processElement(sub, depth); fb2to && fb2to.children.push(sub); return null; } } return super.startNode(node, depth, fb2to); } processElement(fb2el, depth) { if (fb2el && !depth) this._chapter.children.push(fb2el); return super.processElement(fb2el, depth); } } class Dialog { constructor(params) { this._onsubmit = params.onsubmit; this._onhide = params.onhide; this._dlgEl = null; this.log = null; this.button = null; } show() { this._mainEl = document.createElement("div"); this._mainEl.tabIndex = -1; this._mainEl.classList.add("modal"); this._mainEl.setAttribute("role", "dialog"); const backEl = document.createElement("div"); backEl.classList.add("modal-backdrop", "in"); backEl.style.zIndex = 0; backEl.addEventListener("click", () => this.hide()); const dlgEl = document.createElement("div"); dlgEl.classList.add("modal-dialog"); dlgEl.setAttribute("role", "document"); const ctnEl = document.createElement("div"); ctnEl.classList.add("modal-content"); dlgEl.append(ctnEl); const bdyEl = document.createElement("div"); bdyEl.classList.add("modal-body"); ctnEl.append(bdyEl); const tlEl = document.createElement("div"); const clBtn = document.createElement("button"); clBtn.classList.add("close"); clBtn.innerHTML = "×"; clBtn.addEventListener("click", () => this.hide()); const hdrEl = document.createElement("h3"); hdrEl.textContent = "Формирование файла FB2"; tlEl.append(clBtn, hdrEl); const container = document.createElement("form"); container.classList.add("modal-container"); container.addEventListener("submit", event => { event.preventDefault(); this._onsubmit && this._onsubmit(); }); bdyEl.append(tlEl, container); this.log = document.createElement("div"); const buttons = document.createElement("div"); buttons.style.display = "flex"; buttons.style.justifyContent = "center"; this.button = document.createElement("button"); this.button.type = "submit"; this.button.disabled = true; this.button.classList.add("btn", "btn-primary"); this.button.textContent = "Продолжить"; buttons.append(this.button); container.append(this.log, buttons); this._mainEl.append(backEl, dlgEl); const dlgList = document.querySelector("div.js-modal-destination"); if (!dlgList) throw new Error("Не найден контейнер для модальных окон"); dlgList.append(this._mainEl); document.body.classList.add("modal-open"); this._mainEl.style.display = "block"; this._mainEl.focus(); } hide() { this.log = null; this.button = null; this._mainEl && this._mainEl.remove(); document.body.classList.remove("modal-open"); this._onhide && this._onhide(); } } class LogElement { constructor(element) { element.style.padding = ".5em"; element.style.fontSize = "90%"; element.style.border = "1px solid lightgray"; element.style.marginBottom = "1em"; element.style.borderRadius = "5px"; element.style.textAlign = "left"; element.style.overflowY = "auto"; element.style.maxHeight = "50vh"; this._element = element; } message(message, color) { const item = document.createElement("div"); if (message instanceof HTMLElement) { item.appendChild(message); } else { item.textContent = message; } if (color) item.style.color = color; this._element.appendChild(item); this._element.scrollTop = this._element.scrollHeight; return new LogItemElement(item); } warning(s) { this.message(s, "#a00"); } } class LogItemElement { constructor(element) { this._element = element; this._span = null; } ok() { this._setSpan("ok", "green"); } fail() { this._setSpan("ошибка!", "red"); } skipped() { this._setSpan("пропущено", "blue"); } text(s) { this._setSpan(s, ""); } _setSpan(text, color) { if (!this._span) { this._span = document.createElement("span"); this._element.appendChild(this._span); } this._span.style.color = color; this._span.textContent = " " + text; } } class Loader { static async addJob(url, params) { if (!this.ctl_list) this.ctl_list = new Set(); params ||= {}; params.url = url; params.method ||= "GET"; params.responseType = params.responseType === "binary" ? "blob" : "text"; return new Promise((resolve, reject) => { let req = null; params.onload = r => { if (r.status === 200) { resolve(r.response); } else { reject(new Error("Сервер вернул ошибку (" + r.status + ")")); } }; params.onerror = err => reject(err); params.ontimeout = err => reject(err); params.onloadend = () => { if (req) this.ctl_list.delete(req); }; if (params.onprogress) { const progress = params.onprogress; params.onprogress = pe => { if (pe.lengthComputable) { progress(pe.loaded, pe.total); } }; } try { req = GM.xmlHttpRequest(params); if (req) this.ctl_list.add(req); } catch (err) { reject(err); } }); } static abortAll() { if (this.ctl_list) { this.ctl_list.forEach(ctl => ctl.abort()); this.ctl_list.clear(); } } } FB2Image.prototype._load = function(...args) { return Loader.addJob(...args); }; //------------------------- // Запускает скрипт после загрузки страницы сайта if (document.readyState === "loading") window.addEventListener("DOMContentLoaded", init); else init(); })();