ReadliBookExtractor

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name           ReadliBookExtractor
// @namespace      90h.yy.zz
// @version        0.8.2
// @author         Ox90
// @match          https://readli.net/*
// @description    The script adds a button to the site for downloading books to an FB2 file
// @description:ru Скрипт добавляет кнопку для скачивания книги в формате FB2
// @require        https://update.gf.qytechs.cn/scripts/468831/1478439/HTML2FB2Lib.js
// @grant          unsafeWindow
// @run-at         document-start
// @license        MIT
// ==/UserScript==

(function start() {

let env = {};
let stage = 0;

function init() {
  env.popupShow = window.popupShow || (unsafeWindow && unsafeWindow.popupShow);
  pageHandler();
}

async function pageHandler() {
  let book_doc = null;
  if (document.querySelector("a.book-actions__button[href^=\"/chitat-online/\"]")) {
    book_doc = document;
  } else if (document.querySelector("div.reading-end__content")) {
    const hdr = document.querySelector("h1>a");
    if (hdr && hdr.href) book_doc = await getBookOverview(hdr.href);
  }
  if (book_doc) {
    const book_page = book_doc.querySelector("main.main>section.wrapper.page");
    if (book_page) {
      const dlg_data = makeDownloadDialog();
      const btn_list = document.querySelector("section.download>ul.download__list");
      insertDownloadButton(book_page, dlg_data, btn_list);
    }
  }
}

async function getBookOverview(url) {
  return (new DOMParser()).parseFromString(await FB2Loader.addJob(url), "text/html");
}

function insertDownloadButton(book_page, dlg_data, btn_list) {
  // Создать кнопку
  const btn = document.createElement("li");
  btn.classList.add("download__item");
  const link = document.createElement("a");
  link.classList.add("download__link");
  link.href = "#";
  link.textContent = "fb2-ex";
  btn.appendChild(link);
  // Попытаться вставить новую кнопку сразу после оригинальной fb2
  let item = btn_list.firstElementChild;
  while (item) {
    if (item.textContent === "fb2") break;
    item = item.nextElementSibling;
  }
  if (item) {
    item.after(btn);
  } else {
    btn_list.appendChild(btn);
  }
  // Ссылка на данные книги
  let fb2doc = null;
  // Установить обработчик для новой кнопки
  btn.addEventListener("click", event => {
    event.preventDefault();
    try {
      fb2doc = new ReadliFB2Document();
      fb2doc.lang = "ru";
      fb2doc.idPrefix = "rdlbe_";
      dlg_data.log.clean();
      dlg_data.lat.disabled = false;
      dlg_data.lat.checked = Settings.get("fixlatin");
      dlg_data.sbm.textContent = setStage(0);
      env.popupShow("#rbe-download-dlg");
      getBookInfo(fb2doc, book_page, dlg_data.log);
    } catch (e) {
      dlg_data.log.message(e.message, "red");
      dlg_data.sbm.textContent = setStage(3);
    } finally {
      dlg_data.sbm.disabled = false;
    }
  });
  // Установить обработчик для основной кнопки диалога
  dlg_data.sbm.addEventListener("click", () => makeAction(fb2doc, dlg_data));
  // Установить обработчик для скрытия диалога
  dlg_data.dlg.addEventListener("dlg-hide", () => {
    if (dlg_data.link) {
      URL.revokeObjectURL(dlg_data.link.href);
      dlg_data.link = null;
    }
    fb2doc = null;
  });
}

function makeDownloadDialog() {
  const popups = document.querySelector("div.popups");
  if (!popups) throw new Error("Не найден блок popups");
  const dlg_c = document.createElement("div");
  dlg_c.id = "rbe-download-dlg";
  popups.appendChild(dlg_c);
  dlg_c.innerHTML =
    '<div class="popup" data-src="#rbe-download-dlg">' +
    '<button class="popup__close button-close-2"></button>' +
    '<div class="popup-form" style="display:flex; flex-direction:column; gap:.5em;">' +
    '<h2 style="margin:0; padding:0 0 .5em;">Скачать книгу</h2>' +
    '<div class="rbe-log"></div>' +
    '<label style="display:flex; gap:.5em; cursor:pointer;">' +
      '<input type="checkbox" name="fix_lat" style="appearance:auto;">Исправлять латиницу в тексте</label>' +
    '<button class="button rbe-submit" disabled="true">Продолжить</button>' +
    '</div>' +
    '</div>';
  const dlg = dlg_c.querySelector("div.popup-form");
  const dlg_data = {
    dlg: dlg,
    log: new LogElement(dlg.querySelector(".rbe-log")),
    lat: dlg.querySelector("input[name=fix_lat]"),
    sbm: dlg.querySelector("button.rbe-submit")
  };
  (new MutationObserver(() => {
    if (dlg_c.children.length) {
      dlg.dispatchEvent(new CustomEvent("dlg-hide"));
    }
  })).observe(dlg_c, { childList: true });
  return dlg_data;
}

async function makeAction(fb2doc, dlg_data) {
  try {
    switch (stage) {
      case 0:
        {
          dlg_data.sbm.textContent = setStage(1);
          dlg_data.lat.disabled = true;
          const lat = dlg_data.lat.checked;
          Settings.set("fixlatin", lat);
          Settings.save();
          await getBookContent(fb2doc, dlg_data.log, { fixLat: lat });
          dlg_data.sbm.textContent = setStage(2);
        }
        break;
      case 1:
        FB2Loader.abortAll();
        dlg_data.sbm.textContent = setStage(3);
        break;
      case 2:
        if (!dlg_data.link) {
          dlg_data.link = document.createElement("a");
          dlg_data.link.download = genBookFileName(fb2doc);
          dlg_data.link.href = URL.createObjectURL(new Blob([ fb2doc ], { type: "application/octet-stream" }));
          dlg_data.fb2doc = null;
        }
        dlg_data.link.click();
        break;
      case 3:
        dlg_data.dlg.closest("div.popup[data-src=\"#rbe-download-dlg\"]").querySelector("button.popup__close").click();
        break;
    }
  } catch (err) {
    console.error(err);
    dlg_data.log.message(err.message, "red");
    dlg_data.sbm.textContent = setStage(3);
  }
}

function setStage(new_stage) {
  stage = new_stage;
  return [ "Продолжить", "Прервать", "Сохранить в файл", "Закрыть" ][new_stage] || "Error";
}

function getBookInfo(fb2doc, book_page, log) {
  // Id книги
  fb2doc.id = (() => {
    const el = book_page.querySelector("a.book-actions__button[href^=\"/chitat-online/\"]");
    if (el) {
      const id = (new URL(el)).searchParams.get("b");
      if (id) return id;
    }
    throw new Error("Не найден Id книги!");
  })();
  // Название книги
  const title = (() => {
    const el = book_page.querySelector("div.main-info>h1.main-info__title");
    return el && el.textContent.trim() || "";
  })();
  if (!title) throw new Error("Не найдено название книги");
  let li = log.message("Название:").text(title);
  fb2doc.bookTitle = title;
  // Авторы
  const authors = Array.from(book_page.querySelectorAll("div.main-info>a[href^=\"/avtor/\"]")).reduce((list, el) => {
    const content = el.textContent.trim();
    if (content) {
      const author = new FB2Author(content);
      author.homePage = el.href;
      list.push(author);
    }
    return list;
  }, []);
  log.message("Авторы:").text(authors.length || "нет");
  if (!authors.length) log.warning("Не найдена информация об авторах");
  fb2doc.bookAuthors = authors;
  // Жанры
  const genres = Array.from(book_page.querySelectorAll("div.book-info a[href^=\"/cat/\"]")).reduce((list, el) => {
    const content = el.textContent.trim();
    if (content) list.push(content);
    return list;
  }, []);
  fb2doc.genres = new FB2GenreList(genres);
  log.message("Жанры:").text(fb2doc.genres.length || "нет");
  // Ключевые слова
  fb2doc.keywords = Array.from(book_page.querySelectorAll("div.tags>ul.tags__list>li.tags__item")).reduce((list, el) => {
    const content = el.textContent.trim();
    const r = /^#(.+)$/.exec(content);
    if (r) list.push(r[1]);
    return list;
  }, []);
  log.message("Теги:").text(fb2doc.keywords.length || "нет");
  // Серия
  fb2doc.sequence = (() => {
    let el = book_page.querySelector("div.book-info a[href^=\"/serie/\"]");
    if (el) {
      let r = /^(.+?)(?:\s+#(\d+))?$/.exec(el.textContent.trim());
      if (r && r[1]) {
        const res = { name: r[1] };
        log.message("Серия:").text(r[1]);
        if (r[2]) {
          res.number = r[2];
          log.message("Номер в серии:").text(r[2]);
        }
        return res;
      }
    }
    return null;
  })();
  // Дата
  fb2doc.bookDate = (() => {
    const el = book_page.querySelector("ul.book-chars>li.book-chars__item");
    if (el) {
      const r = /^Размещено.+(\d{2})\.(\d{2})\.(\d{4})$/.exec(el.textContent.trim());
      if (r) {
        log.message("Последнее обновление:").text(`${r[1]}.${r[2]}.${r[3]}`);
        return new Date(`${r[3]}-${r[2]}-${r[1]}`);
      }
    }
    return null;
  })();
  // Ссылка на источник
  fb2doc.sourceURL = document.location.origin + document.location.pathname;
  // Аннотация
  fb2doc.annotation = (() => {
    const el = book_page.querySelector("article.seo__content");
    if (el && el.firstElementChild && el.firstElementChild.tagName === "H2" && el.firstElementChild.textContent === "Аннотация") {
      const c_el = el.cloneNode(true);
      c_el.firstElementChild.remove();
      return c_el;
    }
    log.warning("Аннотация не найдена!");
    return null;
  })();
  // Количество страниц
  fb2doc.pageCount = (() => {
    const li = log.message("Количество страниц:");
    const el = book_page.querySelector(".book-about__pages .button-pages__right");
    if (el) {
      const pages_str = el.textContent;
      let r = /^(\d+)/.exec(pages_str);
      if (r) {
        li.text(r[1]);
        return parseInt(r[1]);
      }
    }
    li.fail();
    return 0;
  })();
  // Обложка книги
  fb2doc.coverpageURL = (() => {
    const el = book_page.querySelector("div.book-image img");
    if (el) return el.src;
    return null;
  })();
}

async function getBookContent(fb2doc, log, params) {
  let li = null;
  try {
    // Обложка книги
    if (fb2doc.coverpageURL) {
      li = log.message("Загрузка обложки...");
      fb2doc.coverpage = new FB2Image(fb2doc.coverpageURL);
      await fb2doc.coverpage.load((loaded, total) => li.text("" + Math.round(loaded / total * 100) + "%"));
      fb2doc.coverpage.id = "cover" + fb2doc.coverpage.suffix();
      fb2doc.binaries.push(fb2doc.coverpage);
      li.ok();
      li = null;
      log.message("Размер обложки:").text(fb2doc.coverpage.size + " байт");
      log.message("Тип файла обложки:").text(fb2doc.coverpage.type);
    } else {
      log.warning("Обложка книги не найдена!");
    }
    // Анализ аннотации
    if (fb2doc.annotation) {
      const li = log.message("Анализ аннотации...");
      fb2doc.bindParser("a", new ReadliFB2AnnotationParser());
      try {
        await fb2doc.parse("a", log, params, fb2doc.annotation);
        li.ok();
        if (!fb2doc.annotation) log.warning("Не найдено содержимое аннотации!");
      } catch (err) {
        li.fail();
        throw err;
      }
    }
    //--
    li = null;
    // Версия программы
    fb2doc.programName = GM_info.script.name + " v" + GM_info.script.version;
    //--
    log.message("---");
    // Страницы
    fb2doc.bindParser("n", new ReadliFB2NotesParser());
    fb2doc.bindParser("p", new ReadliFB2PageParser());
    const page_url = new URL("/chitat-online/", document.location);
    page_url.searchParams.set("b", fb2doc.id);
    for (let pn = 1; pn <= fb2doc.pageCount; ++pn) {
      li = log.message(`Получение страницы ${pn}/${fb2doc.pageCount}...`);
      page_url.searchParams.set("pg", pn);
      const page = getPageElement(await FB2Loader.addJob(page_url));
      if (pn !== 1 || ! await getAuthorNotes(fb2doc, page, log, params)) {
        await fb2doc.parse("p", log, params, page);
      }
      li.ok();
    }
    li = null;
    log.message("---");
    // Информация
    log.message("Всего глав:").text(fb2doc.chapters.length);
    if (fb2doc.unknowns) {
      log.warning(`Найдены неизвестные элементы: ${fb2doc.unknowns}`);
      log.message("Преобразованы в текст без форматирования");
    }
    if (params.fixLat) log.message("Заменено латинских букв:").text(fb2doc.latCount.toLocaleString());
    const icnt = fb2doc.binaries.reduce((cnt, img) => {
      if (!img.value) ++cnt;
      return cnt;
    }, 0);
    if (icnt) {
      log.warning(`Проблемы с загрузкой изображений: ${icnt}`);
      log.message("Проблемные изображения заменены на текст");
    }
    const webpList = fb2doc.binaries.reduce((list, bin) => {
      if (bin instanceof FB2Image && bin.type === "image/webp" && bin.value) list.push(bin);
      return list;
    }, []);
    if (webpList.length) {
      log.message("---");
      log.warning("Найдены изображения формата WebP. Могут быть проблемы с отображением на старых читалках.");
      await new Promise(resolve => setTimeout(resolve, 100)); // Чтобы лог успел обновиться
      if (confirm("Выполнить конвертацию WebP --> JPEG?")) {
        const li = log.message("Конвертация изображений...");
        let ecnt = 0;
        for (const img of webpList) {
          try {
            await img.convert("image/jpeg");
          } catch(err) {
            console.log(`Ошибка конвертации изображения: id=${img.id}; type=${img.type};`);
            ++ecnt;
          }
        }
        if (!ecnt) {
          li.ok();
        } else {
          li.fail();
          log.warning("Часть изображений не удалось сконвертировать!");
        }
      }
    }
    log.message("---");
    log.message("Готово!");
  } catch (err) {
    li && li.fail();
    fb2doc.bindParser();
    throw err;
  }
}

async function getAuthorNotes(fb2doc, page, log, params) {
  const hdr = page.querySelector("section>subtitle");
  if (!hdr || hdr.textContent !== "Примечания автора:" || !hdr.nextSibling) return false;
  if (await fb2doc.parse("n", log, params, hdr.parentNode, hdr.nextSibling)) {
    log.message("Найдены примечания автора");
    return true;
  }
  return false;
}

function getPageElement(html) {
  const doc = (new DOMParser()).parseFromString(html, "text/html");
  const page_el = doc.querySelector("article.reading__content>div.reading__text");
  if (!page_el) throw new Error("Ошибка анализа HTML страницы");
  return page_el;
}

function genBookFileName(fb2doc) {
  function xtrim(s) {
    const r = /^[\s=\-_.,;!]*(.+?)[\s=\-_.,;!]*$/.exec(s);
    return r && r[1] || s;
  }

  const parts = [];
  if (fb2doc.bookAuthors.length) parts.push(fb2doc.bookAuthors[0]);
  if (fb2doc.sequence) {
    let name = xtrim(fb2doc.sequence.name);
    if (fb2doc.sequence.number) {
      const num = fb2doc.sequence.number;
      name += (num.length < 2 ? "0" : "") + num;
    }
    parts.push(name);
  }
  parts.push(xtrim(fb2doc.bookTitle));
  let fname = (parts.join(". ") + " [RL-" + fb2doc.id + "]").replace(/[\0\/\\\"\*\?\<\>\|:]+/g, "");
  if (fname.length > 250) fname = fname.substr(0, 250);
  return fname + ".fb2";
}

//---------- Классы ----------

class ReadliFB2Document extends FB2Document {
  constructor() {
    super();
    this.fixLat = false;
    this.latCount = 0;
    this.unknowns = 0;
  }

  parse(parser_id, log, params, ...args) {
    const bin_start = this.binaries.length;
    this.fixLat = !!params.fixLat;
    const pdata = super.parse(parser_id, ...args);
    pdata.unknownNodes.forEach(el => {
      log.warning(`Найден неизвестный элемент: ${el.nodeName}`);
      ++this.unknowns;
    });
    if (pdata.latCount) this.latCount += pdata.latCount;
    const u_bin = this.binaries.slice(bin_start);
    return (async () => {
      const it = u_bin[Symbol.iterator]();
      const get_list = function() {
        const list = [];
        for (let i = 0; i < 5; ++i) {
          const r = it.next();
          if (r.done) break;
          list.push(r.value);
        }
        return list;
      };
      while (true) {
        const list = get_list();
        if (!list.length) break;
        await Promise.all(list.map(bin => {
          const li = log.message("Загрузка изображения...");
          if (!bin.url) {
            log.warning("Отсутствует ссылка");
            li.skipped();
            return Promise.resolve();
          }
          return bin.load((loaded, total) => li.text("" + Math.round(loaded / total * 100) + "%"))
            .then(() => li.ok())
            .catch((err) => {
              li.fail();
              if (err.name === "AbortError") throw err;
            });
        }));
      }
      return pdata.result;
    })();
  }
}

class ReadliFB2Parser extends FB2Parser {
  constructor() {
    super();
    this._latinMap = new Map([
      [ "A", "А" ], [ "a", "а" ], [ "C", "С" ], [ "c", "с" ], [ "E", "Е" ], [ "e", "е" ],
      [ "M", "М" ], [ "O", "О" ], [ "o", "о" ], [ "P", "Р" ], [ "p", "р" ], [ "X", "Х"], [ "x", "х" ]
    ]);
  }

  run(fb2doc, htmlNode, fromNode) {
    this._doc = fb2doc;
    this._unknown_nodes = [];
    this._lat_cnt = 0;
    // Предварительно вырезать элементы с заведомо бесполезным содержимым, чтобы оно не попадало в textContent во время проверки блоков.
    // Ноды страниц хранятся только в памяти, а нода аннотации клонируется, так что можно безопасно править переданную в параметре ноду.
    htmlNode.querySelectorAll("script").forEach(el => el.remove());
    // Запустить парсинг
    const res = super.parse(htmlNode, fromNode);
    const un = this._unknown_nodes;
    this._unknown_nodes = null;
    return { result: res, unknownNodes: un, latCount: this._lat_cnt };
  }

  startNode(node, depth) {
    switch (node.nodeName) {
      case "DIV":
      case "INS":
        // Пропустить динамически подгружаемые рекламные блоки. Могут быть на 0 и 1 уровне вложения.
        // Поскольку изначально они пустые, то другие проверки можно не делать.
        if (!node.children.length && node.textContent.trim() === "") return null;
        break;
      case "SECTION":
      case "EMPTY-LINE":
        // Кривизна переноса текста книги из FB2-файла на сайт
        {
          const n = node.ownerDocument.createElement("p");
          while (node.firstChild) n.appendChild(node.firstChild);
          return n;
        }
      case "STRIKETHROUGH":
        // Элемент из формата FB2
        {
          const n = node.ownerDocument.createElement("strike");
          while (node.firstChild) n.appendChild(node.firstChild);
          return n;
        }
    }
    return node;
  }

  processElement(fb2el, depth) {
    if (fb2el) {
      if (fb2el instanceof FB2Image) {
        this._doc.binaries.push(fb2el);
      } else if (fb2el instanceof FB2UnknownNode) {
        this._unknown_nodes.push(fb2el.value);
      } else if (this._doc.fixLat && typeof(fb2el.value) === "string") {
        fb2el.value = fb2el.value.replace(/([AaCcEeMOoPpXx]+)([ЁёА-Яа-я]?)/g, (match, p1, p2, offset, str) => {
          if (p1.length <= 3 || p2.length || (offset && /[ЁёА-Яа-я]/.test(str.at(offset - 1)))) {
            const a = [];
            for (const c of p1) a.push(this._latinMap.get(c));
            p1 = a.join("");
            this._lat_cnt += p1.length;
          }
          return `${p1}${p2}`;
        });
      }
    }
    return super.processElement(fb2el, depth);
  }
}

class ReadliFB2AnnotationParser extends ReadliFB2Parser {
  run(fb2doc, htmlNode) {
    this._annotation = new FB2Annotation();
    const pdata = super.run(fb2doc, htmlNode);
    if (this._annotation.children.length) {
      this._annotation.normalize();
    } else {
      this._annotation = null;
    }
    fb2doc.annotation = this._annotation;
    return pdata;
  }

  processElement(fb2el, depth) {
    if (fb2el && !depth) this._annotation.children.push(fb2el);
    return super.processElement(fb2el, depth);
  }
}

class ReadliFB2NotesParser extends ReadliFB2Parser {
  run(fb2doc, htmlNode, fromNode) {
    this._annotation = new FB2Annotation();
    const pdata = super.run(fb2doc, htmlNode, fromNode);
    let n_ann = this._annotation;
    let d_ann = this._doc.annotation;
    if (n_ann.children.length) {
      n_ann.normalize();
      if (d_ann) {
        d_ann.children.push(new FB2EmptyLine());
      } else {
        d_ann = new FB2Annotation();
      }
      d_ann.children.push(new FB2Paragraph("Примечания автора:"));
      n_ann.children.forEach(ne => d_ann.children.push(ne));
    }
    this._doc.annotation = d_ann;
    pdata.result = (n_ann.children.length > 0);
    return pdata;
  }

  startNode(node, depth) {
    if (depth === 0 && node.nodeName === "SUBTITLE") {
      this._stop = true;
      return null;
    }
    return super.startNode(node, depth);
  }

  processElement(fb2el, depth) {
    if (fb2el && !depth) this._annotation.children.push(fb2el);
    return super.processElement(fb2el, depth);
  }
}

class ReadliFB2PageParser extends ReadliFB2Parser {
  constructor() {
    super();
    this._chapter = null;
  }

  run(fb2doc, htmlNode) {
    const pdata = super.run(fb2doc, htmlNode);
    if (this._chapter) this._chapter.normalize();
    return pdata;
  }

  startNode(node, depth) {
    if (depth === 0) {
      switch (node.nodeName) {
        case "H3":
          // Нормализовать предыдущую главу
          if (this._chapter) this._chapter.normalize();
          // Удалить, если без заголовка и пустая.
          // Такое происходит из-за пустых блоков перед заголовком первой главы.
          if (!this._chapter.title && !this._chapter.children.length) this._doc.chapters.pop();
          // Добавить новую главу
          this._chapter = new FB2Chapter(node.textContent.trim());
          this._doc.chapters.push(this._chapter);
          return null;
      }
    }
    return super.startNode(node, depth);
  }

  processElement(fb2el, depth) {
    if (fb2el && !depth) {
      if (!this._chapter) {
        this._chapter = new FB2Chapter();
        this._doc.chapters.push(this._chapter);
      }
      this._chapter.children.push(fb2el);
    }
    return super.processElement(fb2el, depth);
  }
}

class LogElement {
  constructor(element) {
    element.style.padding = ".5em";
    element.style.fontSize = "90%";
    element.style.border = "1px solid lightgray";
    element.style.borderRadius = "6px";
    element.style.textAlign = "left";
    element.style.overflowY = "auto";
    element.style.maxHeight = "50vh";
    this._element = element;
  }

  clean() {
    while (this._element.firstChild) this._element.lastChild.remove();
  }

  message(message, color) {
    const item = document.createElement("div");
    if (message instanceof HTMLElement) {
      item.appendChild(message);
    } else {
      item.textContent = message;
    }
    if (color) item.style.color = color;
    this._element.appendChild(item);
    this._element.scrollTop = this._element.scrollHeight;
    return new LogItemElement(item);
  }

  warning(s) {
    this.message(s, "#a00");
  }
}

class LogItemElement {
  constructor(element) {
    this._element = element;
    this._span = null;
  }

  ok() {
    this._setSpan("ok", "green");
  }

  fail() {
    this._setSpan("ошибка!", "red");
  }

  skipped() {
    this._setSpan("пропущено", "blue");
  }

  text(s) {
    this._setSpan(s, "");
  }

  _setSpan(text, color) {
    if (!this._span) {
      this._span = document.createElement("span");
      this._element.appendChild(this._span);
    }
    this._span.style.color = color;
    this._span.textContent = " " + text;
  }
}

class Settings {
  static get(name, reset) {
    if (reset) Settings._values = null;
    this._ensureValues();
    let val = Settings._values[name];
    switch (name) {
      case "fixlatin":
        if (typeof(val) !== "boolean") val = false;
        break;
    }
    return val;
  }

  static set(name, value) {
    this._ensureValues();
    this._values[name] = value;
  }

  static save() {
    try {
      localStorage.setItem("rbe.settings", JSON.stringify(this._values || {}));
    } catch (err) {
    }
  }

  static _ensureValues() {
    if (this._values) return;
    try {
      this._values = JSON.parse(localStorage.getItem("rbe.settings"));
    } catch (err) {
      this._values = null;
    }
    if (!this._values || typeof(this._values) !== "object") Settings._values = {};
  }
}


//-------------------------

// Запускает скрипт после загрузки страницы сайта
if (document.readyState === "loading") window.addEventListener("DOMContentLoaded", init);
  else init();

})();

QingJ © 2025

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