AuthorTodayExtractor

The script adds a button to the site to download books in FB2 format

目前为 2022-12-23 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name AuthorTodayExtractor
  3. // @name:ru AuthorTodayExtractor
  4. // @namespace 90h.yy.zz
  5. // @version 0.12.2
  6. // @author Ox90
  7. // @include https://author.today/*
  8. // @description The script adds a button to the site to download books in FB2 format
  9. // @description:ru Скрипт добавляет кнопку для выгрузки книги в формате FB2
  10. // @grant GM.xmlHttpRequest
  11. // @grant unsafeWindow
  12. // @connect *
  13. // @run-at document-start
  14. // @license MIT
  15. // ==/UserScript==
  16.  
  17. /**
  18. * Разрешение `@connect *` Необходимо для пользователей tampermonkey, чтобы получить возможность загружать картинки
  19. * внутри глав со сторонних ресурсов, когда авторы ссылаются в своих главах на сторонние хостинги картинок.
  20. * Такое хоть и редко, но встречается. Это разрешение прописано, чтобы пользователю отображалась кнопка
  21. * "Always allow all domains" при подтверждении запроса. Детали:
  22. * https://www.tampermonkey.net/documentation.php#_connect
  23. */
  24.  
  25. (function start() {
  26. "use strict";
  27.  
  28. let PROGRAM_NAME = "ATExtractor";
  29. let PROGRAM_ID = "atextr";
  30.  
  31. let app = null;
  32. let button = null;
  33. let mobile = false;
  34. let observer = null;
  35. let modalDialog = null;
  36.  
  37. /**
  38. * Начальный запуск скрипта сразу после загрузки страницы сайта
  39. *
  40. * @return void
  41. */
  42. function init() {
  43. // Найти и сохранить объект App.
  44. // Он нужен для получения userId, который используется как часть ключа при расшифровке.
  45. app = window.app || (unsafeWindow && unsafeWindow.app) || {};
  46. // Инициировать структуру прерываемых запросов
  47. afetch.init();
  48. // Добавить кнопку на панель
  49. setMainButton();
  50. // Следить за логотипом сайта для проверки наличия кнопки
  51. observer.start(function() {
  52. observer.stop();
  53. setMainButton();
  54. observer.start();
  55. });
  56. }
  57.  
  58. /**
  59. * Находит панель и добавляет туда кнопку, если она отсутствует.
  60. * Вызывается не только при инициализации скрипта но и при изменениях в DOM-дереве
  61. *
  62. * @return void
  63. */
  64. function setMainButton() {
  65. // Найти панель для вставки кнопки
  66. let a_panel = document.querySelector("div.book-panel div.book-action-panel");
  67. if (!a_panel) {
  68. a_panel = document.querySelector("div.work-details div.row div.btn-library-work");
  69. if (!a_panel) return;
  70. a_panel = a_panel.parentElement;
  71. mobile = true;
  72. }
  73.  
  74. if (!button) {
  75. // Создает кнопку и привязывает действие
  76. button = createButton(mobile);
  77. let ael = mobile && button || button.children[0];
  78. ael.addEventListener("click", showChaptersDialog);
  79. }
  80.  
  81. if (!a_panel.contains(button)) {
  82. // Выбрать позицию для кнопки: или после оригинальной, или перед группой кнопок внизу.
  83. // Если не найти нужную позицию, тогда добавить кнопку последней в панели.
  84. let sbl = null;
  85. if (!mobile) {
  86. sbl = a_panel.querySelector("div.mt-lg>a.btn>i.icon-download");
  87. sbl && (sbl = sbl.parentElement.parentElement.nextElementSibling);
  88. } else {
  89. sbl = a_panel.querySelector("#btn-download");
  90. sbl && (sbl = sbl.nextElementSibling);
  91. }
  92. if (!sbl) {
  93. if (!mobile) {
  94. sbl = document.querySelector("div.mt-lg.text-center");
  95. } else {
  96. sbl = a_panel.querySelector("a.btn-work-more");
  97. }
  98. }
  99. // Добавить кнопку в документ
  100. if (sbl) {
  101. a_panel.insertBefore(button, sbl);
  102. } else {
  103. a_panel.appendChild(button);
  104. }
  105. }
  106. }
  107.  
  108. /**
  109. * Возвращает список глав из DOM-дерева сайта в формате
  110. * { title: string, locked: bool, workId: string, chapterId: string }.
  111. *
  112. * @return Promise Возвращается промис, который вернет массив объектов с данными о главах
  113. */
  114. function getChaptersList(params) {
  115. let res = [];
  116. let el_list = document.querySelectorAll(
  117. mobile &&
  118. "div.work-table-of-content>ul.list-unstyled>li" ||
  119. "div.book-tab-content>div#tab-chapters>ul.table-of-content>li"
  120. );
  121.  
  122. if (!el_list.length) {
  123. // Не найдено ни одной главы, возможно это рассказ
  124. // Запрашивает первую главу чтобы получить объект в исходном коде ответа сервера
  125. return afetch("/reader/" + params.workId, {
  126. method: "GET",
  127. responseType: "text",
  128. }).catch(function(err) {
  129. console.error(err);
  130. throw new Error("Ошибка загрузки метаданных главы");
  131. }).then(function(r) {
  132. let meta = /app\.init\("readerIndex",\s*(\{[\s\S]+?\})\s*\)/.exec(r.response); // Ищет строку инициализации с данными главы
  133. if (!meta) throw new Error("Не найдены метаданные книги в ответе сервера");
  134. let w_id = /\bworkId\s*:\s*(\d+)/.exec(r.response);
  135. w_id = w_id && w_id[1] || params.workId;
  136. let c_ls = /\bchapters\s*:\s*(\[.+\])\s*,?[\n\r]+/.exec(r.response);
  137. c_ls = c_ls && c_ls[1] || "[]";
  138. let chapters = (JSON.parse(c_ls) || []).map(function(ch) {
  139. return { title: ch.title, workId: w_id, chapterId: "" + ch.id };
  140. });
  141. let w_fm = /\bworkForm\s*:\s*"(.+)"/.exec(r.response);
  142. if (w_fm && w_fm[1].toLowerCase() === "story" && chapters.length === 1) {
  143. chapters[0].title = "";
  144. }
  145. chapters[0].locked = false;
  146. return chapters;
  147. });
  148. }
  149.  
  150. // Анализирует найденные HTML элементы с главами
  151. for (let i = 0; i < el_list.length; ++i) {
  152. let el = el_list[i].children[0];
  153. if (el) {
  154. let ids = null;
  155. let title = el.textContent;
  156. let locked = false;
  157. if (el.tagName === "A" && el.hasAttribute("href")) {
  158. ids = /^\/reader\/(\d+)\/(\d+)$/.exec(el.getAttribute("href"));
  159. } else if (el.tagName === "SPAN") {
  160. if (el.parentElement.querySelector("i.icon-lock")) {
  161. locked = true;
  162. }
  163. }
  164. if (title && (ids || locked)) {
  165. let ch = { title: title, locked: locked };
  166. if (ids) {
  167. ch.workId = ids[1];
  168. ch.chapterId = ids[2];
  169. }
  170. res.push(ch);
  171. }
  172. }
  173. }
  174. return Promise.resolve(res);
  175. }
  176.  
  177. /**
  178. * Запрашивает содержимое главы с сервера
  179. *
  180. * @param workId string Id книги
  181. * @param chapterId string Id главы
  182. *
  183. * @return Promise Возвращается промис, который вернет расшифрованную HTML-строку.
  184. */
  185. function getChapterContent(workId, chapterId) {
  186. // Id-ы числовые, отфильтрованы регуляркой, кодировать для запроса не нужно
  187. return afetch(document.location.origin + "/reader/" + workId + "/chapter?id=" + chapterId, {
  188. method: "GET",
  189. headers: { "Content-Type": "application/json; charset=utf-8" },
  190. responseType: "json",
  191. }).then(function(result) {
  192. let readerSecret = result.headers["reader-secret"];
  193. if (!readerSecret)
  194. throw new Error("Не найден ключ для расшифровки текста");
  195. if (!result.response.isSuccessful)
  196. throw new Error("Сервер ответил: Unsuccessful");
  197. return decryptText(result.response, readerSecret);
  198. }).catch(function(err) {
  199. console.error(err.message);
  200. throw err;
  201. });
  202. }
  203.  
  204. /**
  205. * Извлекает доступные данные описания книги из DOM сайта
  206. *
  207. * @param params object params Элементы описания книги
  208. * @param log Element HTML элемент для отображения процесса выгрузки
  209. *
  210. * @return Promise Возвращает промис который вернет описание книги в виде объекта
  211. */
  212. function extractDescriptionData(params, log) {
  213. let descr = {};
  214. let book_panel = params.bookPanel;
  215. return new Promise(function(resolve, reject) {
  216. if (!book_panel) throw new Error("Не найдена панель с информацией о книге!");
  217.  
  218. // Заголовок книги
  219. if (!params.title) throw new Error("Не найден заголовок книги");
  220. descr.bookTitle = params.title;
  221. logMessage(log, "Заголовок: " + params.title);
  222. // Авторы
  223. let authors = mobile ?
  224. book_panel.querySelectorAll("div.card-author>a") :
  225. book_panel.querySelectorAll("div.book-authors>span[itemprop=author]>a");
  226. authors = Array.prototype.reduce.call(authors, function(list, el) {
  227. let au = el.textContent.trim();
  228. if (au) {
  229. let ao = {};
  230. au = au.split(" ");
  231. switch (au.length) {
  232. case 1:
  233. ao = { nickname: au[0] };
  234. break;
  235. case 2:
  236. ao = { firstName: au[0], lastName: au[1] };
  237. break;
  238. default:
  239. ao = { firstName: au[0], middleName: au.slice(1, -1).join(" "), lastName: au[au.length - 1] };
  240. break;
  241. }
  242. let hp = /^\/u\/([^\/]+)\/works$/.exec(el.getAttribute("href"));
  243. if (hp) ao.homePage = document.location.origin + "/u/" + hp[1];
  244. list.push(ao);
  245. }
  246. return list;
  247. }, []);
  248. if (!authors.length) throw new Error("Не найдена информация об авторах");
  249. descr.authors = authors;
  250. logMessage(log, "Авторы: " + authors.length);
  251. // Вытягивает данные о жанрах, если это возможно
  252. let genres = mobile ?
  253. book_panel.querySelectorAll("div.work-stats a[href^=\"/work/genre/\"]") :
  254. book_panel.querySelectorAll("div.book-genres>a[href^=\"/work/genre/\"]");
  255. genres = Array.prototype.reduce.call(genres, function(list, el) {
  256. let gen = el.textContent.trim();
  257. if (gen) list.push(gen);
  258. return list;
  259. }, []);
  260. genres = identifyGenre(genres);
  261. if (genres.length) {
  262. descr.genres = genres;
  263. console.info("Жанры: " + genres.join(", "));
  264. } else {
  265. console.warn("Не идентифицирован ни один жанр!");
  266. }
  267. logMessage(log, "Жанры: " + genres.length);
  268. // Ключевые слова
  269. let tags = mobile ?
  270. document.querySelectorAll("div.work-details ul.work-tags a[href^=\"/work/tag/\"]") :
  271. book_panel.querySelectorAll("span.tags a[href^=\"/work/tag/\"]");
  272. tags = Array.prototype.reduce.call(tags, function(list, el) {
  273. let tag = el.textContent.trim();
  274. if (tag) list.push(tag);
  275. return list;
  276. }, []);
  277. if (tags.length) descr.keywords = tags;
  278. logMessage(log, "Ключевые слова: " + (tags && tags.length || "нет"));
  279. // Серия
  280. let seq_el = Array.prototype.find.call(book_panel.querySelectorAll("div>a"), function(el) {
  281. return /^\/work\/series\/\d+$/.test(el.getAttribute("href"));
  282. });
  283. if (seq_el) {
  284. let name = seq_el.textContent.trim();
  285. if (name) {
  286. let seq = { name: name };
  287. seq_el = seq_el.nextElementSibling;
  288. if (seq_el && seq_el.tagName === "SPAN") {
  289. let num = /^#(\d+)$/.exec(seq_el.textContent.trim());
  290. if (num) seq.number = num[1];
  291. }
  292. descr.sequence = seq;
  293. logMessage(log, "Серия: " + seq.name);
  294. if (seq.number !== undefined) logMessage(log, "Номер в серии: " + seq.number);
  295. }
  296. }
  297. // Дата книги (Последнее обновление)
  298. let dt = book_panel.querySelector("span[data-format=calendar-short][data-time]");
  299. if (dt) {
  300. dt = new Date(dt.getAttribute("data-time"));
  301. if (!isNaN(dt.valueOf())) descr.bookDate = { value: dt };
  302. }
  303. logMessage(log, "Дата книги: " + (descr.bookDate ? descr.bookDate.value.toISOString() : "n/a"));
  304. // Ссылка на источник
  305. descr.srcUrl = document.location.origin + document.location.pathname;
  306. logMessage(log, "Источник: " + descr.srcUrl);
  307. // Обложка книги
  308. let cp_el = mobile ?
  309. document.querySelector("div.work-cover>a.work-cover-content>img.cover-image") :
  310. document.querySelector("div.book-cover>a.book-cover-content>img.cover-image");
  311. if (cp_el) {
  312. let li = logMessage(log, "Загрузка обложки...");
  313. loadImage(cp_el.getAttribute("src"), li).then(function(img_data) {
  314. descr.coverpage = img_data;
  315. logMessage(log, "Размер обложки: " + img_data.size + " байт");
  316. logMessage(log, "Тип файла обложки: " + img_data.contentType);
  317. li.ok();
  318. resolve(descr);
  319. }).catch(function(err) {
  320. li.fail();
  321. reject(err);
  322. });
  323. } else {
  324. logWarning(log, "Обложка книги не найдена!");
  325. resolve(descr);
  326. }
  327. }).then(function() {
  328. // Аннотация
  329. let li = logMessage(log, "Анализ аннотации...");
  330. let ann_a = [];
  331. if (params.annotation) ann_a.push(params.annotation);
  332. if (params.authorNotes) ann_a.push(params.authorNotes);
  333. if (ann_a.length) {
  334. let par_el = null;
  335. let newParagraph = function() {
  336. if (!par_el || par_el.childNodes.length)
  337. par_el = document.createElement("p");
  338. else // Если идут два переноса подряд, то вместо параграфа добавляется empty-line.
  339. ann_el.insertBefore(document.createElement("br"), par_el);
  340. ann_el.appendChild(par_el);
  341. };
  342. let ann_el = document.createElement("annotation");
  343. ann_a.forEach(function(el, idx) {
  344. if (idx) newParagraph(); // Пустая строка между аннотацией и примечаниями автора
  345. newParagraph();
  346. el.childNodes.forEach(function(node) {
  347. switch (node.nodeName) {
  348. case "BR":
  349. newParagraph();
  350. break;
  351. case "P":
  352. if (par_el.children.length) newParagraph();
  353. par_el.appendChild(document.createTextNode(node.textContent.trim()));
  354. newParagraph();
  355. break;
  356. case "#text":
  357. {
  358. let text = node.textContent.trim();
  359. if (text.length)
  360. par_el.appendChild(document.createTextNode(text));
  361. }
  362. break;
  363. default:
  364. par_el.appendChild(node.cloneNode(true));
  365. break;
  366. }
  367. });
  368. });
  369. li.ok();
  370. return elementToFragment(ann_el, log);
  371. }
  372. logWarning(log, "Нет аннотации!");
  373. }).then(function(a_fr) {
  374. if (a_fr) {
  375. descr.annotation = a_fr;
  376. }
  377. return descr;
  378. });
  379. }
  380.  
  381. /**
  382. * Возвращает объект с предварительными результатами анализа книги
  383. *
  384. * @return Object
  385. */
  386. function getBookParams() {
  387. let res = {};
  388.  
  389. res.bookPanel = document.querySelector("div.book-panel div.book-meta-panel") ||
  390. document.querySelector("div.work-details div.work-header-content");
  391.  
  392. res.title = res.bookPanel && (res.bookPanel.querySelector(".book-title") || res.bookPanel.querySelector(".card-title"));
  393. res.title = res.title ? res.title.textContent.trim() : null;
  394.  
  395. let wid = /^\/work\/(\d+)$/.exec(document.location.pathname);
  396. res.workId = wid && wid[1] || null;
  397.  
  398. let empty = function(el) {
  399. if (!el) return false;
  400. // Считается что аннотация есть только в том случае,
  401. // если имеются непустые текстовые ноды непосредственно в блоке аннотации
  402. return !Array.prototype.some.call(el.childNodes, function(node) {
  403. return node.nodeName === "#text" && node.textContent.trim() !== "";
  404. });
  405. };
  406.  
  407. let annotation = mobile ?
  408. document.querySelector("div.card-content-inner>div.card-description") :
  409. (res.bookPanel && res.bookPanel.querySelector("#tab-annotation>div.annotation"));
  410. if (annotation.children.length > 0) {
  411. let notes = annotation.querySelector(":scope>div.rich-content>p.text-primary.mb0");
  412. if (notes && !empty(notes.parentElement)) res.authorNotes = notes.parentElement;
  413. annotation = annotation.querySelector(":scope>div.rich-content");
  414. if (!empty(annotation) && annotation !== notes) res.annotation = annotation;
  415. }
  416.  
  417. let materials = mobile ?
  418. document.querySelector("#accordion-item-materials>div.accordion-item-content div.picture") :
  419. res.bookPanel && res.bookPanel.querySelector("div.book-materials div.picture");
  420. if (materials) {
  421. res.materials = materials;
  422. }
  423.  
  424. return res;
  425. }
  426.  
  427. /**
  428. * Запрашивает выбранные ранее части книги с сервера по переданному в аргументе списку.
  429. * Главы запрашиваются последовательно, чтобы не удивлять сервер запросами всех глав одновременно.
  430. * TODO: Может следует добавить случайную задержку в несколько секунд между запросами?
  431. *
  432. * @param chapterList Array Массив с описанием глав (id и название)
  433. * @param log Element HTML-элемент лога.
  434. * @param params object Параметры формирования глав
  435. *
  436. * @return Promise
  437. */
  438. function extractChapters(chaptersList, log, params) {
  439. let chapters = [];
  440. let _resolve = null;
  441. let _reject = null;
  442. let requestsRunner = function(position) {
  443. let ch_data = chaptersList[position++];
  444. let li = logMessage(log, "Получение главы " + position + "/" + chaptersList.length + "...");
  445. getChapterContent(ch_data.workId, ch_data.chapterId).then(function(ch_str) {
  446. li.ok();
  447. li = null;
  448. return parseChapterContent(ch_str, ch_data.title, log, params);
  449. }).then(function(chapter) {
  450. normalizeChapterFragment(chapter);
  451. chapters.push(chapter);
  452. if (position < chaptersList.length) {
  453. requestsRunner(position);
  454. } else {
  455. _resolve(chapters);
  456. }
  457. }).catch(function(err) {
  458. li && li.fail();
  459. _reject(err);
  460. });
  461. };
  462.  
  463. return new Promise(function(resolve, reject) {
  464. _resolve = resolve;
  465. _reject = reject;
  466. requestsRunner(0);
  467. });
  468. }
  469.  
  470. /**
  471. * Просматривает элементы с картинками в дополнительных материалах,
  472. * затем загружает их по ссылкам и сохраняет в виде массива с описанием, если оно есть.
  473. *
  474. * @param materials Element HTML-элемент с дополнительными материалами
  475. * @param log Element HTML-элемент лога.
  476. *
  477. * @return Promise
  478. */
  479. function extractMaterials(materials, log) {
  480. let getMaterial = function(fragment) {
  481. let li = logMessage(log, "Загрузка изображения...");
  482.  
  483. return loadImage(fragment.url, li).then(function(img) {
  484. li.ok();
  485. fragment.children[0].value = img;
  486. }).catch(function(err) {
  487. li.fail();
  488. }).finally(function() {
  489. delete fragment.url;
  490. });
  491. };
  492.  
  493. let list = Array.prototype.reduce.call(materials.querySelectorAll("figure"), function(res, el) {
  494. let link = el.querySelector("a");
  495. if (link && link.hasAttribute("href")) {
  496. let fragment = {
  497. url: link.getAttribute("href"),
  498. type: "chapter",
  499. children: [ { type: "image", value: null } ]
  500. };
  501. let caption = el.querySelector("figcaption");
  502. if (caption && caption.textContent !== "") {
  503. fragment.children.push({
  504. type: "paragraph",
  505. children: [ { type: "text", value: caption.textContent } ]
  506. });
  507. }
  508. res.push(fragment);
  509. }
  510. return res;
  511. }, []);
  512.  
  513. return new Promise(function(resolve, reject) {
  514. if (!list.length) resolve(null);
  515. Promise.all(list.map(function(it) {
  516. return getMaterial(it);
  517. })).then(function() {
  518. resolve(list);
  519. }).catch(function(err) {
  520. reject(err);
  521. });
  522. });
  523. }
  524.  
  525. /**
  526. * Конвертирует HTML-строку в HTMLDocument, запускает анализ и преобразование страницы
  527. * во внутреннее представление.
  528. *
  529. * @param chapter_str string HTML-строка, полученная от сервера
  530. * @param title string Заголовок главы
  531. * @param log Element HTML-элемент лога.
  532. * @param params object Параметры формирования глав
  533. *
  534. * @return Promise Да, опять промис
  535. *
  536. */
  537. function parseChapterContent(chapter_str, title, log, params) {
  538. // Присваивание innerHTML не ипользуется по причине его небезопасности.
  539. // Вряд ли сервер будет гадить своим пользователям, но лучше перестраховаться.
  540. let chapter_doc = new DOMParser().parseFromString(chapter_str, "text/html");
  541. let fragment = {};
  542. if (title) fragment.children = [ { type: "title", value: title } ];
  543. return elementToFragment(chapter_doc.body, log, params, fragment);
  544. }
  545.  
  546. /**
  547. * Рекурсивно и асинхронно сканирует переданный элемент со всеми его потомками,
  548. * возвращая специальную структуру, очищенную от HTML-разметки. Загружает внешние ресурсы,
  549. * такие как картинки. Возвращаемая структура может использоваться для формирования FB2 документа.
  550. * Используется для анализа аннотации к книге и для анализа полученных от сервера глав.
  551. *
  552. * @param element Element HTML-элемент с текстом, картинками и разметкой
  553. * @param log Element HTML-элемент лога. Необязательный параметр.
  554. * @param params object Необязательный параметр. Параметры формирования глав.
  555. * @param fragment object Необязательный параметр. В него будут записаны результирующие данные
  556. * Он же будет возвращен в результате промиса. Удобно для предварительного
  557. * размещения результата во внешнем списке. Если не указан, то будет инициирован
  558. * пустым объектом.
  559. * @param depth number Необязательный параметр. Глубина рекурсии. Используется в рекурсивном вызове.
  560. *
  561. * @return Promise Функция асинхронная, так что возрващает промис, который вернет заполненный данными объект,
  562. * который передан в параметре fragment или вновь созданный.
  563. */
  564. function elementToFragment(element, log, params, fragment, depth) {
  565. let markUnknown = function() {
  566. fragment.type = "unknown";
  567. fragment.value = element.nodeName + " [" + depth + "] | " + element.textContent.slice(0, 35);
  568. };
  569. return new Promise(function(resolve, reject) {
  570. depth ||= 0;
  571. fragment ||= {};
  572. fragment.children ||= [];
  573. switch (element.nodeName) {
  574. case "IMG":
  575. let li = null;
  576. if (log) li = logMessage(log, "Загрузка изображения...");
  577. if (params.withoutImages) {
  578. fragment.type = "emphasis";
  579. li && li.skipped();
  580. fragment.value = null;
  581. fragment.children = [ { type: "text", value: "[* Здесь было изображение *]" } ];
  582. resolve(fragment);
  583. return;
  584. }
  585. fragment.type = "image";
  586. loadImage(element.getAttribute("src"), li).then(function(img) {
  587. li && li.ok();
  588. fragment.value = img;
  589. resolve(fragment);
  590. }).catch(function(err) {
  591. li && li.fail();
  592. fragment.value = null;
  593. resolve(fragment);
  594. });
  595. return;
  596. case "A":
  597. fragment.type = "text";
  598. fragment.value = element.textContent;
  599. resolve(fragment);
  600. return;
  601. case "BR":
  602. fragment.type = "empty";
  603. resolve(fragment);
  604. return;
  605. case "P":
  606. fragment.type = "paragraph";
  607. break;
  608. case "DIV":
  609. fragment.type = "block";
  610. break;
  611. case "BODY":
  612. fragment.type = "chapter";
  613. break;
  614. case "ANNOTATION":
  615. fragment.type = "annotation";
  616. break;
  617. case "STRONG":
  618. fragment.type = "strong";
  619. break;
  620. case "U":
  621. case "EM":
  622. fragment.type = "emphasis";
  623. break;
  624. case "SPAN":
  625. fragment.type = "span";
  626. break;
  627. case "DEL":
  628. case "S":
  629. case "STRIKE":
  630. fragment.type = "strike";
  631. break;
  632. case "BLOCKQUOTE":
  633. fragment.type = "cite";
  634. break;
  635. default:
  636. logWarning(log, "Найден неизвестный тег: " + element.nodeName);
  637. markUnknown();
  638. break;
  639. }
  640. // Сканировать вложенные ноды
  641. let queue = [];
  642. let nodes = element.childNodes;
  643. for (let i = 0; i < nodes.length; ++i) {
  644. let node = nodes[i];
  645. let child = {};
  646. switch (node.nodeName) {
  647. case "#text":
  648. child.type = "text";
  649. child.value = node.textContent;
  650. break;
  651. case "#comment":
  652. break;
  653. default:
  654. queue.push([ node, child ]);
  655. break;
  656. }
  657. fragment.children.push(child);
  658. }
  659. // Запустить асинхронную обработку очереди для вложенных нод
  660. if (queue.length) {
  661. Promise.all(queue.map(function(it) {
  662. return elementToFragment(it[0], log, params, it[1], depth + 1);
  663. })).then(function() {
  664. resolve(fragment);
  665. }).catch(function(err) {
  666. reject(err);
  667. });
  668. } else {
  669. resolve(fragment);
  670. }
  671. });
  672. }
  673.  
  674. /**
  675. * Нормализация уже сгерерированного документа. Например картинки и пустые строки
  676. * будут вынесены из параграфов на первый уровень, непосредственно в <section>.
  677. * Также тут будут удалены пустые стилистические блоки, если они есть.
  678. * Если всплывающий элемент находятся внутри фрагмента с другими данными,
  679. * такой фрагмент будет разбит на два фрагмента, а всплывающий элемент будет
  680. * размещен между ними.
  681. *
  682. * @param fragment Документ для анализа и исправления
  683. *
  684. * @return void
  685. */
  686. function normalizeChapterFragment(fragment) {
  687. let title = null;
  688. let cloneFragment = function(fr) {
  689. let new_fr = { type: fr.type };
  690. fr.children && (new_fr.children = fr.children);
  691. fr.value && (new_fr.value = fr.value);
  692. return new_fr;
  693. };
  694. let normalizeFragment = function(fr, depth) {
  695. if (depth === 1 && fr.type === "title") title = fr.value;
  696. if (fr.children) {
  697. // Обработать детей текущего фрагмента с заменой новыми
  698. fr.children = fr.children.reduce(function(new_list, ch) {
  699. normalizeFragment(ch, depth + 1).forEach(function(fr) {
  700. new_list.push(fr);
  701. });
  702. return new_list;
  703. }, []);
  704. // Проверить обновленный список детей фрагмента на необходимость чистки и корректировки
  705. let l_chtype = 0;
  706. let l_chlist = null;
  707. let new_children = fr.children.reduce(function(new_list, ch) {
  708. let chtype = 1;
  709. let remove = false;
  710. let squeeze = false;
  711. switch (ch.type) {
  712. case "empty":
  713. squeeze = true;
  714. // no break
  715. case "image":
  716. if (depth > 0) chtype = 2;
  717. break;
  718. case "block":
  719. if (depth > 0 && fr.type === "block") chtype = 2;
  720. // no break
  721. case "text":
  722. case "cite":
  723. case "paragraph":
  724. case "strong":
  725. case "emphasis":
  726. case "strike":
  727. case "span":
  728. if (!ch.value && (!ch.children || !ch.children.length)) {
  729. // Удалить пустые элементы разметки
  730. remove = true;
  731. console.info(title + " | Удален пустой элемент " + ch.type);
  732. }
  733. break;
  734. default:
  735. break;
  736. }
  737.  
  738. if (ch.type === "paragraph") {
  739. if ([ "strong", "emphasis", "strike", "span" ].includes(fr.type)) {
  740. // Параграф внутри inline блока
  741. chtype = 3;
  742. }
  743. } else if (depth === 0) {
  744. if ([ "strong", "emphasis", "strike", "span" ].includes(ch.type)) {
  745. // Inline элемент на уровне секции
  746. chtype = 4;
  747. }
  748. }
  749.  
  750. if (!remove) {
  751. if (!squeeze || l_chtype !== chtype || l_chlist[l_chlist.length - 1].type !== ch.type) {
  752. if (l_chtype !== chtype) {
  753. l_chlist = [];
  754. new_list.push([ chtype, l_chlist ]);
  755. }
  756. l_chlist.push(ch);
  757. l_chtype = chtype;
  758. } else {
  759. console.info(title + " | Удален дублирующийся элемент " + ch.type);
  760. }
  761. }
  762. return new_list;
  763. }, []);
  764.  
  765. if (new_children.length === 0) {
  766. // Детей не осталось, возратить изначальный элемент без детей
  767. fr.children = [];
  768. return [ fr ];
  769. }
  770.  
  771. // Оборачивание inline элементов в параграф с заменой типа
  772. let i_cnt = 0;
  773. new_children.forEach(function(it) {
  774. if (it[0] === 4) {
  775. it[0] = 1; // Обычный блок
  776. it[1] = [ { type: "paragraph", children: it[1] } ]; // Единственный элемент - параграф с inline элементами внутри
  777. console.info(title + " | Создан параграф для inline элемент" + (it[1].length === 1 && "а" || "ов"));
  778. ++i_cnt;
  779. }
  780. });
  781. if (i_cnt) {
  782. let accum = null;
  783. new_children = new_children.reduce(function(new_list, it) {
  784. if (it[0] === 1) {
  785. if (!accum)
  786. new_list.push([ 1, accum = [] ]);
  787. it[1].forEach(function(ch) {
  788. accum.push(ch);
  789. });
  790. } else {
  791. accum = null;
  792. new_list.push(it);
  793. }
  794. return new_list;
  795. }, []);
  796. }
  797.  
  798. let popups = {};
  799. let pcount = 0;
  800. let new_fragments = new_children.reduce(function(accum, it) {
  801. switch (it[0]) {
  802. case 2:
  803. // Всплывающие элементы самодостаточны, возвратить как есть
  804. it[1].forEach(function(it) {
  805. accum.push(it);
  806. popups[it.type] = (popups[it.type] || 0) + 1;
  807. ++pcount;
  808. });
  809. break;
  810. case 3:
  811. // Параграф вложен в inline элемент. Да, да, такое тоже встречается на AT.
  812. // Переписывает как параграфы с вложенными inline элементами и с детьми параграфа
  813. it[1].forEach(function(it) {
  814. let new_inline = cloneFragment(fr);
  815. new_inline.children = it.children;
  816. let new_paragraph = cloneFragment(it);
  817. new_paragraph.children = [ new_inline ];
  818. accum.push(new_paragraph);
  819. });
  820. console.info(title + " | Рокировка " + fr.type + " <-> paragraph (" + it[1].length + ")");
  821. break;
  822. default:
  823. // Обычный вложенный фрагмент. Пересоздает родителя и помещает в результат
  824. let f = cloneFragment(fr);
  825. f.children = it[1];
  826. accum.push(f);
  827. break;
  828. }
  829. return accum;
  830. }, []);
  831. if (pcount) {
  832. // Отобразить информацию о всплытиях в консоли
  833. let pl = Object.keys(popups).reduce(function(list, key) {
  834. list.push(key + " (" + popups[key] + ")");
  835. return list;
  836. }, []);
  837. console.info(title + " | Всплытие для " + pl.join(", "));
  838. }
  839. return new_fragments;
  840. }
  841. return [ fr ];
  842. };
  843. let fragments = normalizeFragment(fragment, 0);
  844. if (fragments.length === 1) fragment.children = fragments[0].children;
  845. }
  846.  
  847. /**
  848. * Асинхронно загружает изображение с переданного в первом аргументе адреса
  849. * и сохраняет в возвращаемой структуре в base64 с content-type.
  850. * Используется для загрузки обложки, изображений внутри глав и доп.материалов.
  851. *
  852. * @param url string Адрес картинки, которую требуется загрузить
  853. * @param li object Запись лога, для отображения прогресса. Необязательный параметр.
  854. *
  855. * @return Promise Промис, который вернет структуру с данными изображения.
  856. */
  857. function loadImage(url, li) {
  858. let origin = document.location.origin;
  859. if (url.startsWith("/")) url = origin + url;
  860. let result = null;
  861. return new Promise(function(resolve, reject) {
  862. afetch(url, {
  863. method: "GET",
  864. responseType: "blob",
  865. }, li).then(function(r) {
  866. let blob = r.response;
  867. result = { size: blob.size, contentType: blob.type };
  868. return new Promise(function(resolve, reject) {
  869. let reader = new FileReader();
  870. reader.onloadend = function() { resolve(reader.result); };
  871. reader.readAsDataURL(blob);
  872. });
  873. }).then(function(base64str) {
  874. result.data = base64str.substr(base64str.indexOf(",") + 1);
  875. resolve(result);
  876. }).catch(function(err) {
  877. console.error(err);
  878. reject(new Error("Ошибка загрузки изображения " + url));
  879. });
  880. });
  881. }
  882.  
  883. /**
  884. * Проверяет картинки внутри глав и предлагает замену, если есть сбойные.
  885. * Выбрасывает исключение в случае неустранимых проблем.
  886. *
  887. * @param book_data object Данные сформированного документа
  888. *
  889. * @return void
  890. */
  891. function checkBinary(book_data) {
  892. let confirm_stub = function() {
  893. if (confirm("Имеются незагруженные изображения. Использовать заглушку?")) return;
  894. throw new Error("Есть нерешенные проблемы с загрузкой изображений");
  895. };
  896.  
  897. for (let i = 0; i < book_data.chapters.length; ++i) {
  898. let ch = book_data.chapters[i];
  899. for (let k = 0; k < ch.children.length; ++k) {
  900. let fr = ch.children[k];
  901. if (fr.type === "image" && !fr.value) {
  902. confirm_stub();
  903. return;
  904. }
  905. }
  906. }
  907. if (book_data.materials) {
  908. for (let i = 0; i < book_data.materials.length; ++i) {
  909. if (!book_data.materials[i].children[0].value) {
  910. confirm_stub();
  911. return;
  912. }
  913. }
  914. }
  915. }
  916.  
  917. /**
  918. * Просматривает все картинки в сформированном документе и назначает каждой уникальный id.
  919. *
  920. * @param book_data object Данные сформированного документа
  921. *
  922. * @return void
  923. */
  924. function makeBinaryIds(book_data) {
  925. let ids_map = {};
  926. let seq_num = 0;
  927.  
  928. let setImageId = function(img, def) {
  929. if (!img.id || ids_map[img.id.toLowerCase()]) {
  930. let id = def || ("image" + (++seq_num));
  931. switch (img.contentType) {
  932. case "image/png":
  933. id += ".png"
  934. break;
  935. case "image/jpeg":
  936. id += ".jpg"
  937. break;
  938. }
  939. img.id = id;
  940. }
  941. ids_map[img.id.toLowerCase()] = true;
  942. };
  943.  
  944. if (book_data.descr.coverpage) setImageId(book_data.descr.coverpage, "cover");
  945.  
  946. book_data.chapters.forEach(function(ch) {
  947. if (ch.children) {
  948. ch.children.forEach(function(frl1) {
  949. if (frl1.type === "image") {
  950. if (frl1.value)
  951. setImageId(frl1.value);
  952. else
  953. frl1.value = { id: "dummy.png" };
  954. }
  955. })
  956. }
  957. });
  958.  
  959. if (book_data.materials) {
  960. book_data.materials.forEach(function(mt) {
  961. let fr_im = mt.children[0];
  962. if (fr_im.value)
  963. setImageId(fr_im.value);
  964. else
  965. fr_im.value = { id: "dummy.png" };
  966. });
  967. }
  968. }
  969.  
  970. /**
  971. * Формирует описательную часть книги в виде XML-элемента description
  972. * и добавляет ее в переданный root элемент fb2 документа
  973. *
  974. * @param doc XMLDocument Основной XML-документ
  975. * @param root Element Основной элемент fb2 документа, в который будет добавлено описание
  976. * @param descr object Объект данных с описанием книги
  977. *
  978. * @return void
  979. **/
  980. function documentAddDescription(doc, root, descr) {
  981. let descr_el = documentElement(doc, "description");
  982. root.appendChild(descr_el);
  983.  
  984. let title_info = documentElement(doc, "title-info");
  985. descr_el.appendChild(title_info);
  986. // Жанры
  987. documentElement(doc, title_info, (descr.genres || [ "unrecognised" ]).map(function(g) {
  988. return documentElement(doc, "genre", g);
  989. }));
  990. // Авторы
  991. documentElement(doc, title_info, (descr.authors || []).map(function(a) {
  992. let items = [];
  993. if (a.firstName || !a.nickname) {
  994. items.push(documentElement(doc, "first-name", a.firstName || "Unknown"));
  995. }
  996. if (a.middleName) {
  997. items.push(documentElement(doc, "middle-name", a.middleName));
  998. }
  999. if (a.lastName || !a.nickname) {
  1000. items.push(documentElement(doc, "last-name", a.lastName || ""));
  1001. }
  1002. if (a.nickname) {
  1003. items.push(documentElement(doc, "nickname", a.nickname));
  1004. }
  1005. if (a.homePage) {
  1006. items.push(documentElement(doc, "home-page", a.homePage));
  1007. }
  1008. return documentElement(doc, "author", items);
  1009. }));
  1010. // Название книги
  1011. documentElement(doc, title_info, documentElement(doc, "book-title", descr.bookTitle || "???"));
  1012. // Аннотация
  1013. if (descr.annotation) {
  1014. documentAddContentFragment(doc, descr.annotation, title_info);
  1015. }
  1016. // Ключевые слова
  1017. if (descr.keywords) {
  1018. documentElement(doc, title_info, documentElement(doc, "keywords", descr.keywords.join(", ")));
  1019. }
  1020. // Дата книги
  1021. if (descr.bookDate) {
  1022. let d_el = documentElement(doc, "date", descr.bookDate.text || descr.bookDate.value.toUTCString());
  1023. if (descr.bookDate.value) {
  1024. d_el.setAttribute("value", descr.bookDate.value.toISOString());
  1025. }
  1026. title_info.appendChild(d_el);
  1027. }
  1028. // Обложка
  1029. if (descr.coverpage) {
  1030. let img_el = documentElement(doc, "image");
  1031. img_el.setAttribute("l:href", "#" + descr.coverpage.id);
  1032. documentElement(doc, title_info, documentElement(doc, "coverpage", img_el));
  1033. }
  1034. // Язык книги
  1035. documentElement(doc, title_info, documentElement(doc, "lang", "ru"));
  1036. // Серия, в которую входит книга
  1037. if (descr.sequence) {
  1038. let seq = documentElement(doc, "sequence");
  1039. seq.setAttribute("name", descr.sequence.name);
  1040. if (descr.sequence.number) {
  1041. seq.setAttribute("number", descr.sequence.number);
  1042. }
  1043. title_info.appendChild(seq);
  1044. }
  1045.  
  1046. let doc_info = documentElement(doc, "document-info");
  1047. descr_el.appendChild(doc_info);
  1048. // Автор файла-контейнера
  1049. documentElement(doc, doc_info, documentElement(doc, "author", documentElement(doc, "nickname", "Ox90")));
  1050. // Программа, с помощью которой был сгенерен файл
  1051. documentElement(doc, doc_info, documentElement(doc, "program-used", PROGRAM_NAME + " v" + GM_info.script.version));
  1052. // Дата генерации файла
  1053. let file_time = descr.fileTime || new Date();
  1054. let time_el = documentElement(doc, "date", file_time.toUTCString());
  1055. time_el.setAttribute("value", file_time.toISOString());
  1056. doc_info.appendChild(time_el);
  1057. // Ссылка на источник
  1058. let src_url = descr.srcUrl || (document.location.origin + document.location.pathname);
  1059. documentElement(doc, doc_info, documentElement(doc, "src-url", src_url));
  1060. // ID документа. Формирует на основе scrUrl.
  1061. documentElement(doc, doc_info, documentElement(doc, "id", PROGRAM_ID + "_" + stringHash(src_url)));
  1062. // Версия документа
  1063. documentElement(doc, doc_info, documentElement(doc, "version", "1.0"));
  1064. }
  1065.  
  1066. /**
  1067. * Формирует дерево XML-элементов по переданному в параметре фрагменту с контентом
  1068. * Обычно фрагметом является аннотация или содержимое главы.
  1069. *
  1070. * @param doc XMLDocument Корневой XML-документ
  1071. * @param fragment object Внутреннее представление данных в будущем fb2 документе
  1072. * @param element Element Родительский элемент, к которому будет добавлено дерево с контентом
  1073. *
  1074. * @return void
  1075. */
  1076. function documentAddContentFragment(doc, fragment, element, depth) {
  1077. let title = null;
  1078. let addContentFragment = function(doc, fragment, element, depth, ptype) {
  1079. let cur_el = element;
  1080. let depthFail = function() {
  1081. throw new Error(
  1082. (title ? "\"" + title + "\"" : "Аннотация") +
  1083. ": \nНеверный уровень вложенности [" + depth + "] для " + fragment.type
  1084. );
  1085. };
  1086. let appendChild = function(name) {
  1087. cur_el = documentElement(doc, name);
  1088. element.appendChild(cur_el);
  1089. };
  1090. switch (fragment.type) {
  1091. case "chapter":
  1092. if (depth) depthFail();
  1093. appendChild("section");
  1094. break;
  1095. case "annotation":
  1096. if (depth) depthFail();
  1097. appendChild("annotation");
  1098. break;
  1099. case "title":
  1100. if (depth !== 1) depthFail();
  1101. title = fragment.value;
  1102. cur_el.appendChild(documentElement(doc, "title", documentElement(doc, "p", fragment.value)));
  1103. break;
  1104. case "paragraph":
  1105. case "block":
  1106. if (depth !== 1 && ptype !== "cite") depthFail();
  1107. appendChild("p");
  1108. break;
  1109. case "strong":
  1110. if (depth <= 1) depthFail();
  1111. appendChild("strong");
  1112. break;
  1113. case "emphasis":
  1114. if (depth <= 1) depthFail();
  1115. appendChild("emphasis");
  1116. break;
  1117. case "strike":
  1118. if (depth <= 1) depthFail();
  1119. appendChild("strikethrough");
  1120. break;
  1121. case "text":
  1122. if (depth <= 1) depthFail();
  1123. cur_el.appendChild(doc.createTextNode(fragment.value));
  1124. break;
  1125. case "span":
  1126. // Как text но с потомками
  1127. if (depth <= 1) depthFail();
  1128. break;
  1129. case "cite":
  1130. if (depth !== 1) depthFail();
  1131. appendChild("cite");
  1132. break;
  1133. case "empty":
  1134. if (depth !== 1) depthFail();
  1135. cur_el.appendChild(documentElement(doc, "empty-line", fragment.value));
  1136. break;
  1137. case "image":
  1138. if (depth !== 1) depthFail();
  1139. {
  1140. let img = documentElement(doc, "image");
  1141. img.setAttribute("l:href", "#" + fragment.value.id);
  1142. cur_el.appendChild(img);
  1143. }
  1144. break;
  1145. case "unknown":
  1146. default:
  1147. throw new Error("Неизвестный тип фрагмента: " + fragment.type + " | " + fragment.value);
  1148. }
  1149. fragment.children && fragment.children.forEach(function(ch_fr) {
  1150. addContentFragment(doc, ch_fr, cur_el, depth + 1, fragment.type);
  1151. });
  1152. };
  1153.  
  1154. addContentFragment(doc, fragment, element, 0);
  1155. }
  1156.  
  1157. /**
  1158. * Формирует дерево XML-документа по переданному списку глав, элемент body
  1159. *
  1160. * @param doc XMLDocument Корневой XML-документ
  1161. * @param body Element Элемент body fb2 документа
  1162. * @param chapters Array Массив с внутренним представлением глав в виде фрагметов
  1163. *
  1164. * @return void
  1165. */
  1166. function documentAddChapters(doc, body, chapters) {
  1167. chapters.forEach(function(ch) {
  1168. documentAddContentFragment(doc, ch, body);
  1169. });
  1170. }
  1171.  
  1172. /**
  1173. * Формирует дерево дополнительных материалов по переданному списку
  1174. *
  1175. * @param doc XMLDocument Корневой XML-документ
  1176. * @param body Element Элемент body fb2 документа
  1177. * @param materials Array Массив с внутренним представлением материалов в виде фрагментов
  1178. *
  1179. * @return void
  1180. */
  1181. function documentAddMaterials(doc, body, materials) {
  1182. let section = documentElement(doc, "section",
  1183. documentElement(doc, "title",
  1184. documentElement(doc, "p", "Дополнительные материалы")
  1185. )
  1186. );
  1187. body.appendChild(section);
  1188. materials.forEach(function(mt) {
  1189. documentAddContentFragment(doc, mt, section);
  1190. });
  1191. }
  1192.  
  1193. /**
  1194. * Сканирует элементы книги, ищет картинки, добавляет их как элементы binary,
  1195. * содержащие картинки, в корневой элемент fb2 документа
  1196. *
  1197. * @param doc XMLDocument Корневой XML-документ
  1198. * @param root Element Корневой элемент fb2 документа
  1199. * @param book_data object Данные книги, по которым формируются элементы binary
  1200. *
  1201. * @return void
  1202. */
  1203. function documentAddBinary(doc, root, book_data) {
  1204. let dummy = false;
  1205.  
  1206. let makeBinary = function(img) {
  1207. let bin_el = documentElement(doc, "binary");
  1208. root.appendChild(bin_el);
  1209. if (img.data) {
  1210. bin_el.setAttribute("id", img.id);
  1211. bin_el.setAttribute("content-type", img.contentType);
  1212. bin_el.textContent = img.data;
  1213. } else if (!dummy) {
  1214. dummy = true;
  1215. bin_el.setAttribute("id", "dummy.png");
  1216. bin_el.setAttribute("content-type", "image/png");
  1217. bin_el.textContent = getDummyImage();
  1218. }
  1219. };
  1220.  
  1221. if (book_data.descr.coverpage) makeBinary(book_data.descr.coverpage);
  1222.  
  1223. book_data.chapters.forEach(function(ch) {
  1224. if (ch.children) {
  1225. ch.children.forEach(function(frl1) {
  1226. if (frl1.type === "image") makeBinary(frl1.value);
  1227. })
  1228. }
  1229. });
  1230.  
  1231. if (book_data.materials) {
  1232. book_data.materials.forEach(function(mt) {
  1233. makeBinary(mt.children[0].value);
  1234. });
  1235. }
  1236. }
  1237.  
  1238. /**
  1239. * Создает или модифицирует элемент документа. При создании используется NS XML-документа
  1240. *
  1241. * @param doc XMLDocument XML документ
  1242. * @param element string|Element Основной элемент. Если передана строка, то это будет tagName для создания элемента
  1243. * @param value Element|array|other Дочерний элемент или массив дочерних элементов, иначе - дочерний TextNode
  1244. *
  1245. * @return Element Основной элемент, переданный в параметре element, или вновь созданный, если была передана строка
  1246. */
  1247. function documentElement(doc, element, value) {
  1248. let el = typeof(element) === "object" ? element : doc.createElementNS(doc.documentElement.namespaceURI, element);
  1249. if (value !== undefined && value !== null) {
  1250. switch (typeof(value)) {
  1251. case "object":
  1252. (Array.isArray(value) ? value : [ value ]).forEach(function(it) {
  1253. el.appendChild(it);
  1254. });
  1255. break;
  1256. default:
  1257. el.appendChild(doc.createTextNode(value));
  1258. break;
  1259. }
  1260. }
  1261. return el;
  1262. }
  1263.  
  1264. /**
  1265. * Старт формирования XML-документа по накопленным данным книги
  1266. *
  1267. * @param book_data object Данные книги, по которым формируется итоговый XML-документ
  1268. * @param log Element Html-элемент в который будут писаться сообщения о прогрессе
  1269. *
  1270. * @return string Содержимое XML-документа, в виде строки
  1271. */
  1272. function documentStart(book_data, log) {
  1273. let doc = new DOMParser().parseFromString(
  1274. '<?xml version="1.0" encoding="UTF-8"?><FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0"/>',
  1275. "application/xml"
  1276. );
  1277. let root = doc.documentElement;
  1278. root.setAttribute("xmlns:l", "http://www.w3.org/1999/xlink");
  1279.  
  1280. logMessage(log, "---");
  1281.  
  1282. let li = null;
  1283. try {
  1284. li = logMessage(log, "Анализ бинарных данных...");
  1285. checkBinary(book_data);
  1286. makeBinaryIds(book_data);
  1287. li.ok();
  1288.  
  1289. li = logMessage(log, "Формирование описания...");
  1290. documentAddDescription(doc, root, book_data.descr);
  1291. let body = documentElement(doc, "body");
  1292. let authors = (book_data.descr.authors || []).map(function(author) {
  1293. let aa = [];
  1294. if (author.firstName) aa.push(author.firstName);
  1295. if (author.middleName) aa.push(author.middleName);
  1296. if (author.lastName) aa.push(author.lastName);
  1297. if (author.nickname) aa.push(author.nickname);
  1298. return aa.join(" ");
  1299. });
  1300. let btitle = documentElement(doc, "title");
  1301. if (authors.length) btitle.appendChild(documentElement(doc, "p", authors.join(", ")));
  1302. btitle.appendChild(documentElement(doc, "p", book_data.descr.bookTitle));
  1303. body.appendChild(btitle);
  1304. root.appendChild(body);
  1305. li.ok();
  1306.  
  1307. li = logMessage(log, "Формирование глав...");
  1308. documentAddChapters(doc, body, book_data.chapters);
  1309. li.ok();
  1310.  
  1311. if (book_data.materials) {
  1312. li = logMessage(log, "Формирование доп.материалов...");
  1313. documentAddMaterials(doc, body, book_data.materials);
  1314. li.ok();
  1315. }
  1316.  
  1317. li = logMessage(log, "Формирование бинарных данных...");
  1318. documentAddBinary(doc, root, book_data);
  1319. li.ok();
  1320. } catch (err) {
  1321. li && li.fail();
  1322. throw err;
  1323. }
  1324.  
  1325. logMessage(log, "---");
  1326. let data = xmldocToString(doc);
  1327. logMessage(log, "Готово!");
  1328. return data;
  1329. }
  1330.  
  1331. /**
  1332. * Создает картинку-заглушку в фомате png и возвращает ее данные в виде строки
  1333. *
  1334. * @return string Base64 строка с данными
  1335. */
  1336. function getDummyImage() {
  1337. let canvas = document.createElement("canvas");
  1338. canvas.setAttribute("width", "300");
  1339. canvas.setAttribute("height", "150");
  1340. if (!canvas.getContext) throw new Error("Ошибка работы с элементом canvas");
  1341. let ctx = canvas.getContext("2d");
  1342. // Фон
  1343. ctx.fillStyle = "White";
  1344. ctx.fillRect(0, 0, 300, 150);
  1345. // Обводка
  1346. ctx.lineWidth = 4;
  1347. ctx.strokeStyle = "Gray";
  1348. ctx.strokeRect(0, 0, 300, 150);
  1349. // Тень
  1350. ctx.shadowOffsetX = 2;
  1351. ctx.shadowOffsetY = 2;
  1352. ctx.shadowBlur = 2;
  1353. ctx.shadowColor = "rgba(0, 0, 0, 0.5)";
  1354. // Крест
  1355. let margin = 25;
  1356. let size = 40;
  1357. ctx.lineWidth = 10;
  1358. ctx.strokeStyle = "Red";
  1359. ctx.moveTo(300 / 2 - size / 2, margin);
  1360. ctx.lineTo(300 / 2 + size / 2, margin + size);
  1361. ctx.stroke();
  1362. ctx.moveTo(300 / 2 + size / 2, margin);
  1363. ctx.lineTo(300 / 2 - size / 2, margin + size);
  1364. ctx.stroke();
  1365. // Текст
  1366. ctx.font = "42px Times New Roman";
  1367. ctx.fillStyle = "Black";
  1368. ctx.textAlign = "center";
  1369. ctx.fillText("No image", 150, 120, 300);
  1370. // Получить данные
  1371. let data_str = canvas.toDataURL("image/png");
  1372. return data_str.substr(data_str.indexOf(",") + 1);
  1373. }
  1374.  
  1375. /**
  1376. * Пишет переданную строку в HTML-элемент лога как текст без дополнительных стилей
  1377. *
  1378. * @param log Element HTML-элемент лога
  1379. * @param message string Строка с сообщением
  1380. *
  1381. * @return object Объект для дальнейших манипуляций с записью
  1382. */
  1383. function logMessage(log, message) {
  1384. let block = document.createElement("div");
  1385. block.textContent = message;
  1386. log.appendChild(block);
  1387. log.scrollTop = log.scrollHeight;
  1388. function setSpan(text, color) {
  1389. if (!block.children.length)
  1390. block.appendChild(document.createElement("span"));
  1391. let sp = block.children[0];
  1392. sp.style.color = color;
  1393. sp.textContent = " " + text;
  1394. };
  1395. return {
  1396. ok: function() { setSpan("ok", "green"); },
  1397. fail: function() { setSpan("ошибка!", "red"); },
  1398. skipped: function() { setSpan("пропущено", "blue"); },
  1399. text: function(s) { setSpan(s, ""); },
  1400. element: function() { return block; },
  1401. };
  1402. }
  1403.  
  1404. /**
  1405. * Пишет переданную строку в HTML-элемент лога как текст предупреждения с цветным выделением
  1406. *
  1407. * @param log Element HTML-элемент лога
  1408. * @param message string Строка с сообщением
  1409. *
  1410. * @return Element Элемент с последним сообщением
  1411. */
  1412. function logWarning(log, message) {
  1413. let lo = logMessage(log, message);
  1414. lo.element().setAttribute("style", "color:#a00;");
  1415. return lo;
  1416. }
  1417.  
  1418. /**
  1419. * Создает и возвращает элемент кнопки, для начала отображения диалога формирования fb2 документа
  1420. *
  1421. * @return Element HTML-элемент кнопки для добавления на страницу
  1422. */
  1423. function createButton() {
  1424. let ae = document.createElement("a");
  1425. ae.setAttribute("class", "btn btn-default " + (mobile && "btn-download-work" || "btn-block"));
  1426. ae.setAttribute("style", "border-color:green;");
  1427. let ie = document.createElement("i");
  1428. ie.setAttribute("class", "icon-download");
  1429. ae.appendChild(ie);
  1430. ae.appendChild(document.createTextNode(""));
  1431. let btn = ae;
  1432. if (!mobile) {
  1433. btn = document.createElement("div");
  1434. btn.setAttribute("class", "mt-lg");
  1435. btn.appendChild(ae);
  1436. }
  1437. btn.setText = function(text) {
  1438. let el = this.nodeName === "A" ? this : this.querySelector("a");
  1439. el.childNodes[1].textContent = " " + (text || "Скачать FB2");
  1440. };
  1441. btn.setText();
  1442. return btn;
  1443. }
  1444.  
  1445. /**
  1446. * Создает и наполняет окно диалога для выбора глав и добавляет обработчики к элементам
  1447. *
  1448. * @return void
  1449. */
  1450. function showChaptersDialog() {
  1451. if (button.disabled) return;
  1452. button.disabled = true;
  1453. button.setText("Анализ...");
  1454.  
  1455. let params = getBookParams();
  1456.  
  1457. // Создает интерактивные элементы, которые будут отображены в форме диалога
  1458. let form = document.createElement("form");
  1459.  
  1460. let fst = document.createElement("fieldset");
  1461. fst.setAttribute("style", "border:1px solid #bbb; border-radius:6px; padding:5px 12px 0 12px;");
  1462. form.appendChild(fst);
  1463. let leg = document.createElement("legend");
  1464. leg.setAttribute("style", "display:inline; width:unset; font-size:100%; margin:0; padding:0 5px; border:none;");
  1465. fst.appendChild(leg);
  1466. leg.appendChild(document.createTextNode("Главы для выгрузки"));
  1467.  
  1468. let chs = document.createElement("div");
  1469. chs.setAttribute("style", "overflow:auto; max-height:50vh;");
  1470. fst.appendChild(chs);
  1471.  
  1472. let ntp = document.createElement("p");
  1473. ntp.setAttribute("class", "mb");
  1474. chs.appendChild(ntp);
  1475. ntp.appendChild(
  1476. document.createTextNode("Выберите главы для выгрузки. Обратите внимание: выгружены могут быть только доступные вам главы.")
  1477. );
  1478.  
  1479. let tbd = document.createElement("div");
  1480. tbd.setAttribute("class", "mt mb");
  1481. tbd.setAttribute("style", "display:flex; padding-top:10px; border-top:1px solid #bbb;");
  1482. fst.appendChild(tbd);
  1483.  
  1484. let its = document.createElement("span");
  1485. its.setAttribute("style", "margin:auto 5px auto 0");
  1486. tbd.appendChild(its);
  1487. its.appendChild(document.createTextNode("Выбрано глав: "));
  1488. let selected = document.createElement("strong");
  1489. selected.appendChild(document.createTextNode("0"));
  1490. its.appendChild(selected);
  1491. its.appendChild(document.createTextNode(" из "));
  1492. let total = document.createElement("strong");
  1493. its.appendChild(total);
  1494.  
  1495. let tb1 = document.createElement("button");
  1496. tb1.setAttribute("type", "button");
  1497. tb1.setAttribute("title", "Выделить все/ничего");
  1498. tb1.setAttribute("style", "margin-left:auto;");
  1499. tbd.appendChild(tb1);
  1500. let tb1i = document.createElement("i");
  1501. tb1i.setAttribute("class", "icon-check");
  1502. tb1.appendChild(tb1i);
  1503. tb1.appendChild(document.createTextNode(" ?"));
  1504.  
  1505. let log = document.createElement("div");
  1506. log.setAttribute("class", "mb");
  1507. log.setAttribute(
  1508. "style",
  1509. "display:none; overflow:auto; height:50vh; min-width:30vw; border:1px solid #bbb; border-radius:6px; padding: 6px;"
  1510. );
  1511. form.appendChild(log);
  1512.  
  1513. let nte = createCheckbox("Добавить примечания автора в аннотацию", !!params.authorNotes);
  1514. if (!params.authorNotes) nte.querySelector("input").disabled = true;
  1515. nte.setAttribute("style", "margin-top:" + (mobile && "10px" || "-10px"));
  1516. form.appendChild(nte);
  1517.  
  1518. let nie = createCheckbox("Не грузить картинки внутри глав", false);
  1519. nie.setAttribute("style", "margin-top:" + (mobile && "10px" || "-10px"));
  1520. form.appendChild(nie);
  1521.  
  1522. let nmt = createCheckbox("Не грузить дополнительные материалы", false);
  1523. if (!params.materials) nmt.querySelector("input").disabled = true;
  1524. nmt.setAttribute("style", "margin-top:" + (mobile && "10px" || "-10px"));
  1525. form.appendChild(nmt);
  1526.  
  1527. let sbd = document.createElement("div");
  1528. sbd.setAttribute("class", "mt text-center");
  1529. form.appendChild(sbd);
  1530. let sbt = document.createElement("button");
  1531. sbt.setAttribute("class", "button btn btn-success");
  1532. sbt.setAttribute("type", "submit");
  1533. sbt.appendChild(document.createTextNode("Продолжить"));
  1534. sbd.appendChild(sbt);
  1535.  
  1536. let chapters_list = [];
  1537.  
  1538. chs.addEventListener("change", function(event) {
  1539. let cnt = chapters_list.reduce(function(cnt, ch) {
  1540. if (!ch.locked && ch.element.children[0].children[0].checked) ++cnt;
  1541. return cnt;
  1542. }, 0);
  1543. selected.textContent = cnt;
  1544. sbt.disabled = !cnt;
  1545. });
  1546.  
  1547. tb1.addEventListener("click", function(event) {
  1548. let chf = chapters_list.some(function(ch) { return !ch.locked && !ch.element.children[0].children[0].checked; });
  1549. chapters_list.forEach(function(ch) { ch.element.children[0].children[0].checked = (chf && !ch.locked); });
  1550. chs.dispatchEvent(new Event("change"));
  1551. });
  1552.  
  1553. let mode = 0;
  1554. let fb2 = null;
  1555. let link = null;
  1556. form.addEventListener("submit", function(event) {
  1557. event.preventDefault();
  1558.  
  1559. if (mode === 1) {
  1560. afetch.abortAll();
  1561. return;
  1562. }
  1563.  
  1564. if (mode === 2) {
  1565. if (!link) {
  1566. link = document.createElement("a");
  1567. link.download = "book_" + chapters_list[0].workId + ".fb2";
  1568. link.href = URL.createObjectURL(new Blob([ fb2 ], { type: 'text/plain' }));
  1569. }
  1570. link.click();
  1571. return;
  1572. }
  1573.  
  1574. if (mode === -1) {
  1575. modalDialog.hide();
  1576. return;
  1577. }
  1578.  
  1579. mode = 1;
  1580. if (!chapters_list.length) {
  1581. alert("Нет глав для выгрузки!");
  1582. return;
  1583. }
  1584.  
  1585. fst.style.display = "none";
  1586. nte.style.display = "none";
  1587. nie.style.display = "none";
  1588. nmt.style.display = "none";
  1589. log.style.display = "block";
  1590. sbt.textContent = "Прервать";
  1591.  
  1592. let book_data = {};
  1593. if (!nte.querySelector("input").checked) params.authorNotes = null;
  1594. let without_img = nie.querySelector("input").checked;
  1595. if (nmt.querySelector("input").checked) params.materials = null;
  1596. extractDescriptionData(params, log).then(function(descr) {
  1597. book_data.descr = descr;
  1598. logMessage(log, "---");
  1599. return extractChapters(chapters_list.filter(function(ch) {
  1600. return !ch.locked && ch.element.children[0].children[0].checked;
  1601. }).map(function(ch) {
  1602. return { title: ch.title, workId: ch.workId, chapterId: ch.chapterId };
  1603. }), log, { withoutImages: without_img });
  1604. }).then(function(chapters) {
  1605. book_data.chapters = chapters;
  1606. if (params.materials) {
  1607. logMessage(log, "---");
  1608. logMessage(log, "Дополнительные материалы:");
  1609. return extractMaterials(params.materials, log);
  1610. }
  1611. }).then(function(materials) {
  1612. book_data.materials = materials;
  1613. fb2 = documentStart(book_data, log);
  1614. sbt.textContent = "Сохранить в файл";
  1615. mode = 2;
  1616. }).catch(function(err) {
  1617. mode = -1;
  1618. sbt.textContent = "Закрыть";
  1619. console.error(err);
  1620. if (err.name === "AbortError")
  1621. alert("Операция прервана")
  1622. else
  1623. alert(err);
  1624. });
  1625. });
  1626.  
  1627. // Получает список глав
  1628. let ch_cnt = 0;
  1629. getChaptersList(params).then(function(list) {
  1630. list.forEach(function(ch) {
  1631. ch.element = createChapterCheckbox(ch);
  1632. chs.appendChild(ch.element);
  1633. ++ch_cnt;
  1634. });
  1635. chapters_list = list;
  1636. chs.dispatchEvent(new Event("change"));
  1637. total.appendChild(document.createTextNode(ch_cnt));
  1638.  
  1639. // Отображает модальное диалоговое окно
  1640. modalDialog.show({
  1641. mobile: mobile,
  1642. title: "Выгрузка книги в FB2",
  1643. body: form,
  1644. onclose: function() {
  1645. fb2 = null;
  1646. if (link) {
  1647. URL.revokeObjectURL(link.href);
  1648. link = null;
  1649. }
  1650. if (mode === 1) afetch.abortAll();
  1651. },
  1652. });
  1653. }).catch(function(err) {
  1654. console.error(err);
  1655. alert(err);
  1656. }).finally(function() {
  1657. button.disabled = false;
  1658. button.setText();
  1659. });
  1660. }
  1661.  
  1662. /**
  1663. * Создает единичный элемент типа checkbox в стиле сайта
  1664. *
  1665. * @param title string Подпись для checkbox
  1666. * @param checked bool Начальное состояние checkbox
  1667. *
  1668. * @return Element HTML-элемент для последующего добавления на форму
  1669. */
  1670. function createCheckbox(title, checked) {
  1671. let root = document.createElement("div");
  1672. root.setAttribute("class", "checkbox c-checkbox no-fastclick mb");
  1673. let label = document.createElement("label");
  1674. root.appendChild(label);
  1675. let input = document.createElement("input");
  1676. input.setAttribute("type", "checkbox");
  1677. label.appendChild(input);
  1678. let span = document.createElement("span");
  1679. span.setAttribute("class", "icon-check-bold");
  1680. label.appendChild(span);
  1681. label.appendChild(document.createTextNode(title));
  1682. if (checked) {
  1683. input.setAttribute("checked", "checked");
  1684. }
  1685. return root;
  1686. }
  1687.  
  1688. /**
  1689. * Создает checkbox для диалога выбора главы
  1690. *
  1691. * @param chapter object Данные главы
  1692. *
  1693. * @return Element HTML-элемент для последующего добавления на форму
  1694. */
  1695. function createChapterCheckbox(chapter) {
  1696. let root = createCheckbox(chapter.title || "Без названия", !chapter.locked);
  1697. if (chapter.locked) {
  1698. root.querySelector("input").disabled = true;
  1699. let lock = document.createElement("i");
  1700. lock.setAttribute("class", "icon-lock text-muted ml-sm");
  1701. root.children[0].appendChild(lock);
  1702. }
  1703. if (!chapter.title) root.style.fontStyle = "italic";
  1704. return root;
  1705. }
  1706.  
  1707. /**
  1708. * Создает диалоговое окно и управляет им.
  1709. * При каждом вызове метода show окно создается заново.
  1710. * Singleton.
  1711. */
  1712. modalDialog = {
  1713. element: null,
  1714. onclose: null,
  1715. mobile: false,
  1716.  
  1717. show: function(params) {
  1718. if (params.mobile) {
  1719. this.mobile = true;
  1720. this._show_m(params);
  1721. return;
  1722. }
  1723.  
  1724. this.element = document.createElement("div");
  1725. this.element.setAttribute("class", "modal fade in");
  1726. this.element.setAttribute("tabindex", "-1");
  1727. this.element.setAttribute("role", "dialog");
  1728. this.element.setAttribute("style", "display:block; padding-right:12px;");
  1729. let dlg = document.createElement("div");
  1730. dlg.setAttribute("class", "modal-dialog");
  1731. dlg.setAttribute("role", "document");
  1732. this.element.appendChild(dlg);
  1733. let ctn = document.createElement("div");
  1734. ctn.setAttribute("class", "modal-content");
  1735. dlg.appendChild(ctn);
  1736. let hdr = document.createElement("div");
  1737. hdr.setAttribute("class", "modal-header");
  1738. ctn.appendChild(hdr);
  1739. let hbt = document.createElement("button");
  1740. hbt.setAttribute("class", "close");
  1741. hbt.setAttribute("type", "button");
  1742. hdr.appendChild(hbt);
  1743. let sbt = document.createElement("span");
  1744. hbt.appendChild(sbt);
  1745. sbt.appendChild(document.createTextNode("×"));
  1746. let htl = document.createElement("h4");
  1747. htl.setAttribute("class", "modal-title");
  1748. hdr.appendChild(htl);
  1749. htl.appendChild(document.createTextNode(params.title));
  1750.  
  1751. let bdy = document.createElement("div");
  1752. bdy.setAttribute("class", "modal-body");
  1753. bdy.setAttribute("style", "color:#656565; min-width:250px; max-width:max(500px,35vw);");
  1754. ctn.appendChild(bdy);
  1755. bdy.appendChild(params.body);
  1756.  
  1757. document.body.appendChild(this.element);
  1758.  
  1759. this.backdrop = document.createElement("div");
  1760. this.backdrop.setAttribute("class", "modal-backdrop fade in");
  1761. document.body.appendChild(this.backdrop);
  1762.  
  1763. document.body.classList.add("modal-open");
  1764.  
  1765. this.onclose = params.onclose || null;
  1766.  
  1767. this.element.addEventListener("click", function(event) {
  1768. if (event.target === this.element || event.target.closest("button.close")) {
  1769. this.hide();
  1770. }
  1771. }.bind(this));
  1772. this.element.addEventListener("keydown", function(event) {
  1773. if (event.code == "Escape" && !event.shiftKey && !event.ctrlKey && !event.altKey) {
  1774. this.hide();
  1775. event.preventDefault();
  1776. }
  1777. }.bind(this));
  1778. },
  1779.  
  1780. hide: function() {
  1781. if (this.mobile) {
  1782. this._hide_m();
  1783. return;
  1784. }
  1785.  
  1786. if (this.element && this.backdrop) {
  1787. this.backdrop.remove();
  1788. this.backdrop = null;
  1789. this.element.remove();
  1790. this.element = null;
  1791. document.body.classList.remove("modal-open");
  1792. if (this.onclose) this.onclose();
  1793. this.onclose = null;
  1794. }
  1795. },
  1796.  
  1797. _show_m: function(params) {
  1798. this.element = document.createElement("div");
  1799. this.element.setAttribute("class", "popup popup-screen-content");
  1800. this.element.setAttribute("style", "overflow:hidden;");
  1801. let ctn = document.createElement("div");
  1802. ctn.setAttribute("class", "content-block");
  1803. this.element.appendChild(ctn);
  1804. let htl = document.createElement("h2");
  1805. htl.setAttribute("class", "text-center");
  1806. htl.appendChild(document.createTextNode(params.title));
  1807. ctn.appendChild(htl);
  1808. let bdy = document.createElement("div");
  1809. bdy.setAttribute("class", "modal-body");
  1810. bdy.setAttribute("style", "color:#656565;");
  1811. ctn.appendChild(bdy);
  1812. bdy.appendChild(params.body);
  1813. let cbt = document.createElement("button");
  1814. cbt.setAttribute("class", "mt button btn btn-default");
  1815. cbt.appendChild(document.createTextNode("Закрыть"));
  1816. ctn.appendChild(cbt);
  1817.  
  1818. cbt.addEventListener("click", function(event) {
  1819. this.hide();
  1820. }.bind(this));
  1821.  
  1822. document.body.appendChild(this.element);
  1823. this.element.style.display = "block";
  1824.  
  1825. this.element.classList.add("modal-in");
  1826. this._turnOverlay_m(true);
  1827. },
  1828.  
  1829. _hide_m: function() {
  1830. if (this.element) {
  1831. this.element.remove();
  1832. this.element = null;
  1833. if (this.onclose) {
  1834. this.onclose();
  1835. this.onclose = null;
  1836. }
  1837. this._turnOverlay_m(false);
  1838. }
  1839. },
  1840.  
  1841. _turnOverlay_m(on) {
  1842. let overlay = document.querySelector("div.popup-overlay");
  1843. if (!overlay && on) {
  1844. overlay = document.createElement("div");
  1845. overlay.setAttribute("class", "popup-overlay");
  1846. document.body.appendChild(overlay);
  1847. }
  1848. if (on) {
  1849. overlay.classList.add("modal-overlay-visible");
  1850. } else if (overlay) {
  1851. overlay.classList.remove("modal-overlay-visible");
  1852. }
  1853. }
  1854. };
  1855.  
  1856. /**
  1857. * Обертка для ассинхронных запросов с возможностью отмены всех запросов разом
  1858. *
  1859. * @param url string Адрес запрашиваемого ресурса
  1860. * @param params object Параметры асинхронного запроса
  1861. * @param li object Запись лога, для отображения прогресса. Необязательный параметр.
  1862. *
  1863. * @return Promise Промис, который вернет запрашиваемые данные
  1864. */
  1865. function afetch(url, params, li) {
  1866. params ||= {};
  1867. params.url = url;
  1868. params.method ||= "GET";
  1869. return new Promise(function(resolve, reject) {
  1870. let req = null;
  1871. params.onload = function(r) {
  1872. if (r.status === 200) {
  1873. let headers = {};
  1874. r.responseHeaders.split("\n").forEach(function(hs) {
  1875. let h = hs.split(":");
  1876. if (h[1]) headers[h[0].trim().toLowerCase()] = h[1].trim();
  1877. });
  1878. resolve({ headers: headers, response: r.response });
  1879. } else {
  1880. reject(new Error("Сервер вернул ошибку (" + r.status + ")"));
  1881. }
  1882. };
  1883. params.onerror = function(e) {
  1884. reject(e);
  1885. };
  1886. params.ontimeout = function(e) {
  1887. reject(e);
  1888. };
  1889. params.onloadend = function() {
  1890. req && afetch.ctl_list.delete(req);
  1891. };
  1892. if (li) {
  1893. params.onprogress = function(pe) {
  1894. if (pe.lengthComputable)
  1895. li.text("" + Math.round(pe.loaded / pe.total * 100) + "%");
  1896. };
  1897. }
  1898. try {
  1899. req = GM.xmlHttpRequest(params);
  1900. req && afetch.ctl_list.add(req);
  1901. } catch (e) {
  1902. reject(e);
  1903. }
  1904. });
  1905. }
  1906.  
  1907. /**
  1908. * Инициирует структуру обертки
  1909. */
  1910. afetch.init = function() {
  1911. afetch.ctl_list = new Set();
  1912. };
  1913.  
  1914. /**
  1915. * Прерывает все выполняющиеся ассинхронные запросы и очищает хранилище контроллеров
  1916. */
  1917. afetch.abortAll = function() {
  1918. afetch.ctl_list.forEach(function(ctl) {
  1919. ctl.abort();
  1920. });
  1921. afetch.ctl_list.clear();
  1922. };
  1923.  
  1924. /**
  1925. * Расшифровывает полученную от сервера строку с текстом
  1926. *
  1927. * @param chapter string Зашифованная глава книги, полученная от сервера
  1928. * @param secret string Часть ключа для расшифровки
  1929. *
  1930. * @return string Расшифрованный текст
  1931. */
  1932. function decryptText(chapter, secret) {
  1933. let ss = secret.split("").reverse().join("") + "@_@" + (app.userId || "");
  1934. let slen = ss.length;
  1935. let clen = chapter.data.text.length;
  1936. let result = [];
  1937. for (let pos = 0; pos < clen; ++pos) {
  1938. result.push(String.fromCharCode(chapter.data.text.charCodeAt(pos) ^ ss.charCodeAt(Math.floor(pos % slen))));
  1939. }
  1940. return result.join("");
  1941. }
  1942.  
  1943. /**
  1944. * Возвращает текстовое представление XML-дерева элементов
  1945. *
  1946. * @param doc XMLDocument XML-документ
  1947. *
  1948. * @return string XML-документ в виде строки
  1949. */
  1950. function xmldocToString(doc) {
  1951. // TODO! Сделать переносы строк и отступы в итоговом XML-файле.
  1952. return (new XMLSerializer()).serializeToString(doc);
  1953. }
  1954.  
  1955. /**
  1956. * Возвращает хэш переданной строки. Используется как часть уникального идентификатора книги
  1957. *
  1958. * @param str string Строка для получения хэша
  1959. *
  1960. * @return string Строковое представление хэша переданной строки
  1961. */
  1962. function stringHash(str) {
  1963. let hash = 0;
  1964. let slen = str.length;
  1965. for (let i = 0; i < slen; ++i) {
  1966. let ch = str.charCodeAt(i);
  1967. hash = ((hash << 5) - hash) + ch;
  1968. hash = hash & hash; // Convert to 32bit integer
  1969. }
  1970. return Math.abs(hash).toString() + (hash > 0 ? "1" : "");
  1971. }
  1972.  
  1973. /**
  1974. * Класс для управления наблюдением за логотипом сайта, чтобы отлавливать изменения в странице
  1975. * производимые сайтом через свои скрипты. Там вместо лого отображается картинка часиков.
  1976. */
  1977. observer = {
  1978. _observer: null,
  1979.  
  1980. start: function(handler) {
  1981. let logo = document.querySelector("div.brand-logo");
  1982. if (logo) {
  1983. if (!this._observer)
  1984. this._observer = new MutationObserver(function() {
  1985. if (!logo.querySelector("div#nprogress"))
  1986. handler();
  1987. });
  1988. this._observer.observe(logo, { childList: true, subtree: true });
  1989. }
  1990. },
  1991.  
  1992. stop: function() {
  1993. if (this._observer)
  1994. this._observer.disconnect();
  1995. }
  1996. };
  1997.  
  1998. /**
  1999. * Список фиксированных жанров для FB2.
  2000. * Первый элемент - Точное название жанра
  2001. * Последующие элементы - ключевые слова в нижнем регистре для дополнительной идентификации жанра
  2002. * Список взят отсюда: https://github.com/gribuser/fb2/blob/master/FictionBookGenres.xsd
  2003. */
  2004. let GENRE_MAP = {
  2005. adv_animal: [ "Природа и животные", "приключения", "животные", "природа" ],
  2006. adv_geo: [ "Путешествия и география", "приключения", "география", "путешествие" ],
  2007. adv_history: [ "Исторические приключения", "история", "приключения" ],
  2008. adv_maritime: [ "Морские приключения", "приключения", "море" ],
  2009. //adv_western: [ ], //??
  2010. adventure: [ "Приключения" ],
  2011. antique: [ "Старинное" ],
  2012. antique_ant: [ "Античная литература", "старинное", "античность" ],
  2013. antique_east: [ "Древневосточная литература", "старинное", "восток" ],
  2014. antique_european: [ "Европейская старинная литература", "старинное", "европа" ],
  2015. antique_myths: [ "Мифы. Легенды. Эпос", "мифы", "легенды", "эпос" ],
  2016. antique_russian: [ "Древнерусская литература", "древнерусское" ],
  2017. aphorism_quote: [ "Афоризмы, цитаты" ],
  2018. architecture_book: [ "Скульптура и архитектура", "дизайн" ],
  2019. auto_regulations: [ "Автомобили и ПДД", "дорожного", "движения", "дорожное", "движение" ],
  2020. banking: [ "Финансы", "банки", "деньги" ],
  2021. beginning_authors: [ "Начинающие авторы" ],
  2022. child_adv: [ "Приключения для детей и подростков" ],
  2023. child_det: [ "Детская остросюжетная литература" ],
  2024. child_education: [ "Детская образовательная литература" ],
  2025. child_prose: [ "Проза для детей" ],
  2026. child_sf: [ "Фантастика для детей" ],
  2027. child_tale: [ "Сказки для детей" ],
  2028. child_verse: [ "Стихи для детей" ],
  2029. children: [ "Детское" ],
  2030. cinema_theatre: [ "Кино и театр" ],
  2031. city_fantasy: [ "Городское фэнтези" ],
  2032. comp_db: [ "Компьютерные базы данных" ],
  2033. comp_hard: [ "Компьютерное железо", "аппаратное" ],
  2034. comp_osnet: [ "ОС и копьютерные сети" ],
  2035. comp_programming: [ "Программирование" ],
  2036. comp_soft: [ "Программное обеспечение" ],
  2037. comp_www: [ "Интернет" ],
  2038. computers: [ "Компьютеры" ],
  2039. design: [ "Дизайн" ],
  2040. det_action: [ "Боевики", "боевик" ],
  2041. det_classic: [ "Классический детектив" ],
  2042. det_crime: [ "Криминальный детектив", "криминал" ],
  2043. det_espionage: [ "Шнионский детектив", "шпион", "шпионы" ],
  2044. det_hard: [ "Крутой детектив" ],
  2045. det_history: [ "Исторический детектив", "история" ],
  2046. det_irony: [ "Иронический детектив" ],
  2047. det_police: [ "Полицейский детектив", "полиция" ],
  2048. det_political: [ "Политический детектив", "политика" ],
  2049. detective: [ "Детективы", "детектив" ],
  2050. dragon_fantasy: [ "Фэнтези с драконами", "драконы", "дракон" ],
  2051. dramaturgy: [ "Драматургия" ],
  2052. economics: [ "Экономика" ],
  2053. essays: [ "Эссэ" ],
  2054. fantasy_fight: [ "Боевое фэнези" ],
  2055. foreign_action: [ "Зарубежные боевики", "иностранные" ],
  2056. foreign_adventure: [ "Зарубежная приключенческая литература", "иностранная", "приключения" ],
  2057. foreign_antique: [ "Средневековая классическая проза" ],
  2058. foreign_business: [ "Зарубежная карьера и бизнес", "иностранная" ],
  2059. foreign_children: [ "Зарубежная литература для детей" ],
  2060. foreign_comp: [ "Зарубежная компьютерная литература" ],
  2061. foreign_contemporary: [ "Зарубежная современная литература" ],
  2062. //foreign_contemporary_lit: [ ], //??
  2063. //foreign_desc: [ ], //??
  2064. foreign_detective: [ "Зарубежные детективы", "иностранные", "зарубежный", "детектив" ],
  2065. foreign_dramaturgy: [ "Зарубежная драматургия" ],
  2066. foreign_edu: [ "Зарубежная образовательная литература", "иностранная" ],
  2067. foreign_fantasy: [ "Зарубежное фэнтези", "иностранное", "иностранная", "зарубежная", "фантастика" ],
  2068. foreign_home: [ "Зарубежное домоводство", "иностранное" ],
  2069. foreign_humor: [ "Зарубежная юмористическая литература", "иностранная" ],
  2070. foreign_language: [ "Иностранные языки" ],
  2071. foreign_love: [ "Зарубежная любовная литература", "иностранная" ],
  2072. foreign_novel: [ "Зарубежные романы", "иностранные" ],
  2073. foreign_other: [ "Другая зарубежная литература", "иностранная" ],
  2074. foreign_poetry: [ "Зарубежная поэзия", "иностранная", "зарубежные", "стихи" ],
  2075. foreign_prose: [ "Зарубежная классическая проза", "иностранная", "проза" ],
  2076. foreign_psychology: [ "Зарубежная литература о прихологии", "иностранная" ],
  2077. foreign_publicism: [ "Зарубежная публицистика", "иностранная", "документальная" ],
  2078. foreign_religion: [ "Зарубежная религия", "иностранная" ],
  2079. foreign_sf: [ "Зарубежная научная фантастика", "иностранная" ],
  2080. geo_guides: [ "Путеводители, карты, атласы", "география" ],
  2081. geography_book: [ "Путешествия и география" ],
  2082. global_economy: [ "Глобальная экономика" ],
  2083. historical_fantasy: [ "Историческое фэнтези" ],
  2084. home: [ "Домоводство", "дом", "семья" ],
  2085. home_cooking: [ "Кулинария" ],
  2086. home_crafts: [ "Хобби и ремесла" ],
  2087. home_diy: [ "Сделай сам" ],
  2088. home_entertain: [ "Развлечения" ],
  2089. home_garden: [ "Сад и огород" ],
  2090. home_health: [ "Здоровье" ],
  2091. home_pets: [ "Домашние животные" ],
  2092. home_sex: [ "Семейные отношения, секс" ],
  2093. home_sport: [ "Боевые исскусства, спорт" ],
  2094. humor: [ "Юмор" ],
  2095. humor_anecdote: [ "Анекдоты" ],
  2096. humor_fantasy: [ "Юмористическое фэтези","юмористическая", "фантастика" ],
  2097. humor_prose: [ "Юмористическая проза" ],
  2098. humor_verse: [ "Юмористические стихи, басни" ],
  2099. industries: [ "Отрасли", "индустрия" ],
  2100. job_hunting: [ "Поиск работы", "работа" ],
  2101. literature_18: [ "Классическая проза XVII-XVIII веков" ],
  2102. literature_19: [ "Классическая проза ХIX века" ],
  2103. literature_20: [ "Классическая проза ХX века" ],
  2104. love_contemporary: [ "Современные любовные романы" ],
  2105. love_detective: [ "Остросюжетные любовные романы", "детектив", "любовь" ],
  2106. love_erotica: [ "Эротическая литература", "эротика" ],
  2107. love_fantasy: [ "Любовное фэнтези" ],
  2108. love_history: [ "Исторические любовные романы", "история", "любовь" ],
  2109. love_sf: [ "Любовно-фантастические романы" ],
  2110. love_short: [ "Короткие любовные романы" ],
  2111. magician_book: [ "Магия, фокусы" ],
  2112. management: [ "Менеджмент", "управление" ],
  2113. marketing: [ "Маркетинг", "продажи" ],
  2114. military_special: [ "Специальная военная литература" ],
  2115. music_dancing: [ "Музыка и танцы" ],
  2116. narrative: [ "Повествование" ],
  2117. newspapers: [ "Газеты" ],
  2118. nonf_biography: [ "Биографии и Мемуары" ],
  2119. nonf_criticism: [ "Критика" ],
  2120. nonf_publicism: [ "Публицистика" ],
  2121. nonfiction: [ "Документальная литература" ],
  2122. org_behavior: [ "Маркентиг, PR", "организации" ],
  2123. paper_work: [ "Канцелярская работа" ],
  2124. pedagogy_book: [ "Педагогическая литература" ],
  2125. periodic: [ "Журналы, газеты" ],
  2126. personal_finance: [ "Личные финансы" ],
  2127. poetry: [ "Поэзия" ],
  2128. popadanec: [ "Попаданцы", "попаданец" ],
  2129. popular_business: [ "Карьера, кадры", "карьера", "дело", "бизнес" ],
  2130. prose_classic: [ "Классическая проза" ],
  2131. prose_counter: [ "Контркультура" ],
  2132. prose_history: [ "Историческая проза", "история", "проза" ],
  2133. prose_military: [ "Проза о войне" ],
  2134. prose_rus_classic: [ "Русская классическая проза" ],
  2135. prose_su_classics: [ "Советская классическая проза" ],
  2136. psy_classic: [ "Классическая психология" ],
  2137. psy_childs: [ "Детская психология" ],
  2138. psy_generic: [ "Общая психология" ],
  2139. psy_personal: [ "Психология личности" ],
  2140. psy_sex_and_family: [ "Семейная психология", "семья", "секс" ],
  2141. psy_social: [ "Социальная психология" ],
  2142. psy_theraphy: [ "Психотерапия", "психология", "терапия" ],
  2143. //real_estate: [ ], // ??
  2144. ref_dict: [ "Словари", "справочник" ],
  2145. ref_encyc: [ "Энциклопедии", "энциклопедия" ],
  2146. ref_guide: [ "Руководства", "руководство", "справочник" ],
  2147. ref_ref: [ "Справочники", "справочник" ],
  2148. reference: [ "Справочная литература" ],
  2149. religion: [ "Религия" ],
  2150. religion_esoterics: [ "Эзотерическая литература", "эзотерика" ],
  2151. //religion_rel: [ ], // ??
  2152. religion_self: [ "Самосовершенствование" ],
  2153. russian_contemporary: [ "Русская современная литература" ],
  2154. russian_fantasy: [ "Славянское фэнтези" ],
  2155. sci_biology: [ "Биология" ],
  2156. sci_chem: [ "Химия" ],
  2157. sci_culture: [ "Культурология" ],
  2158. sci_history: [ "История" ],
  2159. sci_juris: [ "Юриспруденция" ],
  2160. sci_linguistic: [ "Языкознание", "иностранный", "язык" ],
  2161. sci_math: [ "Математика" ],
  2162. sci_medicine: [ "Медицина" ],
  2163. sci_philosophy: [ "Философия" ],
  2164. sci_phys: [ "Физика" ],
  2165. sci_politics: [ "Политика" ],
  2166. sci_religion: [ "Религиоведение", "религия", "духовность" ],
  2167. sci_tech: [ "Технические науки", "техника" ],
  2168. science: [ "Научная литература", "образование" ],
  2169. sf: [ "Научная фантастика", "наука", "фантастика" ],
  2170. sf_action: [ "Боевая фантастика" ],
  2171. sf_cyberpunk: [ "Киберпанк" ],
  2172. sf_detective: [ "Детективная фантастика", "детектив", "фантастика" ],
  2173. sf_fantasy: [ "Фэнтези" ],
  2174. sf_heroic: [ "Героическая фантастика", "герой" ],
  2175. sf_history: [ "Альтернативная история", "история", "фантастика" ],
  2176. sf_horror: [ "Ужасы" ],
  2177. sf_humor: [ "Юмористическая фантастика", "юмор", "фантастика" ],
  2178. sf_social: [ "Социально-психологическая фантастика", "социум", "психология", "фантастика" ],
  2179. sf_space: [ "Космическая фантастика", "космос", "фантастика" ],
  2180. short_story: [ "Рассказы", "рассказ" ],
  2181. sketch: [ "Отрывок", "зарисовка", "набросок", "очерк" ],
  2182. small_business: [ "Малый бизнес", "бизнес", "карьера" ],
  2183. sociology_book: [ "Обществознание", "социология" ],
  2184. stock: [ "Ценные бумаги" ],
  2185. thriller: [ "Триллер", "триллеры" ],
  2186. upbringing_book: [ "Воспитание" ],
  2187. vampire_book: [ "Вампиры", "вампир" ],
  2188. visual_arts: [ "Изобразительное искусство" ],
  2189. };
  2190.  
  2191. /**
  2192. * Преобразование жанров сайта в идентификаторы жанров FB2
  2193. *
  2194. * @param keys Array Массив жанров с сайта
  2195. *
  2196. * @return Array Массив жанров формата FB2
  2197. */
  2198. function identifyGenre(keys) {
  2199. let gmap = {};
  2200. let addWeight = function(name, weight) {
  2201. gmap[name] = (gmap[name] || 0) + weight;
  2202. };
  2203. for (let i = 0; i < keys.length; ++i) {
  2204. let site_key = keys[i].toLowerCase();
  2205. let site_wkeys = site_key.split(/[\s,.;]+/);
  2206. if (site_wkeys.length === 1) site_wkeys = [];
  2207. for (let g_name in GENRE_MAP) {
  2208. let g_values = GENRE_MAP[g_name];
  2209. let g_title = g_values[0].toLowerCase();
  2210. if (site_key === g_title) {
  2211. addWeight(g_name, 3); // Точное совпадение!
  2212. break;
  2213. }
  2214. // Искать каждое слово жанра с сайта отдельно
  2215. let weight = 0;
  2216. if (site_wkeys.indexOf(g_title) !== -1) weight += 2;
  2217. if (site_wkeys.length) {
  2218. for (let k = 1; k < g_values.length; ++k) {
  2219. if (site_wkeys.indexOf(g_values[k]) !== -1) ++weight;
  2220. }
  2221. }
  2222. if (weight >= 2) addWeight(g_name, weight);
  2223. }
  2224. }
  2225.  
  2226. let res = Object.keys(gmap).map(function(genre) {
  2227. return [ genre, gmap[genre] ];
  2228. });
  2229. if (!res.length) return [];
  2230. res.sort(function(a, b) { return b[1] < a[1]; });
  2231.  
  2232. let cur_w = 0;
  2233. let res_genres = [];
  2234. for (let i = 0; i < res.length; ++i) {
  2235. if (res[i][1] !== cur_w && res_genres.length >= 3) break;
  2236. cur_w = res[i][1];
  2237. res_genres.push(res[i][0]);
  2238. }
  2239. return res_genres;
  2240. }
  2241.  
  2242. // Запускает скрипт после загрузки страницы сайта
  2243. if (document.readyState === "loading")
  2244. window.addEventListener("DOMContentLoaded", init);
  2245. else
  2246. init();
  2247. }());
  2248.  

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址