您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
The script allows you to download books to an FB2 file without any limits
当前为
- // ==UserScript==
- // @name FicbookExtractor
- // @namespace 90h.yy.zz
- // @version 0.6.2
- // @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://update.greasyfork.org/scripts/468831/1478439/HTML2FB2Lib.js
- // @grant GM.xmlHttpRequest
- // @license MIT
- // ==/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 = "<h5 class=\"font-bold\">Скачать в fb2</h5>";
- return sec;
- }
- function makeDownloadButton() {
- const ctn = document.createElement("div");
- ctn.classList.add("fanfic-download-container", "fbe-download-section");
- ctn.innerHTML =
- "<svg class=\"ic_document-file-fb2 mb-0 hidden-xs\" viewBox=\"0 0 45.1 45.1\">" +
- "<path d=\"M33.4,0H5.2v45.1h34.7V6.3L33.4,0z M36.9,42.1H8.2V3h23.7v4.8h5L36.9,42.1L36.9,42.1z\"></path>" +
- "<g><path d=\"M10.7,19h6.6v1.8h-3.9v1.5h3.3v1.7h-3.3v3.5h-2.7V19z\"></path>" +
- "<path d=\"M18.7,19h5c0.8,0,1.5,0.2,1.9,0.6s0.7,0.9,0.7,1.5c0,0.5-0.2,0.9-0.5,1.3c-0.2,0.2-0.5,0.4-0.9," +
- "0.6 c0.6,0.1,1.1,0.4,1.4,0.8s0.4,0.8,0.4,1.4c0,0.4-0.1,0.8-0.3,1.2s-0.5,0.6-0.8,0.8c-0.2,0.1-0.6," +
- "0.2-1,0.3c-0.6,0.1-1,0.1-1.2,0.1 h-4.6V19z M21.4,22.4h1.2c0.4,0,0.7-0.1,0.9-0.2s0.2-0.3," +
- "0.2-0.6c0-0.2-0.1-0.4-0.2-0.6s-0.4-0.2-0.8-0.2h-1.2V22.4z M21.4,25.8 h1.4c0.5,0,0.8-0.1,1-0.2s0.3-0.4," +
- "0.3-0.7c0-0.3-0.1-0.5-0.3-0.6s-0.5-0.2-1-0.2h-1.3V25.8z\"></path>" +
- "<path d=\"M34.7,27.6h-7.2c0.1-0.7,0.3-1.4,0.7-2s1.2-1.4,2.3-2.2c0.7-0.5,1.1-0.9,1.3-1.2s0.3-0.5," +
- "0.3-0.8c0-0.3-0.1-0.5-0.3-0.7 s-0.4-0.3-0.7-0.3c-0.3,0-0.6,0.1-0.7,0.3s-0.3,0.5-0.4,1l-2.4-0.2c0.1-0.7," +
- "0.3-1.2,0.5-1.6s0.6-0.7,1.1-0.9s1.1-0.3,1.9-0.3 c0.8,0,1.5,0.1,2,0.3s0.8,0.5,1.1,0.9s0.4,0.8,0.4,1.3c0," +
- "0.5-0.2,1-0.5,1.5s-0.9,1-1.7,1.6c-0.5,0.3-0.8,0.6-1,0.7 s-0.4,0.3-0.6,0.5h3.7V27.6z\"></path></g></svg>" +
- "<div class=\"fanfic-download-description\">FB2 - формат электронных книг. Лимиты не действуют. " +
- "Скачивайте и наслаждайтесь! <em style=\"color:#c69e6b; margin-left:.75em; white-space:nowrap;\">" +
- "[ from FicbookExtractor with love ]</em></div>" +
- "<button class=\"btn btn-primary btn-responsive\">" +
- "<svg class=\"ic_download\" viewBox=\"0 0 32 32\">" +
- "<path d=\"M6 32h20a6 6 0 0 0 6-6H0a6 6 0 0 0 6 6zm20-4h2v2h-2v-2zM25 8l-9 9-9-9h7V0h4v8zm7 15c.1.6-.2 1-.8" +
- " 1H.8c-.6 0-1-.4-.8-1l3.5-10c.2-.6.8-1 1.3-1H7l8 8h2l8-8h2.2c.5 0 1.1.4 1.3 1L32 23z\"></path>" +
- "</svg> Скачать</button>";
- return ctn;
- }
- async function makeAction(doc, dlg, log) {
- try {
- switch (stage) {
- case 0:
- await getBookInfo(doc, log);
- dlg.button.textContent = setStage(1);
- dlg.button.disabled = false;
- break;
- case 1:
- dlg.button.textContent = setStage(2);
- await getBookContent(doc, log);
- dlg.button.textContent = setStage(3);
- break;
- case 2:
- Loader.abortAll();
- dlg.button.textContent = setStage(4);
- break;
- case 3:
- if (!dlg.link) {
- dlg.link = document.createElement("a");
- dlg.link.download = genBookFileName(doc);
- dlg.link.href = URL.createObjectURL(new Blob([ doc ], { type: "application/octet-stream" }));
- }
- dlg.link.click();
- break;
- case 4:
- dlg.hide();
- break;
- }
- } catch (err) {
- console.error(err);
- log.message(err.message, "red");
- dlg.button.textContent = setStage(4);
- dlg.button.disabled = false;
- }
- }
- function setStage(newStage) {
- stage = newStage;
- return [ "Анализ...", "Продолжить", "Прервать", "Сохранить в файл", "Закрыть" ][newStage] || "Error";
- }
- function getBookInfoElement(htmlString) {
- const doc = (new DOMParser()).parseFromString(htmlString, "text/html");
- return doc.querySelector("section.chapter-info");
- }
- async function getBookInfo(doc, log) {
- const logTitle = log.message("Название:");
- const logAuthors = log.message("Авторы:");
- const logTags = log.message("Теги:");
- const logUpdate = log.message("Последнее обновление:");
- const logChapters = log.message("Всего глав:");
- //--
- const idR = /^\/readfic\/([^\/]+)/.exec(document.location.pathname);
- if (!idR) throw new Error("Не найден id произведения");
- const url = new URL(`/readfic/${encodeURIComponent(idR[1])}`, document.location);
- const bookEl = getBookInfoElement(await Loader.addJob(url));
- if (!bookEl) throw new Error("Не найдено описание произведения");
- // ID произведения
- doc.id = idR[1];
- // Название произведения
- doc.bookTitle = (() => {
- const el = bookEl.querySelector("h1[itemprop=name]") || bookEl.querySelector("h1[itemprop=headline]");
- const str = el && el.textContent.trim() || null;
- if (!str) throw new Error("Не найдено название произведения");
- return str;
- })();
- logTitle.text(doc.bookTitle);
- // Авторы
- doc.bookAuthors = (() => {
- return Array.from(
- bookEl.querySelectorAll(".hat-creator-container .creator-info a.creator-username + i")
- ).reduce((list, el) => {
- if ([ "автор", "соавтор", "переводчик", "сопереводчик" ].includes(el.textContent.trim().toLowerCase())) {
- const name = el.previousElementSibling.textContent.trim();
- if (name) {
- const au = new FB2Author(name);
- au.homePage = el.href;
- list.push(au);
- }
- }
- return list;
- }, []);
- })();
- logAuthors.text(doc.bookAuthors.length || "нет");
- if (!doc.bookAuthors.length) log.warning("Не найдена информация об авторах");
- // Жанры
- doc.genres = new FB2GenreList([ "фанфик" ]);
- // Ключевые слова
- doc.keywords = (() => {
- // Селектор :not(.hidden) исключает спойлерные теги
- return Array.from(bookEl.querySelectorAll(".tags a.tag[href^=\"/tags/\"]:not(.hidden)")).reduce((list, el) => {
- const tag = el.textContent.trim();
- if (tag) list.push(tag);
- return list;
- }, []);
- })();
- logTags.text(doc.keywords.length || "нет");
- // Список глав
- const chapters = getChaptersList(bookEl);
- if (!chapters.length) {
- // Возможно это короткий рассказ, так что есть шанс, что единственная глава находится тут же.
- const chData = getChapterData(bookEl);
- if (chData) {
- const titleEl = bookEl.querySelector("article .title-area h2");
- const title = titleEl && titleEl.textContent.trim();
- const pubEl = bookEl.querySelector("article div[itemprop=datePublished] span");
- const published = pubEl && pubEl.title || "";
- chapters.push({
- id: null,
- title: title !== doc.bookTitle ? title : null,
- updated: published,
- data: chData
- });
- }
- }
- // Дата произведения (последнее обновление)
- const months = new Map([
- [ "января", "01" ], [ "февраля", "02" ], [ "марта", "03" ], [ "апреля", "04" ], [ "мая", "05" ], [ "июня", "06" ],
- [ "июля", "07" ], [ "августа", "08" ], [ "сентября", "09" ], [ "октября", "10" ], [ "ноября", "11" ], [ "декабря", "12" ]
- ]);
- doc.bookDate = (() => {
- return chapters.reduce((result, chapter) => {
- const rr = /^(\d+)\s+([^ ]+)\s+(\d+)\s+г\.\s+в\s+(\d+:\d+)$/.exec(chapter.updated);
- if (rr) {
- const m = months.get(rr[2]);
- const d = (rr[1].length === 1 ? "0" : "") + rr[1];
- const ts = new Date(`${rr[3]}-${m}-${d}T${rr[4]}`);
- if (ts instanceof Date && !isNaN(ts.valueOf())) {
- if (!result || result < ts) result = ts;
- }
- }
- return result;
- }, null);
- })();
- logUpdate.text(doc.bookDate && doc.bookDate.toLocaleString() || "n/a");
- // Ссылка на источник
- doc.sourceURL = url.toString();
- //--
- logChapters.text(chapters.length);
- if (!chapters.length) throw new Error("Нет глав для выгрузки!");
- doc.element = bookEl;
- doc.chapters = chapters;
- }
- function getChaptersList(bookEl) {
- return Array.from(bookEl.querySelectorAll("ul.list-of-fanfic-parts>li.part")).reduce((list, el) => {
- const aEl = el.querySelector("a.part-link");
- const rr = /^\/readfic\/[^\/]+\/(\d+)/.exec(aEl.getAttribute("href"));
- if (rr) {
- const tEl = el.querySelector(".part-title");
- const dEl = el.querySelector(".part-info>span[title]");
- const chapter = {
- id: rr[1],
- title: tEl && tEl.textContent.trim() || "Без названия",
- updated: dEl && dEl.title.trim() || null
- };
- list.push(chapter);
- }
- return list;
- }, []);
- }
- async function getBookContent(doc, log) {
- const bookEl = doc.element;
- delete doc.element;
- let li = null;
- try {
- // Загрузка обложки
- doc.coverpage = await ( async () => {
- const el = bookEl.querySelector(".fanfic-hat-body fanfic-cover");
- if (el) {
- const url = el.getAttribute("src-desktop") || el.getAttribute("src-original") || el.getAttribute("src-mobile");
- if (url) {
- const img = new FB2Image(url);
- let li = log.message("Загрузка обложки...");
- try {
- await img.load((loaded, total) => li.text("" + Math.round(loaded / total * 100) + "%"));
- img.id = "cover" + img.suffix();
- doc.binaries.push(img);
- log.message("Размер обложки:").text(img.size + " байт");
- log.message("Тип обложки:").text(img.type);
- li.ok();
- return img;
- } catch (err) {
- li.fail();
- return false;
- }
- }
- }
- })();
- if (!doc.coverpage) log.warning(doc.coverpage === undefined ? "Обложка не найдена" : "Не удалось загрузить обложку");
- // Аннотация
- const annData = (() => {
- const result = [];
- // Фендом
- const fdEl = bookEl.querySelector(".fanfic-main-info svg.ic_book + a");
- if (fdEl) {
- const text = Array.from(fdEl.parentElement.querySelectorAll("a")).map(el => el.textContent.trim()).join(", ");
- result.push({ index: 1, title: "Фэндом:", element: text, inline: true });
- }
- // Бейджики
- Array.from(bookEl.querySelectorAll("section div .badge-text")).forEach(te => {
- const parent = te.parentElement;
- if (parent.classList.contains("direction")) {
- result.push({ index: 2, title: "Направленность:", element: te.textContent.trim(), inline: true });
- } else if (Array.from(parent.classList).some(c => c.startsWith("badge-rating"))) {
- result.push({ index: 3, title: "Рейтинг:", element: te.textContent.trim(), inline: true });
- } else if (Array.from(parent.classList).some(c => c.startsWith("badge-status"))) {
- result.push({ index: 4, title: "Статус:", element: te.textContent.trim(), inline: true });
- }
- });
- // Рейтинг
- // Статус
- const descrMap = new Map([
- [ "автор оригинала:", { index: 5, selector: "a", inline: true } ],
- [ "оригинал:", { index: 6, inline: true } ],
- [ "пэйринг и персонажи:", { index: 7, selector: "a", inline: true } ],
- [ "размер:", { index: 8, inline: true } ],
- [ "метки:", { index: 9, selector: "a:not(.hidden)", inline: true } ],
- [ "описание:", { index: 10, inline: false } ],
- [ "примечания:", { index: 11, inline: false } ]
- ]);
- return Array.from(bookEl.querySelectorAll(".description strong")).reduce((list, strongEl) => {
- const title = strongEl.textContent.trim();
- const md = descrMap.get(title.toLowerCase());
- if (md && strongEl.nextElementSibling) {
- let element = null;
- if (md.selector) {
- element = strongEl.ownerDocument.createElement("span");
- element.textContent = Array.from(
- strongEl.nextElementSibling.querySelectorAll(md.selector)
- ).map(el => el.textContent).join(", ");
- } else {
- element = strongEl.nextElementSibling;
- }
- list.push({ index: md.index, title: title, element: element, inline: md.inline });
- }
- return list;
- }, result);
- })();
- if (annData.length) {
- li = log.message("Формирование аннотации...");
- doc.bindParser("ann", new AnnotationParser());
- annData.sort((a, b) => (a.index - b.index));
- annData.forEach(it => {
- if (doc.annotation) {
- if (!it.inline) doc.annotation.children.push(new FB2EmptyLine());
- } else {
- doc.annotation = new FB2Annotation();
- }
- let par = new FB2Paragraph();
- par.children.push(new FB2Element("strong", it.title));
- doc.annotation.children.push(par);
- if (it.inline) {
- par.children.push(new FB2Text(" " +(typeof(it.element) === "string" ? it.element : it.element.textContent).trim()));
- } else {
- doc.parse("ann", log, it.element);
- }
- });
- doc.bindParser("ann", null);
- li.ok();
- } else {
- log.warning("Аннотация не найдена");
- }
- log.message("---");
- // Получение и формирование глав
- doc.bindParser("chp", new ChapterParser());
- const chapters = doc.chapters;
- doc.chapters = [];
- let chIdx = 0;
- let chCnt = chapters.length;
- while (chIdx < chCnt) {
- const chItem = chapters[chIdx];
- li = log.message(`Получение главы ${chIdx + 1}/${chCnt}...`);
- try {
- let chData = chItem.data;
- if (!chData) {
- const url = new URL(`/readfic/${encodeURIComponent(doc.id)}/${encodeURIComponent(chItem.id)}`, document.location);
- await sleep(100);
- chData = getChapterData(await Loader.addJob(url));
- }
- // Преобразование в FB2
- doc.parse("chp", log, genChapterElement(chData), chItem.title, chData.notes);
- li.ok();
- li = null;
- ++chIdx;
- } catch (err) {
- if (err instanceof HttpError && err.code === 429) {
- li.fail();
- log.warning("Ответ сервера: слишком много запросов");
- log.message("Ждем 30 секунд");
- await sleep(30000);
- } else {
- throw err;
- }
- }
- }
- doc.bindParser("chp", null);
- //--
- doc.history.push("v1.0 - создание fb2 - (Ox90)");
- if (doc.unknowns) {
- log.message("---");
- log.warning(`Найдены неизвестные элементы: ${doc.unknowns}`);
- log.message("Преобразованы в текст без форматирования");
- }
- log.message("---");
- log.message("Готово!");
- } catch (err) {
- li && li.fail();
- doc.bindParser();
- throw err;
- }
- }
- function genChapterElement(chData) {
- const chapterEl = document.createElement("div");
- const parts = [];
- [ "topComment", "content", "bottomComment" ].reduce((list, it) => {
- if (chData[it]) list.push(chData[it]);
- return list;
- }, []).forEach((partEl, idx) => {
- if (idx) chapterEl.append("\n\n----------\n\n");
- if (partEl.id !== "content") {
- const titleEl = document.createElement("strong");
- titleEl.textContent = "Примечания:";
- chapterEl.append(titleEl, "\n\n");
- }
- while (partEl.firstChild) chapterEl.append(partEl.firstChild);
- });
- return chapterEl;
- }
- function getChapterData(html) {
- const result = {};
- const doc = typeof(html) === "string" ? (new DOMParser()).parseFromString(html, "text/html") : html;
- // Извлечение элемента с содержанием
- const chapter = doc.querySelector("article #content[itemprop=articleBody]");
- if (!chapter) throw new Error("Ошибка анализа HTML данных главы");
- result.content = chapter;
- // Поиск данных сносок
- const rr = /\s+textFootnotes\s+=\s+({.*\})/.exec(html);
- if (rr) {
- try {
- result.notes = JSON.parse(rr[1]);
- } catch (err) {
- throw new Error("Ошибка анализа данных заметок");
- }
- }
- // Примечания автора к главе
- [ [ "topComment", ".part-comment-top>strong + div" ], [ "bottomComment", ".part-comment-bottom>strong + div" ] ].forEach(it => {
- const commentEl = chapter.parentElement.querySelector(it[1]);
- if (commentEl) result[it[0]] = commentEl;
- });
- //--
- return result;
- }
- function genBookFileName(doc) {
- function xtrim(s) {
- const r = /^[\s=\-_.,;!]*(.+?)[\s=\-_.,;!]*$/.exec(s);
- return r && r[1] || s;
- }
- const fn_template = Settings.get("filename", true).trim();
- const ndata = new Map();
- // Автор [\a]
- const author = doc.bookAuthors[0];
- if (author) {
- const author_names = [ author.firstName, author.middleName, author.lastName ].reduce((res, nm) => {
- if (nm) res.push(nm);
- return res;
- }, []);
- if (author_names.length) {
- ndata.set("a", author_names.join(" "));
- } else if (author.nickName) {
- ndata.set("a", author.nickName);
- }
- }
- // Название книги [\t]
- ndata.set("t", xtrim(doc.bookTitle));
- // Количество глав [\c]
- ndata.set("c", `${doc.chapters.length}`);
- // Id книги [\i]
- ndata.set("i", doc.id);
- // Окончательное формирование имени файла плюс дополнительные чистки и проверки.
- function replacer(str) {
- let cnt = 0;
- const new_str = str.replace(/\\([atci])/g, (match, ti) => {
- const res = ndata.get(ti);
- if (res === undefined) return "";
- ++cnt;
- return res;
- });
- return { str: new_str, count: cnt };
- }
- function processParts(str, depth) {
- const parts = [];
- const pos = str.indexOf('<');
- if (pos !== 0) {
- parts.push(replacer(pos == -1 ? str : str.slice(0, pos)));
- }
- if (pos !== -1) {
- let i = pos + 1;
- let n = 1;
- for ( ; i < str.length; ++i) {
- const c = str[i];
- if (c == '<') {
- ++n;
- } else if (c == '>') {
- --n;
- if (!n) {
- parts.push(processParts(str.slice(pos + 1, i), depth + 1));
- break;
- }
- }
- }
- if (++i < str.length) parts.push(processParts(str.slice(i), depth));
- }
- const sa = [];
- let cnt = 0
- for (const it of parts) {
- sa.push(it.str);
- cnt += it.count;
- }
- return {
- str: (!depth || cnt) ? sa.join("") : "",
- count: cnt
- };
- }
- const fname = processParts(fn_template, 0).str.replace(/[\0\/\\\"\*\?\<\>\|:]+/g, "");
- return `${fname.substr(0, 250)}.fb2`;
- }
- async function sleep(msecs) {
- return new Promise(resolve => setTimeout(resolve, msecs));
- }
- function decodeHTMLChars(s) {
- const e = document.createElement("div");
- e.innerHTML = s;
- return e.textContent;
- }
- //---------- Классы ----------
- class DocumentEx extends FB2Document {
- constructor() {
- super();
- this.unknowns = 0;
- }
- parse(parserId, log, ...args) {
- const pdata = super.parse(parserId, ...args);
- pdata.unknownNodes.forEach(el => {
- log.warning(`Найден неизвестный элемент: ${el.nodeName}`);
- ++this.unknowns;
- });
- return pdata.result;
- }
- }
- class TextParser extends FB2Parser {
- run(doc, htmlNode) {
- this._unknownNodes = [];
- const res = super.run(doc, htmlNode);
- const pdata = { result: res, unknownNodes: this._unknownNodes };
- delete this._unknowNodes;
- return pdata;
- }
- /**
- * Текст глав на сайте оформляется довольно странно. Фактически это plain text
- * с нерегулярными вкраплениями разметки. Тег <p> используется, но в основном как
- * контейнер для выравнивания строк текста и подзаголовков.
- * ---
- * Перед парсингом блоки текста упаковываются в параграфы, разделитель - символ новой строки
- * Все пустые строки заменяются на 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 (doc.annotation) {
- this._annotation.children.forEach(el => doc.annotation.children.push(el));
- } else {
- 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(decodeHTMLChars(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 = "<span aria-hidden=\"true\">×</span>";
- 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");
- bdyEl.append(tlEl, container);
- this.log = document.createElement("div");
- const stBtn = document.createElement("p");
- stBtn.style.cursor = "pointer";
- stBtn.style.textDecoration = "underline";
- stBtn.style.margin = "-.5em 0 0";
- stBtn.style.fontSize = "85%";
- stBtn.style.opacity = ".7";
- stBtn.textContent = "Настройки";
- const stForm = document.createElement("div");
- stForm.style.display = "none";
- stForm.style.padding = ".5em";
- stForm.style.margin = ".75em 0";
- stForm.style.border = "1px solid lightgray";
- stForm.style.borderRadius = "5px";
- stForm.innerHTML = '<div><label>Шаблон имени файла (без расширения)</label>' +
- '<input type="text" style="width:100%; background-color:transparent; border:1px solid gray; border-radius:3px; font-size:90%">' +
- '<ul style="color:gray; font-size:85%; margin:0; padding-left:1em;">' +
- '<li>\\a - Автор книги;</li><li>\\t - Название книги;</li><li>\\i - Идентификатор книги;</li><li>\\c - Количество глав;</li>' +
- '<li><…> - Если внутри такого блока будут отсутвовать данные для шаблона, то весь блок будет удален;</li>' +
- '</ul><div style="color:gray; font-size:85%;">' +
- '<span style="color:red; font-weight:bold;">!</span> Оставьте это поле пустым, если хотите вернуть шаблон по умолчанию.</div>';
- stBtn.addEventListener("click", event => {
- if (stForm.style.display) {
- stForm.querySelector("input").value = Settings.get("filename");
- stForm.style.removeProperty("display");
- } else {
- stForm.style.display = "none";
- Settings.set("filename", stForm.querySelector("input").value);
- Settings.save();
- }
- });
- 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, stBtn, stForm, buttons);
- this._mainEl.append(backEl, dlgEl);
- container.addEventListener("submit", event => {
- event.preventDefault();
- if (!stForm.style.display) stBtn.dispatchEvent(new Event("click"));
- stBtn.remove();
- this._onsubmit && this._onsubmit();
- });
- 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 Settings {
- static get(name, reset) {
- if (reset) Settings._values = null;
- this._ensureValues();
- let val = Settings._values[name];
- switch (name) {
- case "filename":
- if (typeof(val) !== "string" || val.trim() === "") val = "<\\a. >\\t [FBN-\\i]";
- break;
- }
- return val;
- }
- static set(name, value) {
- this._ensureValues();
- this._values[name] = value;
- }
- static save() {
- localStorage.setItem("fbe.settings", JSON.stringify(this._values || {}));
- }
- static _ensureValues() {
- if (this._values) return;
- try {
- this._values = JSON.parse(localStorage.getItem("fbe.settings"));
- } catch (err) {
- this._values = null;
- }
- if (!this._values || typeof(this._values) !== "object") Settings._values = {};
- }
- }
- class HttpError extends Error {
- constructor(message, code) {
- super(message);
- this.name = "HttpError";
- this.code = code;
- }
- }
- class Loader extends FB2Loader {
- static async addJob(url, params) {
- if (url.origin === document.location.origin) {
- return super.addJob(url, params).catch(err => {
- if (err.message.endsWith("(429)")) err = new HttpError(err.message, 429);
- throw err;
- });
- }
- params ||= {};
- params.url = url;
- params.method ||= "GET";
- params.responseType = params.responseType === "binary" ? "blob" : "text";
- if (!this.ctl_list) this.ctl_list = new Set();
- return new Promise((resolve, reject) => {
- let req = null;
- params.onload = r => {
- if (r.status === 200) {
- resolve(r.response);
- } else {
- reject(new HttpError("Сервер вернул ошибку (" + r.status + ")", 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() {
- super.abortAll();
- if (this.ctl_list) {
- this.ctl_list.forEach(ctl => ctl.abort());
- this.ctl_list.clear();
- }
- }
- }
- FB2Image.prototype._load = function(...args) {
- if (!(this.url instanceof URL)) this.url = new URL(this.url);
- return Loader.addJob(...args);
- };
- //-------------------------
- // Запускает скрипт после загрузки страницы сайта
- if (document.readyState === "loading") window.addEventListener("DOMContentLoaded", init);
- else init();
- })();