ReadliBookExtractor

The script adds a button to the site for downloading books to an FB2 file

  1. // ==UserScript==
  2. // @name ReadliBookExtractor
  3. // @namespace 90h.yy.zz
  4. // @version 0.8.2
  5. // @author Ox90
  6. // @match https://readli.net/*
  7. // @description The script adds a button to the site for downloading books to an FB2 file
  8. // @description:ru Скрипт добавляет кнопку для скачивания книги в формате FB2
  9. // @require https://update.gf.qytechs.cn/scripts/468831/1478439/HTML2FB2Lib.js
  10. // @grant unsafeWindow
  11. // @run-at document-start
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. (function start() {
  16.  
  17. let env = {};
  18. let stage = 0;
  19.  
  20. function init() {
  21. env.popupShow = window.popupShow || (unsafeWindow && unsafeWindow.popupShow);
  22. pageHandler();
  23. }
  24.  
  25. async function pageHandler() {
  26. let book_doc = null;
  27. if (document.querySelector("a.book-actions__button[href^=\"/chitat-online/\"]")) {
  28. book_doc = document;
  29. } else if (document.querySelector("div.reading-end__content")) {
  30. const hdr = document.querySelector("h1>a");
  31. if (hdr && hdr.href) book_doc = await getBookOverview(hdr.href);
  32. }
  33. if (book_doc) {
  34. const book_page = book_doc.querySelector("main.main>section.wrapper.page");
  35. if (book_page) {
  36. const dlg_data = makeDownloadDialog();
  37. const btn_list = document.querySelector("section.download>ul.download__list");
  38. insertDownloadButton(book_page, dlg_data, btn_list);
  39. }
  40. }
  41. }
  42.  
  43. async function getBookOverview(url) {
  44. return (new DOMParser()).parseFromString(await FB2Loader.addJob(url), "text/html");
  45. }
  46.  
  47. function insertDownloadButton(book_page, dlg_data, btn_list) {
  48. // Создать кнопку
  49. const btn = document.createElement("li");
  50. btn.classList.add("download__item");
  51. const link = document.createElement("a");
  52. link.classList.add("download__link");
  53. link.href = "#";
  54. link.textContent = "fb2-ex";
  55. btn.appendChild(link);
  56. // Попытаться вставить новую кнопку сразу после оригинальной fb2
  57. let item = btn_list.firstElementChild;
  58. while (item) {
  59. if (item.textContent === "fb2") break;
  60. item = item.nextElementSibling;
  61. }
  62. if (item) {
  63. item.after(btn);
  64. } else {
  65. btn_list.appendChild(btn);
  66. }
  67. // Ссылка на данные книги
  68. let fb2doc = null;
  69. // Установить обработчик для новой кнопки
  70. btn.addEventListener("click", event => {
  71. event.preventDefault();
  72. try {
  73. fb2doc = new ReadliFB2Document();
  74. fb2doc.lang = "ru";
  75. fb2doc.idPrefix = "rdlbe_";
  76. dlg_data.log.clean();
  77. dlg_data.lat.disabled = false;
  78. dlg_data.lat.checked = Settings.get("fixlatin");
  79. dlg_data.sbm.textContent = setStage(0);
  80. env.popupShow("#rbe-download-dlg");
  81. getBookInfo(fb2doc, book_page, dlg_data.log);
  82. } catch (e) {
  83. dlg_data.log.message(e.message, "red");
  84. dlg_data.sbm.textContent = setStage(3);
  85. } finally {
  86. dlg_data.sbm.disabled = false;
  87. }
  88. });
  89. // Установить обработчик для основной кнопки диалога
  90. dlg_data.sbm.addEventListener("click", () => makeAction(fb2doc, dlg_data));
  91. // Установить обработчик для скрытия диалога
  92. dlg_data.dlg.addEventListener("dlg-hide", () => {
  93. if (dlg_data.link) {
  94. URL.revokeObjectURL(dlg_data.link.href);
  95. dlg_data.link = null;
  96. }
  97. fb2doc = null;
  98. });
  99. }
  100.  
  101. function makeDownloadDialog() {
  102. const popups = document.querySelector("div.popups");
  103. if (!popups) throw new Error("Не найден блок popups");
  104. const dlg_c = document.createElement("div");
  105. dlg_c.id = "rbe-download-dlg";
  106. popups.appendChild(dlg_c);
  107. dlg_c.innerHTML =
  108. '<div class="popup" data-src="#rbe-download-dlg">' +
  109. '<button class="popup__close button-close-2"></button>' +
  110. '<div class="popup-form" style="display:flex; flex-direction:column; gap:.5em;">' +
  111. '<h2 style="margin:0; padding:0 0 .5em;">Скачать книгу</h2>' +
  112. '<div class="rbe-log"></div>' +
  113. '<label style="display:flex; gap:.5em; cursor:pointer;">' +
  114. '<input type="checkbox" name="fix_lat" style="appearance:auto;">Исправлять латиницу в тексте</label>' +
  115. '<button class="button rbe-submit" disabled="true">Продолжить</button>' +
  116. '</div>' +
  117. '</div>';
  118. const dlg = dlg_c.querySelector("div.popup-form");
  119. const dlg_data = {
  120. dlg: dlg,
  121. log: new LogElement(dlg.querySelector(".rbe-log")),
  122. lat: dlg.querySelector("input[name=fix_lat]"),
  123. sbm: dlg.querySelector("button.rbe-submit")
  124. };
  125. (new MutationObserver(() => {
  126. if (dlg_c.children.length) {
  127. dlg.dispatchEvent(new CustomEvent("dlg-hide"));
  128. }
  129. })).observe(dlg_c, { childList: true });
  130. return dlg_data;
  131. }
  132.  
  133. async function makeAction(fb2doc, dlg_data) {
  134. try {
  135. switch (stage) {
  136. case 0:
  137. {
  138. dlg_data.sbm.textContent = setStage(1);
  139. dlg_data.lat.disabled = true;
  140. const lat = dlg_data.lat.checked;
  141. Settings.set("fixlatin", lat);
  142. Settings.save();
  143. await getBookContent(fb2doc, dlg_data.log, { fixLat: lat });
  144. dlg_data.sbm.textContent = setStage(2);
  145. }
  146. break;
  147. case 1:
  148. FB2Loader.abortAll();
  149. dlg_data.sbm.textContent = setStage(3);
  150. break;
  151. case 2:
  152. if (!dlg_data.link) {
  153. dlg_data.link = document.createElement("a");
  154. dlg_data.link.download = genBookFileName(fb2doc);
  155. dlg_data.link.href = URL.createObjectURL(new Blob([ fb2doc ], { type: "application/octet-stream" }));
  156. dlg_data.fb2doc = null;
  157. }
  158. dlg_data.link.click();
  159. break;
  160. case 3:
  161. dlg_data.dlg.closest("div.popup[data-src=\"#rbe-download-dlg\"]").querySelector("button.popup__close").click();
  162. break;
  163. }
  164. } catch (err) {
  165. console.error(err);
  166. dlg_data.log.message(err.message, "red");
  167. dlg_data.sbm.textContent = setStage(3);
  168. }
  169. }
  170.  
  171. function setStage(new_stage) {
  172. stage = new_stage;
  173. return [ "Продолжить", "Прервать", "Сохранить в файл", "Закрыть" ][new_stage] || "Error";
  174. }
  175.  
  176. function getBookInfo(fb2doc, book_page, log) {
  177. // Id книги
  178. fb2doc.id = (() => {
  179. const el = book_page.querySelector("a.book-actions__button[href^=\"/chitat-online/\"]");
  180. if (el) {
  181. const id = (new URL(el)).searchParams.get("b");
  182. if (id) return id;
  183. }
  184. throw new Error("Не найден Id книги!");
  185. })();
  186. // Название книги
  187. const title = (() => {
  188. const el = book_page.querySelector("div.main-info>h1.main-info__title");
  189. return el && el.textContent.trim() || "";
  190. })();
  191. if (!title) throw new Error("Не найдено название книги");
  192. let li = log.message("Название:").text(title);
  193. fb2doc.bookTitle = title;
  194. // Авторы
  195. const authors = Array.from(book_page.querySelectorAll("div.main-info>a[href^=\"/avtor/\"]")).reduce((list, el) => {
  196. const content = el.textContent.trim();
  197. if (content) {
  198. const author = new FB2Author(content);
  199. author.homePage = el.href;
  200. list.push(author);
  201. }
  202. return list;
  203. }, []);
  204. log.message("Авторы:").text(authors.length || "нет");
  205. if (!authors.length) log.warning("Не найдена информация об авторах");
  206. fb2doc.bookAuthors = authors;
  207. // Жанры
  208. const genres = Array.from(book_page.querySelectorAll("div.book-info a[href^=\"/cat/\"]")).reduce((list, el) => {
  209. const content = el.textContent.trim();
  210. if (content) list.push(content);
  211. return list;
  212. }, []);
  213. fb2doc.genres = new FB2GenreList(genres);
  214. log.message("Жанры:").text(fb2doc.genres.length || "нет");
  215. // Ключевые слова
  216. fb2doc.keywords = Array.from(book_page.querySelectorAll("div.tags>ul.tags__list>li.tags__item")).reduce((list, el) => {
  217. const content = el.textContent.trim();
  218. const r = /^#(.+)$/.exec(content);
  219. if (r) list.push(r[1]);
  220. return list;
  221. }, []);
  222. log.message("Теги:").text(fb2doc.keywords.length || "нет");
  223. // Серия
  224. fb2doc.sequence = (() => {
  225. let el = book_page.querySelector("div.book-info a[href^=\"/serie/\"]");
  226. if (el) {
  227. let r = /^(.+?)(?:\s+#(\d+))?$/.exec(el.textContent.trim());
  228. if (r && r[1]) {
  229. const res = { name: r[1] };
  230. log.message("Серия:").text(r[1]);
  231. if (r[2]) {
  232. res.number = r[2];
  233. log.message("Номер в серии:").text(r[2]);
  234. }
  235. return res;
  236. }
  237. }
  238. return null;
  239. })();
  240. // Дата
  241. fb2doc.bookDate = (() => {
  242. const el = book_page.querySelector("ul.book-chars>li.book-chars__item");
  243. if (el) {
  244. const r = /^Размещено.+(\d{2})\.(\d{2})\.(\d{4})$/.exec(el.textContent.trim());
  245. if (r) {
  246. log.message("Последнее обновление:").text(`${r[1]}.${r[2]}.${r[3]}`);
  247. return new Date(`${r[3]}-${r[2]}-${r[1]}`);
  248. }
  249. }
  250. return null;
  251. })();
  252. // Ссылка на источник
  253. fb2doc.sourceURL = document.location.origin + document.location.pathname;
  254. // Аннотация
  255. fb2doc.annotation = (() => {
  256. const el = book_page.querySelector("article.seo__content");
  257. if (el && el.firstElementChild && el.firstElementChild.tagName === "H2" && el.firstElementChild.textContent === "Аннотация") {
  258. const c_el = el.cloneNode(true);
  259. c_el.firstElementChild.remove();
  260. return c_el;
  261. }
  262. log.warning("Аннотация не найдена!");
  263. return null;
  264. })();
  265. // Количество страниц
  266. fb2doc.pageCount = (() => {
  267. const li = log.message("Количество страниц:");
  268. const el = book_page.querySelector(".book-about__pages .button-pages__right");
  269. if (el) {
  270. const pages_str = el.textContent;
  271. let r = /^(\d+)/.exec(pages_str);
  272. if (r) {
  273. li.text(r[1]);
  274. return parseInt(r[1]);
  275. }
  276. }
  277. li.fail();
  278. return 0;
  279. })();
  280. // Обложка книги
  281. fb2doc.coverpageURL = (() => {
  282. const el = book_page.querySelector("div.book-image img");
  283. if (el) return el.src;
  284. return null;
  285. })();
  286. }
  287.  
  288. async function getBookContent(fb2doc, log, params) {
  289. let li = null;
  290. try {
  291. // Обложка книги
  292. if (fb2doc.coverpageURL) {
  293. li = log.message("Загрузка обложки...");
  294. fb2doc.coverpage = new FB2Image(fb2doc.coverpageURL);
  295. await fb2doc.coverpage.load((loaded, total) => li.text("" + Math.round(loaded / total * 100) + "%"));
  296. fb2doc.coverpage.id = "cover" + fb2doc.coverpage.suffix();
  297. fb2doc.binaries.push(fb2doc.coverpage);
  298. li.ok();
  299. li = null;
  300. log.message("Размер обложки:").text(fb2doc.coverpage.size + " байт");
  301. log.message("Тип файла обложки:").text(fb2doc.coverpage.type);
  302. } else {
  303. log.warning("Обложка книги не найдена!");
  304. }
  305. // Анализ аннотации
  306. if (fb2doc.annotation) {
  307. const li = log.message("Анализ аннотации...");
  308. fb2doc.bindParser("a", new ReadliFB2AnnotationParser());
  309. try {
  310. await fb2doc.parse("a", log, params, fb2doc.annotation);
  311. li.ok();
  312. if (!fb2doc.annotation) log.warning("Не найдено содержимое аннотации!");
  313. } catch (err) {
  314. li.fail();
  315. throw err;
  316. }
  317. }
  318. //--
  319. li = null;
  320. // Версия программы
  321. fb2doc.programName = GM_info.script.name + " v" + GM_info.script.version;
  322. //--
  323. log.message("---");
  324. // Страницы
  325. fb2doc.bindParser("n", new ReadliFB2NotesParser());
  326. fb2doc.bindParser("p", new ReadliFB2PageParser());
  327. const page_url = new URL("/chitat-online/", document.location);
  328. page_url.searchParams.set("b", fb2doc.id);
  329. for (let pn = 1; pn <= fb2doc.pageCount; ++pn) {
  330. li = log.message(`Получение страницы ${pn}/${fb2doc.pageCount}...`);
  331. page_url.searchParams.set("pg", pn);
  332. const page = getPageElement(await FB2Loader.addJob(page_url));
  333. if (pn !== 1 || ! await getAuthorNotes(fb2doc, page, log, params)) {
  334. await fb2doc.parse("p", log, params, page);
  335. }
  336. li.ok();
  337. }
  338. li = null;
  339. log.message("---");
  340. // Информация
  341. log.message("Всего глав:").text(fb2doc.chapters.length);
  342. if (fb2doc.unknowns) {
  343. log.warning(`Найдены неизвестные элементы: ${fb2doc.unknowns}`);
  344. log.message("Преобразованы в текст без форматирования");
  345. }
  346. if (params.fixLat) log.message("Заменено латинских букв:").text(fb2doc.latCount.toLocaleString());
  347. const icnt = fb2doc.binaries.reduce((cnt, img) => {
  348. if (!img.value) ++cnt;
  349. return cnt;
  350. }, 0);
  351. if (icnt) {
  352. log.warning(`Проблемы с загрузкой изображений: ${icnt}`);
  353. log.message("Проблемные изображения заменены на текст");
  354. }
  355. const webpList = fb2doc.binaries.reduce((list, bin) => {
  356. if (bin instanceof FB2Image && bin.type === "image/webp" && bin.value) list.push(bin);
  357. return list;
  358. }, []);
  359. if (webpList.length) {
  360. log.message("---");
  361. log.warning("Найдены изображения формата WebP. Могут быть проблемы с отображением на старых читалках.");
  362. await new Promise(resolve => setTimeout(resolve, 100)); // Чтобы лог успел обновиться
  363. if (confirm("Выполнить конвертацию WebP --> JPEG?")) {
  364. const li = log.message("Конвертация изображений...");
  365. let ecnt = 0;
  366. for (const img of webpList) {
  367. try {
  368. await img.convert("image/jpeg");
  369. } catch(err) {
  370. console.log(`Ошибка конвертации изображения: id=${img.id}; type=${img.type};`);
  371. ++ecnt;
  372. }
  373. }
  374. if (!ecnt) {
  375. li.ok();
  376. } else {
  377. li.fail();
  378. log.warning("Часть изображений не удалось сконвертировать!");
  379. }
  380. }
  381. }
  382. log.message("---");
  383. log.message("Готово!");
  384. } catch (err) {
  385. li && li.fail();
  386. fb2doc.bindParser();
  387. throw err;
  388. }
  389. }
  390.  
  391. async function getAuthorNotes(fb2doc, page, log, params) {
  392. const hdr = page.querySelector("section>subtitle");
  393. if (!hdr || hdr.textContent !== "Примечания автора:" || !hdr.nextSibling) return false;
  394. if (await fb2doc.parse("n", log, params, hdr.parentNode, hdr.nextSibling)) {
  395. log.message("Найдены примечания автора");
  396. return true;
  397. }
  398. return false;
  399. }
  400.  
  401. function getPageElement(html) {
  402. const doc = (new DOMParser()).parseFromString(html, "text/html");
  403. const page_el = doc.querySelector("article.reading__content>div.reading__text");
  404. if (!page_el) throw new Error("Ошибка анализа HTML страницы");
  405. return page_el;
  406. }
  407.  
  408. function genBookFileName(fb2doc) {
  409. function xtrim(s) {
  410. const r = /^[\s=\-_.,;!]*(.+?)[\s=\-_.,;!]*$/.exec(s);
  411. return r && r[1] || s;
  412. }
  413.  
  414. const parts = [];
  415. if (fb2doc.bookAuthors.length) parts.push(fb2doc.bookAuthors[0]);
  416. if (fb2doc.sequence) {
  417. let name = xtrim(fb2doc.sequence.name);
  418. if (fb2doc.sequence.number) {
  419. const num = fb2doc.sequence.number;
  420. name += (num.length < 2 ? "0" : "") + num;
  421. }
  422. parts.push(name);
  423. }
  424. parts.push(xtrim(fb2doc.bookTitle));
  425. let fname = (parts.join(". ") + " [RL-" + fb2doc.id + "]").replace(/[\0\/\\\"\*\?\<\>\|:]+/g, "");
  426. if (fname.length > 250) fname = fname.substr(0, 250);
  427. return fname + ".fb2";
  428. }
  429.  
  430. //---------- Классы ----------
  431.  
  432. class ReadliFB2Document extends FB2Document {
  433. constructor() {
  434. super();
  435. this.fixLat = false;
  436. this.latCount = 0;
  437. this.unknowns = 0;
  438. }
  439.  
  440. parse(parser_id, log, params, ...args) {
  441. const bin_start = this.binaries.length;
  442. this.fixLat = !!params.fixLat;
  443. const pdata = super.parse(parser_id, ...args);
  444. pdata.unknownNodes.forEach(el => {
  445. log.warning(`Найден неизвестный элемент: ${el.nodeName}`);
  446. ++this.unknowns;
  447. });
  448. if (pdata.latCount) this.latCount += pdata.latCount;
  449. const u_bin = this.binaries.slice(bin_start);
  450. return (async () => {
  451. const it = u_bin[Symbol.iterator]();
  452. const get_list = function() {
  453. const list = [];
  454. for (let i = 0; i < 5; ++i) {
  455. const r = it.next();
  456. if (r.done) break;
  457. list.push(r.value);
  458. }
  459. return list;
  460. };
  461. while (true) {
  462. const list = get_list();
  463. if (!list.length) break;
  464. await Promise.all(list.map(bin => {
  465. const li = log.message("Загрузка изображения...");
  466. if (!bin.url) {
  467. log.warning("Отсутствует ссылка");
  468. li.skipped();
  469. return Promise.resolve();
  470. }
  471. return bin.load((loaded, total) => li.text("" + Math.round(loaded / total * 100) + "%"))
  472. .then(() => li.ok())
  473. .catch((err) => {
  474. li.fail();
  475. if (err.name === "AbortError") throw err;
  476. });
  477. }));
  478. }
  479. return pdata.result;
  480. })();
  481. }
  482. }
  483.  
  484. class ReadliFB2Parser extends FB2Parser {
  485. constructor() {
  486. super();
  487. this._latinMap = new Map([
  488. [ "A", "А" ], [ "a", "а" ], [ "C", "С" ], [ "c", "с" ], [ "E", "Е" ], [ "e", "е" ],
  489. [ "M", "М" ], [ "O", "О" ], [ "o", "о" ], [ "P", "Р" ], [ "p", "р" ], [ "X", "Х"], [ "x", "х" ]
  490. ]);
  491. }
  492.  
  493. run(fb2doc, htmlNode, fromNode) {
  494. this._doc = fb2doc;
  495. this._unknown_nodes = [];
  496. this._lat_cnt = 0;
  497. // Предварительно вырезать элементы с заведомо бесполезным содержимым, чтобы оно не попадало в textContent во время проверки блоков.
  498. // Ноды страниц хранятся только в памяти, а нода аннотации клонируется, так что можно безопасно править переданную в параметре ноду.
  499. htmlNode.querySelectorAll("script").forEach(el => el.remove());
  500. // Запустить парсинг
  501. const res = super.parse(htmlNode, fromNode);
  502. const un = this._unknown_nodes;
  503. this._unknown_nodes = null;
  504. return { result: res, unknownNodes: un, latCount: this._lat_cnt };
  505. }
  506.  
  507. startNode(node, depth) {
  508. switch (node.nodeName) {
  509. case "DIV":
  510. case "INS":
  511. // Пропустить динамически подгружаемые рекламные блоки. Могут быть на 0 и 1 уровне вложения.
  512. // Поскольку изначально они пустые, то другие проверки можно не делать.
  513. if (!node.children.length && node.textContent.trim() === "") return null;
  514. break;
  515. case "SECTION":
  516. case "EMPTY-LINE":
  517. // Кривизна переноса текста книги из FB2-файла на сайт
  518. {
  519. const n = node.ownerDocument.createElement("p");
  520. while (node.firstChild) n.appendChild(node.firstChild);
  521. return n;
  522. }
  523. case "STRIKETHROUGH":
  524. // Элемент из формата FB2
  525. {
  526. const n = node.ownerDocument.createElement("strike");
  527. while (node.firstChild) n.appendChild(node.firstChild);
  528. return n;
  529. }
  530. }
  531. return node;
  532. }
  533.  
  534. processElement(fb2el, depth) {
  535. if (fb2el) {
  536. if (fb2el instanceof FB2Image) {
  537. this._doc.binaries.push(fb2el);
  538. } else if (fb2el instanceof FB2UnknownNode) {
  539. this._unknown_nodes.push(fb2el.value);
  540. } else if (this._doc.fixLat && typeof(fb2el.value) === "string") {
  541. fb2el.value = fb2el.value.replace(/([AaCcEeMOoPpXx]+)([ЁёА-Яа-я]?)/g, (match, p1, p2, offset, str) => {
  542. if (p1.length <= 3 || p2.length || (offset && /[ЁёА-Яа-я]/.test(str.at(offset - 1)))) {
  543. const a = [];
  544. for (const c of p1) a.push(this._latinMap.get(c));
  545. p1 = a.join("");
  546. this._lat_cnt += p1.length;
  547. }
  548. return `${p1}${p2}`;
  549. });
  550. }
  551. }
  552. return super.processElement(fb2el, depth);
  553. }
  554. }
  555.  
  556. class ReadliFB2AnnotationParser extends ReadliFB2Parser {
  557. run(fb2doc, htmlNode) {
  558. this._annotation = new FB2Annotation();
  559. const pdata = super.run(fb2doc, htmlNode);
  560. if (this._annotation.children.length) {
  561. this._annotation.normalize();
  562. } else {
  563. this._annotation = null;
  564. }
  565. fb2doc.annotation = this._annotation;
  566. return pdata;
  567. }
  568.  
  569. processElement(fb2el, depth) {
  570. if (fb2el && !depth) this._annotation.children.push(fb2el);
  571. return super.processElement(fb2el, depth);
  572. }
  573. }
  574.  
  575. class ReadliFB2NotesParser extends ReadliFB2Parser {
  576. run(fb2doc, htmlNode, fromNode) {
  577. this._annotation = new FB2Annotation();
  578. const pdata = super.run(fb2doc, htmlNode, fromNode);
  579. let n_ann = this._annotation;
  580. let d_ann = this._doc.annotation;
  581. if (n_ann.children.length) {
  582. n_ann.normalize();
  583. if (d_ann) {
  584. d_ann.children.push(new FB2EmptyLine());
  585. } else {
  586. d_ann = new FB2Annotation();
  587. }
  588. d_ann.children.push(new FB2Paragraph("Примечания автора:"));
  589. n_ann.children.forEach(ne => d_ann.children.push(ne));
  590. }
  591. this._doc.annotation = d_ann;
  592. pdata.result = (n_ann.children.length > 0);
  593. return pdata;
  594. }
  595.  
  596. startNode(node, depth) {
  597. if (depth === 0 && node.nodeName === "SUBTITLE") {
  598. this._stop = true;
  599. return null;
  600. }
  601. return super.startNode(node, depth);
  602. }
  603.  
  604. processElement(fb2el, depth) {
  605. if (fb2el && !depth) this._annotation.children.push(fb2el);
  606. return super.processElement(fb2el, depth);
  607. }
  608. }
  609.  
  610. class ReadliFB2PageParser extends ReadliFB2Parser {
  611. constructor() {
  612. super();
  613. this._chapter = null;
  614. }
  615.  
  616. run(fb2doc, htmlNode) {
  617. const pdata = super.run(fb2doc, htmlNode);
  618. if (this._chapter) this._chapter.normalize();
  619. return pdata;
  620. }
  621.  
  622. startNode(node, depth) {
  623. if (depth === 0) {
  624. switch (node.nodeName) {
  625. case "H3":
  626. // Нормализовать предыдущую главу
  627. if (this._chapter) this._chapter.normalize();
  628. // Удалить, если без заголовка и пустая.
  629. // Такое происходит из-за пустых блоков перед заголовком первой главы.
  630. if (!this._chapter.title && !this._chapter.children.length) this._doc.chapters.pop();
  631. // Добавить новую главу
  632. this._chapter = new FB2Chapter(node.textContent.trim());
  633. this._doc.chapters.push(this._chapter);
  634. return null;
  635. }
  636. }
  637. return super.startNode(node, depth);
  638. }
  639.  
  640. processElement(fb2el, depth) {
  641. if (fb2el && !depth) {
  642. if (!this._chapter) {
  643. this._chapter = new FB2Chapter();
  644. this._doc.chapters.push(this._chapter);
  645. }
  646. this._chapter.children.push(fb2el);
  647. }
  648. return super.processElement(fb2el, depth);
  649. }
  650. }
  651.  
  652. class LogElement {
  653. constructor(element) {
  654. element.style.padding = ".5em";
  655. element.style.fontSize = "90%";
  656. element.style.border = "1px solid lightgray";
  657. element.style.borderRadius = "6px";
  658. element.style.textAlign = "left";
  659. element.style.overflowY = "auto";
  660. element.style.maxHeight = "50vh";
  661. this._element = element;
  662. }
  663.  
  664. clean() {
  665. while (this._element.firstChild) this._element.lastChild.remove();
  666. }
  667.  
  668. message(message, color) {
  669. const item = document.createElement("div");
  670. if (message instanceof HTMLElement) {
  671. item.appendChild(message);
  672. } else {
  673. item.textContent = message;
  674. }
  675. if (color) item.style.color = color;
  676. this._element.appendChild(item);
  677. this._element.scrollTop = this._element.scrollHeight;
  678. return new LogItemElement(item);
  679. }
  680.  
  681. warning(s) {
  682. this.message(s, "#a00");
  683. }
  684. }
  685.  
  686. class LogItemElement {
  687. constructor(element) {
  688. this._element = element;
  689. this._span = null;
  690. }
  691.  
  692. ok() {
  693. this._setSpan("ok", "green");
  694. }
  695.  
  696. fail() {
  697. this._setSpan("ошибка!", "red");
  698. }
  699.  
  700. skipped() {
  701. this._setSpan("пропущено", "blue");
  702. }
  703.  
  704. text(s) {
  705. this._setSpan(s, "");
  706. }
  707.  
  708. _setSpan(text, color) {
  709. if (!this._span) {
  710. this._span = document.createElement("span");
  711. this._element.appendChild(this._span);
  712. }
  713. this._span.style.color = color;
  714. this._span.textContent = " " + text;
  715. }
  716. }
  717.  
  718. class Settings {
  719. static get(name, reset) {
  720. if (reset) Settings._values = null;
  721. this._ensureValues();
  722. let val = Settings._values[name];
  723. switch (name) {
  724. case "fixlatin":
  725. if (typeof(val) !== "boolean") val = false;
  726. break;
  727. }
  728. return val;
  729. }
  730.  
  731. static set(name, value) {
  732. this._ensureValues();
  733. this._values[name] = value;
  734. }
  735.  
  736. static save() {
  737. try {
  738. localStorage.setItem("rbe.settings", JSON.stringify(this._values || {}));
  739. } catch (err) {
  740. }
  741. }
  742.  
  743. static _ensureValues() {
  744. if (this._values) return;
  745. try {
  746. this._values = JSON.parse(localStorage.getItem("rbe.settings"));
  747. } catch (err) {
  748. this._values = null;
  749. }
  750. if (!this._values || typeof(this._values) !== "object") Settings._values = {};
  751. }
  752. }
  753.  
  754.  
  755. //-------------------------
  756.  
  757. // Запускает скрипт после загрузки страницы сайта
  758. if (document.readyState === "loading") window.addEventListener("DOMContentLoaded", init);
  759. else init();
  760.  
  761. })();

QingJ © 2025

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