ReadliBookExtractor

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

질문, 리뷰하거나, 이 스크립트를 신고하세요.
// ==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.greasyfork.org/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();

})();