- // ==UserScript==
- // @name AuthorTodayExtractor
- // @name:ru AuthorTodayExtractor
- // @namespace 90h.yy.zz
- // @version 0.12.2
- // @author Ox90
- // @include https://author.today/*
- // @description The script adds a button to the site to download books in FB2 format
- // @description:ru Скрипт добавляет кнопку для выгрузки книги в формате FB2
- // @grant GM.xmlHttpRequest
- // @grant unsafeWindow
- // @connect *
- // @run-at document-start
- // @license MIT
- // ==/UserScript==
-
- /**
- * Разрешение `@connect *` Необходимо для пользователей tampermonkey, чтобы получить возможность загружать картинки
- * внутри глав со сторонних ресурсов, когда авторы ссылаются в своих главах на сторонние хостинги картинок.
- * Такое хоть и редко, но встречается. Это разрешение прописано, чтобы пользователю отображалась кнопка
- * "Always allow all domains" при подтверждении запроса. Детали:
- * https://www.tampermonkey.net/documentation.php#_connect
- */
-
- (function start() {
- "use strict";
-
- let PROGRAM_NAME = "ATExtractor";
- let PROGRAM_ID = "atextr";
-
- let app = null;
- let button = null;
- let mobile = false;
- let observer = null;
- let modalDialog = null;
-
- /**
- * Начальный запуск скрипта сразу после загрузки страницы сайта
- *
- * @return void
- */
- function init() {
- // Найти и сохранить объект App.
- // Он нужен для получения userId, который используется как часть ключа при расшифровке.
- app = window.app || (unsafeWindow && unsafeWindow.app) || {};
- // Инициировать структуру прерываемых запросов
- afetch.init();
- // Добавить кнопку на панель
- setMainButton();
- // Следить за логотипом сайта для проверки наличия кнопки
- observer.start(function() {
- observer.stop();
- setMainButton();
- observer.start();
- });
- }
-
- /**
- * Находит панель и добавляет туда кнопку, если она отсутствует.
- * Вызывается не только при инициализации скрипта но и при изменениях в DOM-дереве
- *
- * @return void
- */
- function setMainButton() {
- // Найти панель для вставки кнопки
- let a_panel = document.querySelector("div.book-panel div.book-action-panel");
- if (!a_panel) {
- a_panel = document.querySelector("div.work-details div.row div.btn-library-work");
- if (!a_panel) return;
- a_panel = a_panel.parentElement;
- mobile = true;
- }
-
- if (!button) {
- // Создает кнопку и привязывает действие
- button = createButton(mobile);
- let ael = mobile && button || button.children[0];
- ael.addEventListener("click", showChaptersDialog);
- }
-
- if (!a_panel.contains(button)) {
- // Выбрать позицию для кнопки: или после оригинальной, или перед группой кнопок внизу.
- // Если не найти нужную позицию, тогда добавить кнопку последней в панели.
- let sbl = null;
- if (!mobile) {
- sbl = a_panel.querySelector("div.mt-lg>a.btn>i.icon-download");
- sbl && (sbl = sbl.parentElement.parentElement.nextElementSibling);
- } else {
- sbl = a_panel.querySelector("#btn-download");
- sbl && (sbl = sbl.nextElementSibling);
- }
- if (!sbl) {
- if (!mobile) {
- sbl = document.querySelector("div.mt-lg.text-center");
- } else {
- sbl = a_panel.querySelector("a.btn-work-more");
- }
- }
- // Добавить кнопку в документ
- if (sbl) {
- a_panel.insertBefore(button, sbl);
- } else {
- a_panel.appendChild(button);
- }
- }
- }
-
- /**
- * Возвращает список глав из DOM-дерева сайта в формате
- * { title: string, locked: bool, workId: string, chapterId: string }.
- *
- * @return Promise Возвращается промис, который вернет массив объектов с данными о главах
- */
- function getChaptersList(params) {
- let res = [];
- let el_list = document.querySelectorAll(
- mobile &&
- "div.work-table-of-content>ul.list-unstyled>li" ||
- "div.book-tab-content>div#tab-chapters>ul.table-of-content>li"
- );
-
- if (!el_list.length) {
- // Не найдено ни одной главы, возможно это рассказ
- // Запрашивает первую главу чтобы получить объект в исходном коде ответа сервера
- return afetch("/reader/" + params.workId, {
- method: "GET",
- responseType: "text",
- }).catch(function(err) {
- console.error(err);
- throw new Error("Ошибка загрузки метаданных главы");
- }).then(function(r) {
- let meta = /app\.init\("readerIndex",\s*(\{[\s\S]+?\})\s*\)/.exec(r.response); // Ищет строку инициализации с данными главы
- if (!meta) throw new Error("Не найдены метаданные книги в ответе сервера");
- let w_id = /\bworkId\s*:\s*(\d+)/.exec(r.response);
- w_id = w_id && w_id[1] || params.workId;
- let c_ls = /\bchapters\s*:\s*(\[.+\])\s*,?[\n\r]+/.exec(r.response);
- c_ls = c_ls && c_ls[1] || "[]";
- let chapters = (JSON.parse(c_ls) || []).map(function(ch) {
- return { title: ch.title, workId: w_id, chapterId: "" + ch.id };
- });
- let w_fm = /\bworkForm\s*:\s*"(.+)"/.exec(r.response);
- if (w_fm && w_fm[1].toLowerCase() === "story" && chapters.length === 1) {
- chapters[0].title = "";
- }
- chapters[0].locked = false;
- return chapters;
- });
- }
-
- // Анализирует найденные HTML элементы с главами
- for (let i = 0; i < el_list.length; ++i) {
- let el = el_list[i].children[0];
- if (el) {
- let ids = null;
- let title = el.textContent;
- let locked = false;
- if (el.tagName === "A" && el.hasAttribute("href")) {
- ids = /^\/reader\/(\d+)\/(\d+)$/.exec(el.getAttribute("href"));
- } else if (el.tagName === "SPAN") {
- if (el.parentElement.querySelector("i.icon-lock")) {
- locked = true;
- }
- }
- if (title && (ids || locked)) {
- let ch = { title: title, locked: locked };
- if (ids) {
- ch.workId = ids[1];
- ch.chapterId = ids[2];
- }
- res.push(ch);
- }
- }
- }
- return Promise.resolve(res);
- }
-
- /**
- * Запрашивает содержимое главы с сервера
- *
- * @param workId string Id книги
- * @param chapterId string Id главы
- *
- * @return Promise Возвращается промис, который вернет расшифрованную HTML-строку.
- */
- function getChapterContent(workId, chapterId) {
- // Id-ы числовые, отфильтрованы регуляркой, кодировать для запроса не нужно
- return afetch(document.location.origin + "/reader/" + workId + "/chapter?id=" + chapterId, {
- method: "GET",
- headers: { "Content-Type": "application/json; charset=utf-8" },
- responseType: "json",
- }).then(function(result) {
- let readerSecret = result.headers["reader-secret"];
- if (!readerSecret)
- throw new Error("Не найден ключ для расшифровки текста");
- if (!result.response.isSuccessful)
- throw new Error("Сервер ответил: Unsuccessful");
- return decryptText(result.response, readerSecret);
- }).catch(function(err) {
- console.error(err.message);
- throw err;
- });
- }
-
- /**
- * Извлекает доступные данные описания книги из DOM сайта
- *
- * @param params object params Элементы описания книги
- * @param log Element HTML элемент для отображения процесса выгрузки
- *
- * @return Promise Возвращает промис который вернет описание книги в виде объекта
- */
- function extractDescriptionData(params, log) {
- let descr = {};
- let book_panel = params.bookPanel;
- return new Promise(function(resolve, reject) {
- if (!book_panel) throw new Error("Не найдена панель с информацией о книге!");
-
- // Заголовок книги
- if (!params.title) throw new Error("Не найден заголовок книги");
- descr.bookTitle = params.title;
- logMessage(log, "Заголовок: " + params.title);
- // Авторы
- let authors = mobile ?
- book_panel.querySelectorAll("div.card-author>a") :
- book_panel.querySelectorAll("div.book-authors>span[itemprop=author]>a");
- authors = Array.prototype.reduce.call(authors, function(list, el) {
- let au = el.textContent.trim();
- if (au) {
- let ao = {};
- au = au.split(" ");
- switch (au.length) {
- case 1:
- ao = { nickname: au[0] };
- break;
- case 2:
- ao = { firstName: au[0], lastName: au[1] };
- break;
- default:
- ao = { firstName: au[0], middleName: au.slice(1, -1).join(" "), lastName: au[au.length - 1] };
- break;
- }
- let hp = /^\/u\/([^\/]+)\/works$/.exec(el.getAttribute("href"));
- if (hp) ao.homePage = document.location.origin + "/u/" + hp[1];
- list.push(ao);
- }
- return list;
- }, []);
- if (!authors.length) throw new Error("Не найдена информация об авторах");
- descr.authors = authors;
- logMessage(log, "Авторы: " + authors.length);
- // Вытягивает данные о жанрах, если это возможно
- let genres = mobile ?
- book_panel.querySelectorAll("div.work-stats a[href^=\"/work/genre/\"]") :
- book_panel.querySelectorAll("div.book-genres>a[href^=\"/work/genre/\"]");
- genres = Array.prototype.reduce.call(genres, function(list, el) {
- let gen = el.textContent.trim();
- if (gen) list.push(gen);
- return list;
- }, []);
- genres = identifyGenre(genres);
- if (genres.length) {
- descr.genres = genres;
- console.info("Жанры: " + genres.join(", "));
- } else {
- console.warn("Не идентифицирован ни один жанр!");
- }
- logMessage(log, "Жанры: " + genres.length);
- // Ключевые слова
- let tags = mobile ?
- document.querySelectorAll("div.work-details ul.work-tags a[href^=\"/work/tag/\"]") :
- book_panel.querySelectorAll("span.tags a[href^=\"/work/tag/\"]");
- tags = Array.prototype.reduce.call(tags, function(list, el) {
- let tag = el.textContent.trim();
- if (tag) list.push(tag);
- return list;
- }, []);
- if (tags.length) descr.keywords = tags;
- logMessage(log, "Ключевые слова: " + (tags && tags.length || "нет"));
- // Серия
- let seq_el = Array.prototype.find.call(book_panel.querySelectorAll("div>a"), function(el) {
- return /^\/work\/series\/\d+$/.test(el.getAttribute("href"));
- });
- if (seq_el) {
- let name = seq_el.textContent.trim();
- if (name) {
- let seq = { name: name };
- seq_el = seq_el.nextElementSibling;
- if (seq_el && seq_el.tagName === "SPAN") {
- let num = /^#(\d+)$/.exec(seq_el.textContent.trim());
- if (num) seq.number = num[1];
- }
- descr.sequence = seq;
- logMessage(log, "Серия: " + seq.name);
- if (seq.number !== undefined) logMessage(log, "Номер в серии: " + seq.number);
- }
- }
- // Дата книги (Последнее обновление)
- let dt = book_panel.querySelector("span[data-format=calendar-short][data-time]");
- if (dt) {
- dt = new Date(dt.getAttribute("data-time"));
- if (!isNaN(dt.valueOf())) descr.bookDate = { value: dt };
- }
- logMessage(log, "Дата книги: " + (descr.bookDate ? descr.bookDate.value.toISOString() : "n/a"));
- // Ссылка на источник
- descr.srcUrl = document.location.origin + document.location.pathname;
- logMessage(log, "Источник: " + descr.srcUrl);
- // Обложка книги
- let cp_el = mobile ?
- document.querySelector("div.work-cover>a.work-cover-content>img.cover-image") :
- document.querySelector("div.book-cover>a.book-cover-content>img.cover-image");
- if (cp_el) {
- let li = logMessage(log, "Загрузка обложки...");
- loadImage(cp_el.getAttribute("src"), li).then(function(img_data) {
- descr.coverpage = img_data;
- logMessage(log, "Размер обложки: " + img_data.size + " байт");
- logMessage(log, "Тип файла обложки: " + img_data.contentType);
- li.ok();
- resolve(descr);
- }).catch(function(err) {
- li.fail();
- reject(err);
- });
- } else {
- logWarning(log, "Обложка книги не найдена!");
- resolve(descr);
- }
- }).then(function() {
- // Аннотация
- let li = logMessage(log, "Анализ аннотации...");
- let ann_a = [];
- if (params.annotation) ann_a.push(params.annotation);
- if (params.authorNotes) ann_a.push(params.authorNotes);
- if (ann_a.length) {
- let par_el = null;
- let newParagraph = function() {
- if (!par_el || par_el.childNodes.length)
- par_el = document.createElement("p");
- else // Если идут два переноса подряд, то вместо параграфа добавляется empty-line.
- ann_el.insertBefore(document.createElement("br"), par_el);
- ann_el.appendChild(par_el);
- };
- let ann_el = document.createElement("annotation");
- ann_a.forEach(function(el, idx) {
- if (idx) newParagraph(); // Пустая строка между аннотацией и примечаниями автора
- newParagraph();
- el.childNodes.forEach(function(node) {
- switch (node.nodeName) {
- case "BR":
- newParagraph();
- break;
- case "P":
- if (par_el.children.length) newParagraph();
- par_el.appendChild(document.createTextNode(node.textContent.trim()));
- newParagraph();
- break;
- case "#text":
- {
- let text = node.textContent.trim();
- if (text.length)
- par_el.appendChild(document.createTextNode(text));
- }
- break;
- default:
- par_el.appendChild(node.cloneNode(true));
- break;
- }
- });
- });
- li.ok();
- return elementToFragment(ann_el, log);
- }
- logWarning(log, "Нет аннотации!");
- }).then(function(a_fr) {
- if (a_fr) {
- descr.annotation = a_fr;
- }
- return descr;
- });
- }
-
- /**
- * Возвращает объект с предварительными результатами анализа книги
- *
- * @return Object
- */
- function getBookParams() {
- let res = {};
-
- res.bookPanel = document.querySelector("div.book-panel div.book-meta-panel") ||
- document.querySelector("div.work-details div.work-header-content");
-
- res.title = res.bookPanel && (res.bookPanel.querySelector(".book-title") || res.bookPanel.querySelector(".card-title"));
- res.title = res.title ? res.title.textContent.trim() : null;
-
- let wid = /^\/work\/(\d+)$/.exec(document.location.pathname);
- res.workId = wid && wid[1] || null;
-
- let empty = function(el) {
- if (!el) return false;
- // Считается что аннотация есть только в том случае,
- // если имеются непустые текстовые ноды непосредственно в блоке аннотации
- return !Array.prototype.some.call(el.childNodes, function(node) {
- return node.nodeName === "#text" && node.textContent.trim() !== "";
- });
- };
-
- let annotation = mobile ?
- document.querySelector("div.card-content-inner>div.card-description") :
- (res.bookPanel && res.bookPanel.querySelector("#tab-annotation>div.annotation"));
- if (annotation.children.length > 0) {
- let notes = annotation.querySelector(":scope>div.rich-content>p.text-primary.mb0");
- if (notes && !empty(notes.parentElement)) res.authorNotes = notes.parentElement;
- annotation = annotation.querySelector(":scope>div.rich-content");
- if (!empty(annotation) && annotation !== notes) res.annotation = annotation;
- }
-
- let materials = mobile ?
- document.querySelector("#accordion-item-materials>div.accordion-item-content div.picture") :
- res.bookPanel && res.bookPanel.querySelector("div.book-materials div.picture");
- if (materials) {
- res.materials = materials;
- }
-
- return res;
- }
-
- /**
- * Запрашивает выбранные ранее части книги с сервера по переданному в аргументе списку.
- * Главы запрашиваются последовательно, чтобы не удивлять сервер запросами всех глав одновременно.
- * TODO: Может следует добавить случайную задержку в несколько секунд между запросами?
- *
- * @param chapterList Array Массив с описанием глав (id и название)
- * @param log Element HTML-элемент лога.
- * @param params object Параметры формирования глав
- *
- * @return Promise
- */
- function extractChapters(chaptersList, log, params) {
- let chapters = [];
- let _resolve = null;
- let _reject = null;
- let requestsRunner = function(position) {
- let ch_data = chaptersList[position++];
- let li = logMessage(log, "Получение главы " + position + "/" + chaptersList.length + "...");
- getChapterContent(ch_data.workId, ch_data.chapterId).then(function(ch_str) {
- li.ok();
- li = null;
- return parseChapterContent(ch_str, ch_data.title, log, params);
- }).then(function(chapter) {
- normalizeChapterFragment(chapter);
- chapters.push(chapter);
- if (position < chaptersList.length) {
- requestsRunner(position);
- } else {
- _resolve(chapters);
- }
- }).catch(function(err) {
- li && li.fail();
- _reject(err);
- });
- };
-
- return new Promise(function(resolve, reject) {
- _resolve = resolve;
- _reject = reject;
- requestsRunner(0);
- });
- }
-
- /**
- * Просматривает элементы с картинками в дополнительных материалах,
- * затем загружает их по ссылкам и сохраняет в виде массива с описанием, если оно есть.
- *
- * @param materials Element HTML-элемент с дополнительными материалами
- * @param log Element HTML-элемент лога.
- *
- * @return Promise
- */
- function extractMaterials(materials, log) {
- let getMaterial = function(fragment) {
- let li = logMessage(log, "Загрузка изображения...");
-
- return loadImage(fragment.url, li).then(function(img) {
- li.ok();
- fragment.children[0].value = img;
- }).catch(function(err) {
- li.fail();
- }).finally(function() {
- delete fragment.url;
- });
- };
-
- let list = Array.prototype.reduce.call(materials.querySelectorAll("figure"), function(res, el) {
- let link = el.querySelector("a");
- if (link && link.hasAttribute("href")) {
- let fragment = {
- url: link.getAttribute("href"),
- type: "chapter",
- children: [ { type: "image", value: null } ]
- };
- let caption = el.querySelector("figcaption");
- if (caption && caption.textContent !== "") {
- fragment.children.push({
- type: "paragraph",
- children: [ { type: "text", value: caption.textContent } ]
- });
- }
- res.push(fragment);
- }
- return res;
- }, []);
-
- return new Promise(function(resolve, reject) {
- if (!list.length) resolve(null);
- Promise.all(list.map(function(it) {
- return getMaterial(it);
- })).then(function() {
- resolve(list);
- }).catch(function(err) {
- reject(err);
- });
- });
- }
-
- /**
- * Конвертирует HTML-строку в HTMLDocument, запускает анализ и преобразование страницы
- * во внутреннее представление.
- *
- * @param chapter_str string HTML-строка, полученная от сервера
- * @param title string Заголовок главы
- * @param log Element HTML-элемент лога.
- * @param params object Параметры формирования глав
- *
- * @return Promise Да, опять промис
- *
- */
- function parseChapterContent(chapter_str, title, log, params) {
- // Присваивание innerHTML не ипользуется по причине его небезопасности.
- // Вряд ли сервер будет гадить своим пользователям, но лучше перестраховаться.
- let chapter_doc = new DOMParser().parseFromString(chapter_str, "text/html");
- let fragment = {};
- if (title) fragment.children = [ { type: "title", value: title } ];
- return elementToFragment(chapter_doc.body, log, params, fragment);
- }
-
- /**
- * Рекурсивно и асинхронно сканирует переданный элемент со всеми его потомками,
- * возвращая специальную структуру, очищенную от HTML-разметки. Загружает внешние ресурсы,
- * такие как картинки. Возвращаемая структура может использоваться для формирования FB2 документа.
- * Используется для анализа аннотации к книге и для анализа полученных от сервера глав.
- *
- * @param element Element HTML-элемент с текстом, картинками и разметкой
- * @param log Element HTML-элемент лога. Необязательный параметр.
- * @param params object Необязательный параметр. Параметры формирования глав.
- * @param fragment object Необязательный параметр. В него будут записаны результирующие данные
- * Он же будет возвращен в результате промиса. Удобно для предварительного
- * размещения результата во внешнем списке. Если не указан, то будет инициирован
- * пустым объектом.
- * @param depth number Необязательный параметр. Глубина рекурсии. Используется в рекурсивном вызове.
- *
- * @return Promise Функция асинхронная, так что возрващает промис, который вернет заполненный данными объект,
- * который передан в параметре fragment или вновь созданный.
- */
- function elementToFragment(element, log, params, fragment, depth) {
- let markUnknown = function() {
- fragment.type = "unknown";
- fragment.value = element.nodeName + " [" + depth + "] | " + element.textContent.slice(0, 35);
- };
- return new Promise(function(resolve, reject) {
- depth ||= 0;
- fragment ||= {};
- fragment.children ||= [];
- switch (element.nodeName) {
- case "IMG":
- let li = null;
- if (log) li = logMessage(log, "Загрузка изображения...");
- if (params.withoutImages) {
- fragment.type = "emphasis";
- li && li.skipped();
- fragment.value = null;
- fragment.children = [ { type: "text", value: "[* Здесь было изображение *]" } ];
- resolve(fragment);
- return;
- }
- fragment.type = "image";
- loadImage(element.getAttribute("src"), li).then(function(img) {
- li && li.ok();
- fragment.value = img;
- resolve(fragment);
- }).catch(function(err) {
- li && li.fail();
- fragment.value = null;
- resolve(fragment);
- });
- return;
- case "A":
- fragment.type = "text";
- fragment.value = element.textContent;
- resolve(fragment);
- return;
- case "BR":
- fragment.type = "empty";
- resolve(fragment);
- return;
- case "P":
- fragment.type = "paragraph";
- break;
- case "DIV":
- fragment.type = "block";
- break;
- case "BODY":
- fragment.type = "chapter";
- break;
- case "ANNOTATION":
- fragment.type = "annotation";
- break;
- case "STRONG":
- fragment.type = "strong";
- break;
- case "U":
- case "EM":
- fragment.type = "emphasis";
- break;
- case "SPAN":
- fragment.type = "span";
- break;
- case "DEL":
- case "S":
- case "STRIKE":
- fragment.type = "strike";
- break;
- case "BLOCKQUOTE":
- fragment.type = "cite";
- break;
- default:
- logWarning(log, "Найден неизвестный тег: " + element.nodeName);
- markUnknown();
- break;
- }
- // Сканировать вложенные ноды
- let queue = [];
- let nodes = element.childNodes;
- for (let i = 0; i < nodes.length; ++i) {
- let node = nodes[i];
- let child = {};
- switch (node.nodeName) {
- case "#text":
- child.type = "text";
- child.value = node.textContent;
- break;
- case "#comment":
- break;
- default:
- queue.push([ node, child ]);
- break;
- }
- fragment.children.push(child);
- }
- // Запустить асинхронную обработку очереди для вложенных нод
- if (queue.length) {
- Promise.all(queue.map(function(it) {
- return elementToFragment(it[0], log, params, it[1], depth + 1);
- })).then(function() {
- resolve(fragment);
- }).catch(function(err) {
- reject(err);
- });
- } else {
- resolve(fragment);
- }
- });
- }
-
- /**
- * Нормализация уже сгерерированного документа. Например картинки и пустые строки
- * будут вынесены из параграфов на первый уровень, непосредственно в <section>.
- * Также тут будут удалены пустые стилистические блоки, если они есть.
- * Если всплывающий элемент находятся внутри фрагмента с другими данными,
- * такой фрагмент будет разбит на два фрагмента, а всплывающий элемент будет
- * размещен между ними.
- *
- * @param fragment Документ для анализа и исправления
- *
- * @return void
- */
- function normalizeChapterFragment(fragment) {
- let title = null;
- let cloneFragment = function(fr) {
- let new_fr = { type: fr.type };
- fr.children && (new_fr.children = fr.children);
- fr.value && (new_fr.value = fr.value);
- return new_fr;
- };
- let normalizeFragment = function(fr, depth) {
- if (depth === 1 && fr.type === "title") title = fr.value;
- if (fr.children) {
- // Обработать детей текущего фрагмента с заменой новыми
- fr.children = fr.children.reduce(function(new_list, ch) {
- normalizeFragment(ch, depth + 1).forEach(function(fr) {
- new_list.push(fr);
- });
- return new_list;
- }, []);
- // Проверить обновленный список детей фрагмента на необходимость чистки и корректировки
- let l_chtype = 0;
- let l_chlist = null;
- let new_children = fr.children.reduce(function(new_list, ch) {
- let chtype = 1;
- let remove = false;
- let squeeze = false;
- switch (ch.type) {
- case "empty":
- squeeze = true;
- // no break
- case "image":
- if (depth > 0) chtype = 2;
- break;
- case "block":
- if (depth > 0 && fr.type === "block") chtype = 2;
- // no break
- case "text":
- case "cite":
- case "paragraph":
- case "strong":
- case "emphasis":
- case "strike":
- case "span":
- if (!ch.value && (!ch.children || !ch.children.length)) {
- // Удалить пустые элементы разметки
- remove = true;
- console.info(title + " | Удален пустой элемент " + ch.type);
- }
- break;
- default:
- break;
- }
-
- if (ch.type === "paragraph") {
- if ([ "strong", "emphasis", "strike", "span" ].includes(fr.type)) {
- // Параграф внутри inline блока
- chtype = 3;
- }
- } else if (depth === 0) {
- if ([ "strong", "emphasis", "strike", "span" ].includes(ch.type)) {
- // Inline элемент на уровне секции
- chtype = 4;
- }
- }
-
- if (!remove) {
- if (!squeeze || l_chtype !== chtype || l_chlist[l_chlist.length - 1].type !== ch.type) {
- if (l_chtype !== chtype) {
- l_chlist = [];
- new_list.push([ chtype, l_chlist ]);
- }
- l_chlist.push(ch);
- l_chtype = chtype;
- } else {
- console.info(title + " | Удален дублирующийся элемент " + ch.type);
- }
- }
- return new_list;
- }, []);
-
- if (new_children.length === 0) {
- // Детей не осталось, возратить изначальный элемент без детей
- fr.children = [];
- return [ fr ];
- }
-
- // Оборачивание inline элементов в параграф с заменой типа
- let i_cnt = 0;
- new_children.forEach(function(it) {
- if (it[0] === 4) {
- it[0] = 1; // Обычный блок
- it[1] = [ { type: "paragraph", children: it[1] } ]; // Единственный элемент - параграф с inline элементами внутри
- console.info(title + " | Создан параграф для inline элемент" + (it[1].length === 1 && "а" || "ов"));
- ++i_cnt;
- }
- });
- if (i_cnt) {
- let accum = null;
- new_children = new_children.reduce(function(new_list, it) {
- if (it[0] === 1) {
- if (!accum)
- new_list.push([ 1, accum = [] ]);
- it[1].forEach(function(ch) {
- accum.push(ch);
- });
- } else {
- accum = null;
- new_list.push(it);
- }
- return new_list;
- }, []);
- }
-
- let popups = {};
- let pcount = 0;
- let new_fragments = new_children.reduce(function(accum, it) {
- switch (it[0]) {
- case 2:
- // Всплывающие элементы самодостаточны, возвратить как есть
- it[1].forEach(function(it) {
- accum.push(it);
- popups[it.type] = (popups[it.type] || 0) + 1;
- ++pcount;
- });
- break;
- case 3:
- // Параграф вложен в inline элемент. Да, да, такое тоже встречается на AT.
- // Переписывает как параграфы с вложенными inline элементами и с детьми параграфа
- it[1].forEach(function(it) {
- let new_inline = cloneFragment(fr);
- new_inline.children = it.children;
- let new_paragraph = cloneFragment(it);
- new_paragraph.children = [ new_inline ];
- accum.push(new_paragraph);
- });
- console.info(title + " | Рокировка " + fr.type + " <-> paragraph (" + it[1].length + ")");
- break;
- default:
- // Обычный вложенный фрагмент. Пересоздает родителя и помещает в результат
- let f = cloneFragment(fr);
- f.children = it[1];
- accum.push(f);
- break;
- }
- return accum;
- }, []);
- if (pcount) {
- // Отобразить информацию о всплытиях в консоли
- let pl = Object.keys(popups).reduce(function(list, key) {
- list.push(key + " (" + popups[key] + ")");
- return list;
- }, []);
- console.info(title + " | Всплытие для " + pl.join(", "));
- }
- return new_fragments;
- }
- return [ fr ];
- };
- let fragments = normalizeFragment(fragment, 0);
- if (fragments.length === 1) fragment.children = fragments[0].children;
- }
-
- /**
- * Асинхронно загружает изображение с переданного в первом аргументе адреса
- * и сохраняет в возвращаемой структуре в base64 с content-type.
- * Используется для загрузки обложки, изображений внутри глав и доп.материалов.
- *
- * @param url string Адрес картинки, которую требуется загрузить
- * @param li object Запись лога, для отображения прогресса. Необязательный параметр.
- *
- * @return Promise Промис, который вернет структуру с данными изображения.
- */
- function loadImage(url, li) {
- let origin = document.location.origin;
- if (url.startsWith("/")) url = origin + url;
- let result = null;
- return new Promise(function(resolve, reject) {
- afetch(url, {
- method: "GET",
- responseType: "blob",
- }, li).then(function(r) {
- let blob = r.response;
- result = { size: blob.size, contentType: blob.type };
- return new Promise(function(resolve, reject) {
- let reader = new FileReader();
- reader.onloadend = function() { resolve(reader.result); };
- reader.readAsDataURL(blob);
- });
- }).then(function(base64str) {
- result.data = base64str.substr(base64str.indexOf(",") + 1);
- resolve(result);
- }).catch(function(err) {
- console.error(err);
- reject(new Error("Ошибка загрузки изображения " + url));
- });
- });
- }
-
- /**
- * Проверяет картинки внутри глав и предлагает замену, если есть сбойные.
- * Выбрасывает исключение в случае неустранимых проблем.
- *
- * @param book_data object Данные сформированного документа
- *
- * @return void
- */
- function checkBinary(book_data) {
- let confirm_stub = function() {
- if (confirm("Имеются незагруженные изображения. Использовать заглушку?")) return;
- throw new Error("Есть нерешенные проблемы с загрузкой изображений");
- };
-
- for (let i = 0; i < book_data.chapters.length; ++i) {
- let ch = book_data.chapters[i];
- for (let k = 0; k < ch.children.length; ++k) {
- let fr = ch.children[k];
- if (fr.type === "image" && !fr.value) {
- confirm_stub();
- return;
- }
- }
- }
- if (book_data.materials) {
- for (let i = 0; i < book_data.materials.length; ++i) {
- if (!book_data.materials[i].children[0].value) {
- confirm_stub();
- return;
- }
- }
- }
- }
-
- /**
- * Просматривает все картинки в сформированном документе и назначает каждой уникальный id.
- *
- * @param book_data object Данные сформированного документа
- *
- * @return void
- */
- function makeBinaryIds(book_data) {
- let ids_map = {};
- let seq_num = 0;
-
- let setImageId = function(img, def) {
- if (!img.id || ids_map[img.id.toLowerCase()]) {
- let id = def || ("image" + (++seq_num));
- switch (img.contentType) {
- case "image/png":
- id += ".png"
- break;
- case "image/jpeg":
- id += ".jpg"
- break;
- }
- img.id = id;
- }
- ids_map[img.id.toLowerCase()] = true;
- };
-
- if (book_data.descr.coverpage) setImageId(book_data.descr.coverpage, "cover");
-
- book_data.chapters.forEach(function(ch) {
- if (ch.children) {
- ch.children.forEach(function(frl1) {
- if (frl1.type === "image") {
- if (frl1.value)
- setImageId(frl1.value);
- else
- frl1.value = { id: "dummy.png" };
- }
- })
- }
- });
-
- if (book_data.materials) {
- book_data.materials.forEach(function(mt) {
- let fr_im = mt.children[0];
- if (fr_im.value)
- setImageId(fr_im.value);
- else
- fr_im.value = { id: "dummy.png" };
- });
- }
- }
-
- /**
- * Формирует описательную часть книги в виде XML-элемента description
- * и добавляет ее в переданный root элемент fb2 документа
- *
- * @param doc XMLDocument Основной XML-документ
- * @param root Element Основной элемент fb2 документа, в который будет добавлено описание
- * @param descr object Объект данных с описанием книги
- *
- * @return void
- **/
- function documentAddDescription(doc, root, descr) {
- let descr_el = documentElement(doc, "description");
- root.appendChild(descr_el);
-
- let title_info = documentElement(doc, "title-info");
- descr_el.appendChild(title_info);
- // Жанры
- documentElement(doc, title_info, (descr.genres || [ "unrecognised" ]).map(function(g) {
- return documentElement(doc, "genre", g);
- }));
- // Авторы
- documentElement(doc, title_info, (descr.authors || []).map(function(a) {
- let items = [];
- if (a.firstName || !a.nickname) {
- items.push(documentElement(doc, "first-name", a.firstName || "Unknown"));
- }
- if (a.middleName) {
- items.push(documentElement(doc, "middle-name", a.middleName));
- }
- if (a.lastName || !a.nickname) {
- items.push(documentElement(doc, "last-name", a.lastName || ""));
- }
- if (a.nickname) {
- items.push(documentElement(doc, "nickname", a.nickname));
- }
- if (a.homePage) {
- items.push(documentElement(doc, "home-page", a.homePage));
- }
- return documentElement(doc, "author", items);
- }));
- // Название книги
- documentElement(doc, title_info, documentElement(doc, "book-title", descr.bookTitle || "???"));
- // Аннотация
- if (descr.annotation) {
- documentAddContentFragment(doc, descr.annotation, title_info);
- }
- // Ключевые слова
- if (descr.keywords) {
- documentElement(doc, title_info, documentElement(doc, "keywords", descr.keywords.join(", ")));
- }
- // Дата книги
- if (descr.bookDate) {
- let d_el = documentElement(doc, "date", descr.bookDate.text || descr.bookDate.value.toUTCString());
- if (descr.bookDate.value) {
- d_el.setAttribute("value", descr.bookDate.value.toISOString());
- }
- title_info.appendChild(d_el);
- }
- // Обложка
- if (descr.coverpage) {
- let img_el = documentElement(doc, "image");
- img_el.setAttribute("l:href", "#" + descr.coverpage.id);
- documentElement(doc, title_info, documentElement(doc, "coverpage", img_el));
- }
- // Язык книги
- documentElement(doc, title_info, documentElement(doc, "lang", "ru"));
- // Серия, в которую входит книга
- if (descr.sequence) {
- let seq = documentElement(doc, "sequence");
- seq.setAttribute("name", descr.sequence.name);
- if (descr.sequence.number) {
- seq.setAttribute("number", descr.sequence.number);
- }
- title_info.appendChild(seq);
- }
-
- let doc_info = documentElement(doc, "document-info");
- descr_el.appendChild(doc_info);
- // Автор файла-контейнера
- documentElement(doc, doc_info, documentElement(doc, "author", documentElement(doc, "nickname", "Ox90")));
- // Программа, с помощью которой был сгенерен файл
- documentElement(doc, doc_info, documentElement(doc, "program-used", PROGRAM_NAME + " v" + GM_info.script.version));
- // Дата генерации файла
- let file_time = descr.fileTime || new Date();
- let time_el = documentElement(doc, "date", file_time.toUTCString());
- time_el.setAttribute("value", file_time.toISOString());
- doc_info.appendChild(time_el);
- // Ссылка на источник
- let src_url = descr.srcUrl || (document.location.origin + document.location.pathname);
- documentElement(doc, doc_info, documentElement(doc, "src-url", src_url));
- // ID документа. Формирует на основе scrUrl.
- documentElement(doc, doc_info, documentElement(doc, "id", PROGRAM_ID + "_" + stringHash(src_url)));
- // Версия документа
- documentElement(doc, doc_info, documentElement(doc, "version", "1.0"));
- }
-
- /**
- * Формирует дерево XML-элементов по переданному в параметре фрагменту с контентом
- * Обычно фрагметом является аннотация или содержимое главы.
- *
- * @param doc XMLDocument Корневой XML-документ
- * @param fragment object Внутреннее представление данных в будущем fb2 документе
- * @param element Element Родительский элемент, к которому будет добавлено дерево с контентом
- *
- * @return void
- */
- function documentAddContentFragment(doc, fragment, element, depth) {
- let title = null;
- let addContentFragment = function(doc, fragment, element, depth, ptype) {
- let cur_el = element;
- let depthFail = function() {
- throw new Error(
- (title ? "\"" + title + "\"" : "Аннотация") +
- ": \nНеверный уровень вложенности [" + depth + "] для " + fragment.type
- );
- };
- let appendChild = function(name) {
- cur_el = documentElement(doc, name);
- element.appendChild(cur_el);
- };
- switch (fragment.type) {
- case "chapter":
- if (depth) depthFail();
- appendChild("section");
- break;
- case "annotation":
- if (depth) depthFail();
- appendChild("annotation");
- break;
- case "title":
- if (depth !== 1) depthFail();
- title = fragment.value;
- cur_el.appendChild(documentElement(doc, "title", documentElement(doc, "p", fragment.value)));
- break;
- case "paragraph":
- case "block":
- if (depth !== 1 && ptype !== "cite") depthFail();
- appendChild("p");
- break;
- case "strong":
- if (depth <= 1) depthFail();
- appendChild("strong");
- break;
- case "emphasis":
- if (depth <= 1) depthFail();
- appendChild("emphasis");
- break;
- case "strike":
- if (depth <= 1) depthFail();
- appendChild("strikethrough");
- break;
- case "text":
- if (depth <= 1) depthFail();
- cur_el.appendChild(doc.createTextNode(fragment.value));
- break;
- case "span":
- // Как text но с потомками
- if (depth <= 1) depthFail();
- break;
- case "cite":
- if (depth !== 1) depthFail();
- appendChild("cite");
- break;
- case "empty":
- if (depth !== 1) depthFail();
- cur_el.appendChild(documentElement(doc, "empty-line", fragment.value));
- break;
- case "image":
- if (depth !== 1) depthFail();
- {
- let img = documentElement(doc, "image");
- img.setAttribute("l:href", "#" + fragment.value.id);
- cur_el.appendChild(img);
- }
- break;
- case "unknown":
- default:
- throw new Error("Неизвестный тип фрагмента: " + fragment.type + " | " + fragment.value);
- }
- fragment.children && fragment.children.forEach(function(ch_fr) {
- addContentFragment(doc, ch_fr, cur_el, depth + 1, fragment.type);
- });
- };
-
- addContentFragment(doc, fragment, element, 0);
- }
-
- /**
- * Формирует дерево XML-документа по переданному списку глав, элемент body
- *
- * @param doc XMLDocument Корневой XML-документ
- * @param body Element Элемент body fb2 документа
- * @param chapters Array Массив с внутренним представлением глав в виде фрагметов
- *
- * @return void
- */
- function documentAddChapters(doc, body, chapters) {
- chapters.forEach(function(ch) {
- documentAddContentFragment(doc, ch, body);
- });
- }
-
- /**
- * Формирует дерево дополнительных материалов по переданному списку
- *
- * @param doc XMLDocument Корневой XML-документ
- * @param body Element Элемент body fb2 документа
- * @param materials Array Массив с внутренним представлением материалов в виде фрагментов
- *
- * @return void
- */
- function documentAddMaterials(doc, body, materials) {
- let section = documentElement(doc, "section",
- documentElement(doc, "title",
- documentElement(doc, "p", "Дополнительные материалы")
- )
- );
- body.appendChild(section);
- materials.forEach(function(mt) {
- documentAddContentFragment(doc, mt, section);
- });
- }
-
- /**
- * Сканирует элементы книги, ищет картинки, добавляет их как элементы binary,
- * содержащие картинки, в корневой элемент fb2 документа
- *
- * @param doc XMLDocument Корневой XML-документ
- * @param root Element Корневой элемент fb2 документа
- * @param book_data object Данные книги, по которым формируются элементы binary
- *
- * @return void
- */
- function documentAddBinary(doc, root, book_data) {
- let dummy = false;
-
- let makeBinary = function(img) {
- let bin_el = documentElement(doc, "binary");
- root.appendChild(bin_el);
- if (img.data) {
- bin_el.setAttribute("id", img.id);
- bin_el.setAttribute("content-type", img.contentType);
- bin_el.textContent = img.data;
- } else if (!dummy) {
- dummy = true;
- bin_el.setAttribute("id", "dummy.png");
- bin_el.setAttribute("content-type", "image/png");
- bin_el.textContent = getDummyImage();
- }
- };
-
- if (book_data.descr.coverpage) makeBinary(book_data.descr.coverpage);
-
- book_data.chapters.forEach(function(ch) {
- if (ch.children) {
- ch.children.forEach(function(frl1) {
- if (frl1.type === "image") makeBinary(frl1.value);
- })
- }
- });
-
- if (book_data.materials) {
- book_data.materials.forEach(function(mt) {
- makeBinary(mt.children[0].value);
- });
- }
- }
-
- /**
- * Создает или модифицирует элемент документа. При создании используется NS XML-документа
- *
- * @param doc XMLDocument XML документ
- * @param element string|Element Основной элемент. Если передана строка, то это будет tagName для создания элемента
- * @param value Element|array|other Дочерний элемент или массив дочерних элементов, иначе - дочерний TextNode
- *
- * @return Element Основной элемент, переданный в параметре element, или вновь созданный, если была передана строка
- */
- function documentElement(doc, element, value) {
- let el = typeof(element) === "object" ? element : doc.createElementNS(doc.documentElement.namespaceURI, element);
- if (value !== undefined && value !== null) {
- switch (typeof(value)) {
- case "object":
- (Array.isArray(value) ? value : [ value ]).forEach(function(it) {
- el.appendChild(it);
- });
- break;
- default:
- el.appendChild(doc.createTextNode(value));
- break;
- }
- }
- return el;
- }
-
- /**
- * Старт формирования XML-документа по накопленным данным книги
- *
- * @param book_data object Данные книги, по которым формируется итоговый XML-документ
- * @param log Element Html-элемент в который будут писаться сообщения о прогрессе
- *
- * @return string Содержимое XML-документа, в виде строки
- */
- function documentStart(book_data, log) {
- let doc = new DOMParser().parseFromString(
- '<?xml version="1.0" encoding="UTF-8"?><FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0"/>',
- "application/xml"
- );
- let root = doc.documentElement;
- root.setAttribute("xmlns:l", "http://www.w3.org/1999/xlink");
-
- logMessage(log, "---");
-
- let li = null;
- try {
- li = logMessage(log, "Анализ бинарных данных...");
- checkBinary(book_data);
- makeBinaryIds(book_data);
- li.ok();
-
- li = logMessage(log, "Формирование описания...");
- documentAddDescription(doc, root, book_data.descr);
- let body = documentElement(doc, "body");
- let authors = (book_data.descr.authors || []).map(function(author) {
- let aa = [];
- if (author.firstName) aa.push(author.firstName);
- if (author.middleName) aa.push(author.middleName);
- if (author.lastName) aa.push(author.lastName);
- if (author.nickname) aa.push(author.nickname);
- return aa.join(" ");
- });
- let btitle = documentElement(doc, "title");
- if (authors.length) btitle.appendChild(documentElement(doc, "p", authors.join(", ")));
- btitle.appendChild(documentElement(doc, "p", book_data.descr.bookTitle));
- body.appendChild(btitle);
- root.appendChild(body);
- li.ok();
-
- li = logMessage(log, "Формирование глав...");
- documentAddChapters(doc, body, book_data.chapters);
- li.ok();
-
- if (book_data.materials) {
- li = logMessage(log, "Формирование доп.материалов...");
- documentAddMaterials(doc, body, book_data.materials);
- li.ok();
- }
-
- li = logMessage(log, "Формирование бинарных данных...");
- documentAddBinary(doc, root, book_data);
- li.ok();
- } catch (err) {
- li && li.fail();
- throw err;
- }
-
- logMessage(log, "---");
- let data = xmldocToString(doc);
- logMessage(log, "Готово!");
- return data;
- }
-
- /**
- * Создает картинку-заглушку в фомате png и возвращает ее данные в виде строки
- *
- * @return string Base64 строка с данными
- */
- function getDummyImage() {
- let canvas = document.createElement("canvas");
- canvas.setAttribute("width", "300");
- canvas.setAttribute("height", "150");
- if (!canvas.getContext) throw new Error("Ошибка работы с элементом canvas");
- let ctx = canvas.getContext("2d");
- // Фон
- ctx.fillStyle = "White";
- ctx.fillRect(0, 0, 300, 150);
- // Обводка
- ctx.lineWidth = 4;
- ctx.strokeStyle = "Gray";
- ctx.strokeRect(0, 0, 300, 150);
- // Тень
- ctx.shadowOffsetX = 2;
- ctx.shadowOffsetY = 2;
- ctx.shadowBlur = 2;
- ctx.shadowColor = "rgba(0, 0, 0, 0.5)";
- // Крест
- let margin = 25;
- let size = 40;
- ctx.lineWidth = 10;
- ctx.strokeStyle = "Red";
- ctx.moveTo(300 / 2 - size / 2, margin);
- ctx.lineTo(300 / 2 + size / 2, margin + size);
- ctx.stroke();
- ctx.moveTo(300 / 2 + size / 2, margin);
- ctx.lineTo(300 / 2 - size / 2, margin + size);
- ctx.stroke();
- // Текст
- ctx.font = "42px Times New Roman";
- ctx.fillStyle = "Black";
- ctx.textAlign = "center";
- ctx.fillText("No image", 150, 120, 300);
- // Получить данные
- let data_str = canvas.toDataURL("image/png");
- return data_str.substr(data_str.indexOf(",") + 1);
- }
-
- /**
- * Пишет переданную строку в HTML-элемент лога как текст без дополнительных стилей
- *
- * @param log Element HTML-элемент лога
- * @param message string Строка с сообщением
- *
- * @return object Объект для дальнейших манипуляций с записью
- */
- function logMessage(log, message) {
- let block = document.createElement("div");
- block.textContent = message;
- log.appendChild(block);
- log.scrollTop = log.scrollHeight;
- function setSpan(text, color) {
- if (!block.children.length)
- block.appendChild(document.createElement("span"));
- let sp = block.children[0];
- sp.style.color = color;
- sp.textContent = " " + text;
- };
- return {
- ok: function() { setSpan("ok", "green"); },
- fail: function() { setSpan("ошибка!", "red"); },
- skipped: function() { setSpan("пропущено", "blue"); },
- text: function(s) { setSpan(s, ""); },
- element: function() { return block; },
- };
- }
-
- /**
- * Пишет переданную строку в HTML-элемент лога как текст предупреждения с цветным выделением
- *
- * @param log Element HTML-элемент лога
- * @param message string Строка с сообщением
- *
- * @return Element Элемент с последним сообщением
- */
- function logWarning(log, message) {
- let lo = logMessage(log, message);
- lo.element().setAttribute("style", "color:#a00;");
- return lo;
- }
-
- /**
- * Создает и возвращает элемент кнопки, для начала отображения диалога формирования fb2 документа
- *
- * @return Element HTML-элемент кнопки для добавления на страницу
- */
- function createButton() {
- let ae = document.createElement("a");
- ae.setAttribute("class", "btn btn-default " + (mobile && "btn-download-work" || "btn-block"));
- ae.setAttribute("style", "border-color:green;");
- let ie = document.createElement("i");
- ie.setAttribute("class", "icon-download");
- ae.appendChild(ie);
- ae.appendChild(document.createTextNode(""));
- let btn = ae;
- if (!mobile) {
- btn = document.createElement("div");
- btn.setAttribute("class", "mt-lg");
- btn.appendChild(ae);
- }
- btn.setText = function(text) {
- let el = this.nodeName === "A" ? this : this.querySelector("a");
- el.childNodes[1].textContent = " " + (text || "Скачать FB2");
- };
- btn.setText();
- return btn;
- }
-
- /**
- * Создает и наполняет окно диалога для выбора глав и добавляет обработчики к элементам
- *
- * @return void
- */
- function showChaptersDialog() {
- if (button.disabled) return;
- button.disabled = true;
- button.setText("Анализ...");
-
- let params = getBookParams();
-
- // Создает интерактивные элементы, которые будут отображены в форме диалога
- let form = document.createElement("form");
-
- let fst = document.createElement("fieldset");
- fst.setAttribute("style", "border:1px solid #bbb; border-radius:6px; padding:5px 12px 0 12px;");
- form.appendChild(fst);
- let leg = document.createElement("legend");
- leg.setAttribute("style", "display:inline; width:unset; font-size:100%; margin:0; padding:0 5px; border:none;");
- fst.appendChild(leg);
- leg.appendChild(document.createTextNode("Главы для выгрузки"));
-
- let chs = document.createElement("div");
- chs.setAttribute("style", "overflow:auto; max-height:50vh;");
- fst.appendChild(chs);
-
- let ntp = document.createElement("p");
- ntp.setAttribute("class", "mb");
- chs.appendChild(ntp);
- ntp.appendChild(
- document.createTextNode("Выберите главы для выгрузки. Обратите внимание: выгружены могут быть только доступные вам главы.")
- );
-
- let tbd = document.createElement("div");
- tbd.setAttribute("class", "mt mb");
- tbd.setAttribute("style", "display:flex; padding-top:10px; border-top:1px solid #bbb;");
- fst.appendChild(tbd);
-
- let its = document.createElement("span");
- its.setAttribute("style", "margin:auto 5px auto 0");
- tbd.appendChild(its);
- its.appendChild(document.createTextNode("Выбрано глав: "));
- let selected = document.createElement("strong");
- selected.appendChild(document.createTextNode("0"));
- its.appendChild(selected);
- its.appendChild(document.createTextNode(" из "));
- let total = document.createElement("strong");
- its.appendChild(total);
-
- let tb1 = document.createElement("button");
- tb1.setAttribute("type", "button");
- tb1.setAttribute("title", "Выделить все/ничего");
- tb1.setAttribute("style", "margin-left:auto;");
- tbd.appendChild(tb1);
- let tb1i = document.createElement("i");
- tb1i.setAttribute("class", "icon-check");
- tb1.appendChild(tb1i);
- tb1.appendChild(document.createTextNode(" ?"));
-
- let log = document.createElement("div");
- log.setAttribute("class", "mb");
- log.setAttribute(
- "style",
- "display:none; overflow:auto; height:50vh; min-width:30vw; border:1px solid #bbb; border-radius:6px; padding: 6px;"
- );
- form.appendChild(log);
-
- let nte = createCheckbox("Добавить примечания автора в аннотацию", !!params.authorNotes);
- if (!params.authorNotes) nte.querySelector("input").disabled = true;
- nte.setAttribute("style", "margin-top:" + (mobile && "10px" || "-10px"));
- form.appendChild(nte);
-
- let nie = createCheckbox("Не грузить картинки внутри глав", false);
- nie.setAttribute("style", "margin-top:" + (mobile && "10px" || "-10px"));
- form.appendChild(nie);
-
- let nmt = createCheckbox("Не грузить дополнительные материалы", false);
- if (!params.materials) nmt.querySelector("input").disabled = true;
- nmt.setAttribute("style", "margin-top:" + (mobile && "10px" || "-10px"));
- form.appendChild(nmt);
-
- let sbd = document.createElement("div");
- sbd.setAttribute("class", "mt text-center");
- form.appendChild(sbd);
- let sbt = document.createElement("button");
- sbt.setAttribute("class", "button btn btn-success");
- sbt.setAttribute("type", "submit");
- sbt.appendChild(document.createTextNode("Продолжить"));
- sbd.appendChild(sbt);
-
- let chapters_list = [];
-
- chs.addEventListener("change", function(event) {
- let cnt = chapters_list.reduce(function(cnt, ch) {
- if (!ch.locked && ch.element.children[0].children[0].checked) ++cnt;
- return cnt;
- }, 0);
- selected.textContent = cnt;
- sbt.disabled = !cnt;
- });
-
- tb1.addEventListener("click", function(event) {
- let chf = chapters_list.some(function(ch) { return !ch.locked && !ch.element.children[0].children[0].checked; });
- chapters_list.forEach(function(ch) { ch.element.children[0].children[0].checked = (chf && !ch.locked); });
- chs.dispatchEvent(new Event("change"));
- });
-
- let mode = 0;
- let fb2 = null;
- let link = null;
- form.addEventListener("submit", function(event) {
- event.preventDefault();
-
- if (mode === 1) {
- afetch.abortAll();
- return;
- }
-
- if (mode === 2) {
- if (!link) {
- link = document.createElement("a");
- link.download = "book_" + chapters_list[0].workId + ".fb2";
- link.href = URL.createObjectURL(new Blob([ fb2 ], { type: 'text/plain' }));
- }
- link.click();
- return;
- }
-
- if (mode === -1) {
- modalDialog.hide();
- return;
- }
-
- mode = 1;
- if (!chapters_list.length) {
- alert("Нет глав для выгрузки!");
- return;
- }
-
- fst.style.display = "none";
- nte.style.display = "none";
- nie.style.display = "none";
- nmt.style.display = "none";
- log.style.display = "block";
- sbt.textContent = "Прервать";
-
- let book_data = {};
- if (!nte.querySelector("input").checked) params.authorNotes = null;
- let without_img = nie.querySelector("input").checked;
- if (nmt.querySelector("input").checked) params.materials = null;
- extractDescriptionData(params, log).then(function(descr) {
- book_data.descr = descr;
- logMessage(log, "---");
- return extractChapters(chapters_list.filter(function(ch) {
- return !ch.locked && ch.element.children[0].children[0].checked;
- }).map(function(ch) {
- return { title: ch.title, workId: ch.workId, chapterId: ch.chapterId };
- }), log, { withoutImages: without_img });
- }).then(function(chapters) {
- book_data.chapters = chapters;
- if (params.materials) {
- logMessage(log, "---");
- logMessage(log, "Дополнительные материалы:");
- return extractMaterials(params.materials, log);
- }
- }).then(function(materials) {
- book_data.materials = materials;
- fb2 = documentStart(book_data, log);
- sbt.textContent = "Сохранить в файл";
- mode = 2;
- }).catch(function(err) {
- mode = -1;
- sbt.textContent = "Закрыть";
- console.error(err);
- if (err.name === "AbortError")
- alert("Операция прервана")
- else
- alert(err);
- });
- });
-
- // Получает список глав
- let ch_cnt = 0;
- getChaptersList(params).then(function(list) {
- list.forEach(function(ch) {
- ch.element = createChapterCheckbox(ch);
- chs.appendChild(ch.element);
- ++ch_cnt;
- });
- chapters_list = list;
- chs.dispatchEvent(new Event("change"));
- total.appendChild(document.createTextNode(ch_cnt));
-
- // Отображает модальное диалоговое окно
- modalDialog.show({
- mobile: mobile,
- title: "Выгрузка книги в FB2",
- body: form,
- onclose: function() {
- fb2 = null;
- if (link) {
- URL.revokeObjectURL(link.href);
- link = null;
- }
- if (mode === 1) afetch.abortAll();
- },
- });
- }).catch(function(err) {
- console.error(err);
- alert(err);
- }).finally(function() {
- button.disabled = false;
- button.setText();
- });
- }
-
- /**
- * Создает единичный элемент типа checkbox в стиле сайта
- *
- * @param title string Подпись для checkbox
- * @param checked bool Начальное состояние checkbox
- *
- * @return Element HTML-элемент для последующего добавления на форму
- */
- function createCheckbox(title, checked) {
- let root = document.createElement("div");
- root.setAttribute("class", "checkbox c-checkbox no-fastclick mb");
- let label = document.createElement("label");
- root.appendChild(label);
- let input = document.createElement("input");
- input.setAttribute("type", "checkbox");
- label.appendChild(input);
- let span = document.createElement("span");
- span.setAttribute("class", "icon-check-bold");
- label.appendChild(span);
- label.appendChild(document.createTextNode(title));
- if (checked) {
- input.setAttribute("checked", "checked");
- }
- return root;
- }
-
- /**
- * Создает checkbox для диалога выбора главы
- *
- * @param chapter object Данные главы
- *
- * @return Element HTML-элемент для последующего добавления на форму
- */
- function createChapterCheckbox(chapter) {
- let root = createCheckbox(chapter.title || "Без названия", !chapter.locked);
- if (chapter.locked) {
- root.querySelector("input").disabled = true;
- let lock = document.createElement("i");
- lock.setAttribute("class", "icon-lock text-muted ml-sm");
- root.children[0].appendChild(lock);
- }
- if (!chapter.title) root.style.fontStyle = "italic";
- return root;
- }
-
- /**
- * Создает диалоговое окно и управляет им.
- * При каждом вызове метода show окно создается заново.
- * Singleton.
- */
- modalDialog = {
- element: null,
- onclose: null,
- mobile: false,
-
- show: function(params) {
- if (params.mobile) {
- this.mobile = true;
- this._show_m(params);
- return;
- }
-
- this.element = document.createElement("div");
- this.element.setAttribute("class", "modal fade in");
- this.element.setAttribute("tabindex", "-1");
- this.element.setAttribute("role", "dialog");
- this.element.setAttribute("style", "display:block; padding-right:12px;");
- let dlg = document.createElement("div");
- dlg.setAttribute("class", "modal-dialog");
- dlg.setAttribute("role", "document");
- this.element.appendChild(dlg);
- let ctn = document.createElement("div");
- ctn.setAttribute("class", "modal-content");
- dlg.appendChild(ctn);
- let hdr = document.createElement("div");
- hdr.setAttribute("class", "modal-header");
- ctn.appendChild(hdr);
- let hbt = document.createElement("button");
- hbt.setAttribute("class", "close");
- hbt.setAttribute("type", "button");
- hdr.appendChild(hbt);
- let sbt = document.createElement("span");
- hbt.appendChild(sbt);
- sbt.appendChild(document.createTextNode("×"));
- let htl = document.createElement("h4");
- htl.setAttribute("class", "modal-title");
- hdr.appendChild(htl);
- htl.appendChild(document.createTextNode(params.title));
-
- let bdy = document.createElement("div");
- bdy.setAttribute("class", "modal-body");
- bdy.setAttribute("style", "color:#656565; min-width:250px; max-width:max(500px,35vw);");
- ctn.appendChild(bdy);
- bdy.appendChild(params.body);
-
- document.body.appendChild(this.element);
-
- this.backdrop = document.createElement("div");
- this.backdrop.setAttribute("class", "modal-backdrop fade in");
- document.body.appendChild(this.backdrop);
-
- document.body.classList.add("modal-open");
-
- this.onclose = params.onclose || null;
-
- this.element.addEventListener("click", function(event) {
- if (event.target === this.element || event.target.closest("button.close")) {
- this.hide();
- }
- }.bind(this));
- this.element.addEventListener("keydown", function(event) {
- if (event.code == "Escape" && !event.shiftKey && !event.ctrlKey && !event.altKey) {
- this.hide();
- event.preventDefault();
- }
- }.bind(this));
- },
-
- hide: function() {
- if (this.mobile) {
- this._hide_m();
- return;
- }
-
- if (this.element && this.backdrop) {
- this.backdrop.remove();
- this.backdrop = null;
- this.element.remove();
- this.element = null;
- document.body.classList.remove("modal-open");
- if (this.onclose) this.onclose();
- this.onclose = null;
- }
- },
-
- _show_m: function(params) {
- this.element = document.createElement("div");
- this.element.setAttribute("class", "popup popup-screen-content");
- this.element.setAttribute("style", "overflow:hidden;");
- let ctn = document.createElement("div");
- ctn.setAttribute("class", "content-block");
- this.element.appendChild(ctn);
- let htl = document.createElement("h2");
- htl.setAttribute("class", "text-center");
- htl.appendChild(document.createTextNode(params.title));
- ctn.appendChild(htl);
- let bdy = document.createElement("div");
- bdy.setAttribute("class", "modal-body");
- bdy.setAttribute("style", "color:#656565;");
- ctn.appendChild(bdy);
- bdy.appendChild(params.body);
- let cbt = document.createElement("button");
- cbt.setAttribute("class", "mt button btn btn-default");
- cbt.appendChild(document.createTextNode("Закрыть"));
- ctn.appendChild(cbt);
-
- cbt.addEventListener("click", function(event) {
- this.hide();
- }.bind(this));
-
- document.body.appendChild(this.element);
- this.element.style.display = "block";
-
- this.element.classList.add("modal-in");
- this._turnOverlay_m(true);
- },
-
- _hide_m: function() {
- if (this.element) {
- this.element.remove();
- this.element = null;
- if (this.onclose) {
- this.onclose();
- this.onclose = null;
- }
- this._turnOverlay_m(false);
- }
- },
-
- _turnOverlay_m(on) {
- let overlay = document.querySelector("div.popup-overlay");
- if (!overlay && on) {
- overlay = document.createElement("div");
- overlay.setAttribute("class", "popup-overlay");
- document.body.appendChild(overlay);
- }
- if (on) {
- overlay.classList.add("modal-overlay-visible");
- } else if (overlay) {
- overlay.classList.remove("modal-overlay-visible");
- }
- }
- };
-
- /**
- * Обертка для ассинхронных запросов с возможностью отмены всех запросов разом
- *
- * @param url string Адрес запрашиваемого ресурса
- * @param params object Параметры асинхронного запроса
- * @param li object Запись лога, для отображения прогресса. Необязательный параметр.
- *
- * @return Promise Промис, который вернет запрашиваемые данные
- */
- function afetch(url, params, li) {
- params ||= {};
- params.url = url;
- params.method ||= "GET";
- return new Promise(function(resolve, reject) {
- let req = null;
- params.onload = function(r) {
- if (r.status === 200) {
- let headers = {};
- r.responseHeaders.split("\n").forEach(function(hs) {
- let h = hs.split(":");
- if (h[1]) headers[h[0].trim().toLowerCase()] = h[1].trim();
- });
- resolve({ headers: headers, response: r.response });
- } else {
- reject(new Error("Сервер вернул ошибку (" + r.status + ")"));
- }
- };
- params.onerror = function(e) {
- reject(e);
- };
- params.ontimeout = function(e) {
- reject(e);
- };
- params.onloadend = function() {
- req && afetch.ctl_list.delete(req);
- };
- if (li) {
- params.onprogress = function(pe) {
- if (pe.lengthComputable)
- li.text("" + Math.round(pe.loaded / pe.total * 100) + "%");
- };
- }
- try {
- req = GM.xmlHttpRequest(params);
- req && afetch.ctl_list.add(req);
- } catch (e) {
- reject(e);
- }
- });
- }
-
- /**
- * Инициирует структуру обертки
- */
- afetch.init = function() {
- afetch.ctl_list = new Set();
- };
-
- /**
- * Прерывает все выполняющиеся ассинхронные запросы и очищает хранилище контроллеров
- */
- afetch.abortAll = function() {
- afetch.ctl_list.forEach(function(ctl) {
- ctl.abort();
- });
- afetch.ctl_list.clear();
- };
-
- /**
- * Расшифровывает полученную от сервера строку с текстом
- *
- * @param chapter string Зашифованная глава книги, полученная от сервера
- * @param secret string Часть ключа для расшифровки
- *
- * @return string Расшифрованный текст
- */
- function decryptText(chapter, secret) {
- let ss = secret.split("").reverse().join("") + "@_@" + (app.userId || "");
- let slen = ss.length;
- let clen = chapter.data.text.length;
- let result = [];
- for (let pos = 0; pos < clen; ++pos) {
- result.push(String.fromCharCode(chapter.data.text.charCodeAt(pos) ^ ss.charCodeAt(Math.floor(pos % slen))));
- }
- return result.join("");
- }
-
- /**
- * Возвращает текстовое представление XML-дерева элементов
- *
- * @param doc XMLDocument XML-документ
- *
- * @return string XML-документ в виде строки
- */
- function xmldocToString(doc) {
- // TODO! Сделать переносы строк и отступы в итоговом XML-файле.
- return (new XMLSerializer()).serializeToString(doc);
- }
-
- /**
- * Возвращает хэш переданной строки. Используется как часть уникального идентификатора книги
- *
- * @param str string Строка для получения хэша
- *
- * @return string Строковое представление хэша переданной строки
- */
- function stringHash(str) {
- let hash = 0;
- let slen = str.length;
- for (let i = 0; i < slen; ++i) {
- let ch = str.charCodeAt(i);
- hash = ((hash << 5) - hash) + ch;
- hash = hash & hash; // Convert to 32bit integer
- }
- return Math.abs(hash).toString() + (hash > 0 ? "1" : "");
- }
-
- /**
- * Класс для управления наблюдением за логотипом сайта, чтобы отлавливать изменения в странице
- * производимые сайтом через свои скрипты. Там вместо лого отображается картинка часиков.
- */
- observer = {
- _observer: null,
-
- start: function(handler) {
- let logo = document.querySelector("div.brand-logo");
- if (logo) {
- if (!this._observer)
- this._observer = new MutationObserver(function() {
- if (!logo.querySelector("div#nprogress"))
- handler();
- });
- this._observer.observe(logo, { childList: true, subtree: true });
- }
- },
-
- stop: function() {
- if (this._observer)
- this._observer.disconnect();
- }
- };
-
- /**
- * Список фиксированных жанров для FB2.
- * Первый элемент - Точное название жанра
- * Последующие элементы - ключевые слова в нижнем регистре для дополнительной идентификации жанра
- * Список взят отсюда: https://github.com/gribuser/fb2/blob/master/FictionBookGenres.xsd
- */
- let GENRE_MAP = {
- adv_animal: [ "Природа и животные", "приключения", "животные", "природа" ],
- adv_geo: [ "Путешествия и география", "приключения", "география", "путешествие" ],
- adv_history: [ "Исторические приключения", "история", "приключения" ],
- adv_maritime: [ "Морские приключения", "приключения", "море" ],
- //adv_western: [ ], //??
- adventure: [ "Приключения" ],
- antique: [ "Старинное" ],
- antique_ant: [ "Античная литература", "старинное", "античность" ],
- antique_east: [ "Древневосточная литература", "старинное", "восток" ],
- antique_european: [ "Европейская старинная литература", "старинное", "европа" ],
- antique_myths: [ "Мифы. Легенды. Эпос", "мифы", "легенды", "эпос" ],
- antique_russian: [ "Древнерусская литература", "древнерусское" ],
- aphorism_quote: [ "Афоризмы, цитаты" ],
- architecture_book: [ "Скульптура и архитектура", "дизайн" ],
- auto_regulations: [ "Автомобили и ПДД", "дорожного", "движения", "дорожное", "движение" ],
- banking: [ "Финансы", "банки", "деньги" ],
- beginning_authors: [ "Начинающие авторы" ],
- child_adv: [ "Приключения для детей и подростков" ],
- child_det: [ "Детская остросюжетная литература" ],
- child_education: [ "Детская образовательная литература" ],
- child_prose: [ "Проза для детей" ],
- child_sf: [ "Фантастика для детей" ],
- child_tale: [ "Сказки для детей" ],
- child_verse: [ "Стихи для детей" ],
- children: [ "Детское" ],
- cinema_theatre: [ "Кино и театр" ],
- city_fantasy: [ "Городское фэнтези" ],
- comp_db: [ "Компьютерные базы данных" ],
- comp_hard: [ "Компьютерное железо", "аппаратное" ],
- comp_osnet: [ "ОС и копьютерные сети" ],
- comp_programming: [ "Программирование" ],
- comp_soft: [ "Программное обеспечение" ],
- comp_www: [ "Интернет" ],
- computers: [ "Компьютеры" ],
- design: [ "Дизайн" ],
- det_action: [ "Боевики", "боевик" ],
- det_classic: [ "Классический детектив" ],
- det_crime: [ "Криминальный детектив", "криминал" ],
- det_espionage: [ "Шнионский детектив", "шпион", "шпионы" ],
- det_hard: [ "Крутой детектив" ],
- det_history: [ "Исторический детектив", "история" ],
- det_irony: [ "Иронический детектив" ],
- det_police: [ "Полицейский детектив", "полиция" ],
- det_political: [ "Политический детектив", "политика" ],
- detective: [ "Детективы", "детектив" ],
- dragon_fantasy: [ "Фэнтези с драконами", "драконы", "дракон" ],
- dramaturgy: [ "Драматургия" ],
- economics: [ "Экономика" ],
- essays: [ "Эссэ" ],
- fantasy_fight: [ "Боевое фэнези" ],
- foreign_action: [ "Зарубежные боевики", "иностранные" ],
- foreign_adventure: [ "Зарубежная приключенческая литература", "иностранная", "приключения" ],
- foreign_antique: [ "Средневековая классическая проза" ],
- foreign_business: [ "Зарубежная карьера и бизнес", "иностранная" ],
- foreign_children: [ "Зарубежная литература для детей" ],
- foreign_comp: [ "Зарубежная компьютерная литература" ],
- foreign_contemporary: [ "Зарубежная современная литература" ],
- //foreign_contemporary_lit: [ ], //??
- //foreign_desc: [ ], //??
- foreign_detective: [ "Зарубежные детективы", "иностранные", "зарубежный", "детектив" ],
- foreign_dramaturgy: [ "Зарубежная драматургия" ],
- foreign_edu: [ "Зарубежная образовательная литература", "иностранная" ],
- foreign_fantasy: [ "Зарубежное фэнтези", "иностранное", "иностранная", "зарубежная", "фантастика" ],
- foreign_home: [ "Зарубежное домоводство", "иностранное" ],
- foreign_humor: [ "Зарубежная юмористическая литература", "иностранная" ],
- foreign_language: [ "Иностранные языки" ],
- foreign_love: [ "Зарубежная любовная литература", "иностранная" ],
- foreign_novel: [ "Зарубежные романы", "иностранные" ],
- foreign_other: [ "Другая зарубежная литература", "иностранная" ],
- foreign_poetry: [ "Зарубежная поэзия", "иностранная", "зарубежные", "стихи" ],
- foreign_prose: [ "Зарубежная классическая проза", "иностранная", "проза" ],
- foreign_psychology: [ "Зарубежная литература о прихологии", "иностранная" ],
- foreign_publicism: [ "Зарубежная публицистика", "иностранная", "документальная" ],
- foreign_religion: [ "Зарубежная религия", "иностранная" ],
- foreign_sf: [ "Зарубежная научная фантастика", "иностранная" ],
- geo_guides: [ "Путеводители, карты, атласы", "география" ],
- geography_book: [ "Путешествия и география" ],
- global_economy: [ "Глобальная экономика" ],
- historical_fantasy: [ "Историческое фэнтези" ],
- home: [ "Домоводство", "дом", "семья" ],
- home_cooking: [ "Кулинария" ],
- home_crafts: [ "Хобби и ремесла" ],
- home_diy: [ "Сделай сам" ],
- home_entertain: [ "Развлечения" ],
- home_garden: [ "Сад и огород" ],
- home_health: [ "Здоровье" ],
- home_pets: [ "Домашние животные" ],
- home_sex: [ "Семейные отношения, секс" ],
- home_sport: [ "Боевые исскусства, спорт" ],
- humor: [ "Юмор" ],
- humor_anecdote: [ "Анекдоты" ],
- humor_fantasy: [ "Юмористическое фэтези","юмористическая", "фантастика" ],
- humor_prose: [ "Юмористическая проза" ],
- humor_verse: [ "Юмористические стихи, басни" ],
- industries: [ "Отрасли", "индустрия" ],
- job_hunting: [ "Поиск работы", "работа" ],
- literature_18: [ "Классическая проза XVII-XVIII веков" ],
- literature_19: [ "Классическая проза ХIX века" ],
- literature_20: [ "Классическая проза ХX века" ],
- love_contemporary: [ "Современные любовные романы" ],
- love_detective: [ "Остросюжетные любовные романы", "детектив", "любовь" ],
- love_erotica: [ "Эротическая литература", "эротика" ],
- love_fantasy: [ "Любовное фэнтези" ],
- love_history: [ "Исторические любовные романы", "история", "любовь" ],
- love_sf: [ "Любовно-фантастические романы" ],
- love_short: [ "Короткие любовные романы" ],
- magician_book: [ "Магия, фокусы" ],
- management: [ "Менеджмент", "управление" ],
- marketing: [ "Маркетинг", "продажи" ],
- military_special: [ "Специальная военная литература" ],
- music_dancing: [ "Музыка и танцы" ],
- narrative: [ "Повествование" ],
- newspapers: [ "Газеты" ],
- nonf_biography: [ "Биографии и Мемуары" ],
- nonf_criticism: [ "Критика" ],
- nonf_publicism: [ "Публицистика" ],
- nonfiction: [ "Документальная литература" ],
- org_behavior: [ "Маркентиг, PR", "организации" ],
- paper_work: [ "Канцелярская работа" ],
- pedagogy_book: [ "Педагогическая литература" ],
- periodic: [ "Журналы, газеты" ],
- personal_finance: [ "Личные финансы" ],
- poetry: [ "Поэзия" ],
- popadanec: [ "Попаданцы", "попаданец" ],
- popular_business: [ "Карьера, кадры", "карьера", "дело", "бизнес" ],
- prose_classic: [ "Классическая проза" ],
- prose_counter: [ "Контркультура" ],
- prose_history: [ "Историческая проза", "история", "проза" ],
- prose_military: [ "Проза о войне" ],
- prose_rus_classic: [ "Русская классическая проза" ],
- prose_su_classics: [ "Советская классическая проза" ],
- psy_classic: [ "Классическая психология" ],
- psy_childs: [ "Детская психология" ],
- psy_generic: [ "Общая психология" ],
- psy_personal: [ "Психология личности" ],
- psy_sex_and_family: [ "Семейная психология", "семья", "секс" ],
- psy_social: [ "Социальная психология" ],
- psy_theraphy: [ "Психотерапия", "психология", "терапия" ],
- //real_estate: [ ], // ??
- ref_dict: [ "Словари", "справочник" ],
- ref_encyc: [ "Энциклопедии", "энциклопедия" ],
- ref_guide: [ "Руководства", "руководство", "справочник" ],
- ref_ref: [ "Справочники", "справочник" ],
- reference: [ "Справочная литература" ],
- religion: [ "Религия" ],
- religion_esoterics: [ "Эзотерическая литература", "эзотерика" ],
- //religion_rel: [ ], // ??
- religion_self: [ "Самосовершенствование" ],
- russian_contemporary: [ "Русская современная литература" ],
- russian_fantasy: [ "Славянское фэнтези" ],
- sci_biology: [ "Биология" ],
- sci_chem: [ "Химия" ],
- sci_culture: [ "Культурология" ],
- sci_history: [ "История" ],
- sci_juris: [ "Юриспруденция" ],
- sci_linguistic: [ "Языкознание", "иностранный", "язык" ],
- sci_math: [ "Математика" ],
- sci_medicine: [ "Медицина" ],
- sci_philosophy: [ "Философия" ],
- sci_phys: [ "Физика" ],
- sci_politics: [ "Политика" ],
- sci_religion: [ "Религиоведение", "религия", "духовность" ],
- sci_tech: [ "Технические науки", "техника" ],
- science: [ "Научная литература", "образование" ],
- sf: [ "Научная фантастика", "наука", "фантастика" ],
- sf_action: [ "Боевая фантастика" ],
- sf_cyberpunk: [ "Киберпанк" ],
- sf_detective: [ "Детективная фантастика", "детектив", "фантастика" ],
- sf_fantasy: [ "Фэнтези" ],
- sf_heroic: [ "Героическая фантастика", "герой" ],
- sf_history: [ "Альтернативная история", "история", "фантастика" ],
- sf_horror: [ "Ужасы" ],
- sf_humor: [ "Юмористическая фантастика", "юмор", "фантастика" ],
- sf_social: [ "Социально-психологическая фантастика", "социум", "психология", "фантастика" ],
- sf_space: [ "Космическая фантастика", "космос", "фантастика" ],
- short_story: [ "Рассказы", "рассказ" ],
- sketch: [ "Отрывок", "зарисовка", "набросок", "очерк" ],
- small_business: [ "Малый бизнес", "бизнес", "карьера" ],
- sociology_book: [ "Обществознание", "социология" ],
- stock: [ "Ценные бумаги" ],
- thriller: [ "Триллер", "триллеры" ],
- upbringing_book: [ "Воспитание" ],
- vampire_book: [ "Вампиры", "вампир" ],
- visual_arts: [ "Изобразительное искусство" ],
- };
-
- /**
- * Преобразование жанров сайта в идентификаторы жанров FB2
- *
- * @param keys Array Массив жанров с сайта
- *
- * @return Array Массив жанров формата FB2
- */
- function identifyGenre(keys) {
- let gmap = {};
- let addWeight = function(name, weight) {
- gmap[name] = (gmap[name] || 0) + weight;
- };
- for (let i = 0; i < keys.length; ++i) {
- let site_key = keys[i].toLowerCase();
- let site_wkeys = site_key.split(/[\s,.;]+/);
- if (site_wkeys.length === 1) site_wkeys = [];
- for (let g_name in GENRE_MAP) {
- let g_values = GENRE_MAP[g_name];
- let g_title = g_values[0].toLowerCase();
- if (site_key === g_title) {
- addWeight(g_name, 3); // Точное совпадение!
- break;
- }
- // Искать каждое слово жанра с сайта отдельно
- let weight = 0;
- if (site_wkeys.indexOf(g_title) !== -1) weight += 2;
- if (site_wkeys.length) {
- for (let k = 1; k < g_values.length; ++k) {
- if (site_wkeys.indexOf(g_values[k]) !== -1) ++weight;
- }
- }
- if (weight >= 2) addWeight(g_name, weight);
- }
- }
-
- let res = Object.keys(gmap).map(function(genre) {
- return [ genre, gmap[genre] ];
- });
- if (!res.length) return [];
- res.sort(function(a, b) { return b[1] < a[1]; });
-
- let cur_w = 0;
- let res_genres = [];
- for (let i = 0; i < res.length; ++i) {
- if (res[i][1] !== cur_w && res_genres.length >= 3) break;
- cur_w = res[i][1];
- res_genres.push(res[i][0]);
- }
- return res_genres;
- }
-
- // Запускает скрипт после загрузки страницы сайта
- if (document.readyState === "loading")
- window.addEventListener("DOMContentLoaded", init);
- else
- init();
- }());
-