ReadliBookExtractor

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

目前为 2023-06-10 提交的版本。查看 最新版本

// ==UserScript==
// @name           ReadliBookExtractor
// @namespace      90h.yy.zz
// @version        0.1.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
// @grant          unsafeWindow
// @run-at         document-start
// @license        MIT
// ==/UserScript==

(function start() {

const PROGRAM_NAME = "RLBookExtractor";

let env = {};
let stage = 0;

Date.prototype.toAtomDate = function() {
  let m = this.getMonth() + 1;
  let d = this.getDate();
  return "" + this.getFullYear() + '-' + (m < 10 ? "0" : "") + m + "-" + (d < 10 ? "0" : "") + d;
};

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

function pageHandler() {
  if (!document.querySelector("a.book-actions__button[href^=\"/chitat-online/\"]")) return;
  const book_page = document.querySelector("main.main>section.wrapper.page");
  if (book_page) {
    const dlg_data = makeDownloadDialog();
    insertDownloadButton(book_page, dlg_data);
  }
}

function insertDownloadButton(book_page, dlg_data) {
  const btn_list = book_page.querySelector("section.download>ul.download__list");
  if (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 book_data = null;
    // Установить обработчик для новой кнопки
    btn.addEventListener("click", event => {
      event.preventDefault();
      try {
        dlg_data.log.clean();
        dlg_data.sbm.textContent = setStage(0);
        env.popupShow("#rbe-download-dlg");
        book_data = getBookInfo(book_page, dlg_data.log);
        dlg_data.sbm.disabled = false;
      } catch (e) {
        dlg_data.log.message(e.message, "red");
      }
    });
    // Установить обработчик для основной кнопки диалога
    dlg_data.sbm.addEventListener("click", () => makeAction(book_data, dlg_data));
    // Установить обработчик для скрытия диалога
    dlg_data.dlg.addEventListener("dlg-hide", () => {
      if (dlg_data.link) {
        URL.revokeObjectURL(dlg_data.link.href);
        dlg_data.link = null;
      }
      book_data = 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">' +
    '<h2>Скачать книгу</h2>' +
    '<div class="rbe-log"></div>' +
    '<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")),
    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(book_data, dlg_data) {
  try {
    switch (stage) {
      case 0:
        dlg_data.sbm.textContent = setStage(1);
        await getBookContent(book_data, dlg_data.log);
        dlg_data.sbm.textContent = setStage(2);
        break;
      case 1:
        Fetcher.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(book_data.fb2doc);
          dlg_data.link.href = URL.createObjectURL(new Blob([ book_data.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) {
    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(book_page, log) {
  const data = {};
  // Id книги
  const 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 книги!");
  })();
  data.id = 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);
  data.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("Не найдена информация об авторах");
  data.authors = authors;
  // Жанры
  //
  log.message("Жанры:").text("не реализовано");
  // Ключевые слова
  const tags = 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(tags && tags.length || "нет");
  data.keywords = tags;
  // Серия
  const 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;
      }
    }
  })();
  if (sequence) data.sequence = sequence;
  // Дата
  const 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]}`);
      }
    }
  })();
  if (bookDate) data.bookDate = bookDate;
  // Ссылка на источник
  data.sourceURL = document.location.origin + document.location.pathname;
  // Аннотация
  const 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;
    }
  })();
  if (annotation) {
    data.annotation = annotation;
  } else {
    log.warning("Аннотация не найдена!");
  }
  // Количество страниц
  const pages = (() => {
    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;
  })();
  if (pages) data.pageCount = pages;
  // Обложка книги
  const cover_url = (() => {
    const el = book_page.querySelector("div.book-image img");
    if (el) return el.src;
    return null;
  })();
  if (cover_url) data.coverpageURL = cover_url;
  //--
  return data;
}

async function getBookContent(book_data, log) {
  let li = null;
  try {
    const fb2doc = new FB2Document();
    fb2doc.id = book_data.id;
    fb2doc.bookTitle = book_data.bookTitle;
    fb2doc.bookAuthors = book_data.authors;
    if (book_data.genres) {
      fb2doc.genres = book_data.genres;
    } else {
      fb2doc.genres = [ new FB2Element("genre", "network_literature") ];
    }
    if (book_data.sequence) fb2doc.sequence = book_data.sequence;
    fb2doc.lang = "ru";
    // Обложка книги
    if (book_data.coverpageURL) {
      li = log.message("Загрузка обложки...");
      try {
        fb2doc.coverpage = new FB2Image(book_data.coverpageURL);
        await fb2doc.coverpage.load();
        fb2doc.coverpage.id = "cover" + fb2doc.coverpage.suffix();
        fb2doc.binaries.push(fb2doc.coverpage);
        li.ok();
        log.message("Размер обложки:").text(fb2doc.coverpage.size + " байт");
        log.message("Тип файла обложки:").text(fb2doc.coverpage.type);
      } catch (err) {
        li.fail();
        throw err;
      }
    } else {
      log.warning("Обложка книги не найдена!");
    }
    // Анализ аннотации
    if (book_data.annotation) {
      const li = log.message("Анализ аннотации...");
      try {
        const annotation = new FB2Annotation();
        await annotation.setContentFromHTML(book_data.annotation);
        annotation.normalize();
        li.ok();
        if (annotation.children.length) {
          fb2doc.annotation = annotation;
        } else {
          log.warning("Не найдено содержимое аннотации!");
        }
      } catch (err) {
        li.fail();
        throw err;
      }
    }
    //--
    li = null;
    if (book_data.keywords.length) fb2doc.keywords = new FB2Element("keywords", book_data.keywords.join(", "));
    if (book_data.bookDate) fb2doc.bookDate = book_data.bookDate;
    fb2doc.sourceURL = book_data.sourceURL;
    //--
    log.message("---");
    // Страницы
    const page_url = new URL("/chitat-online/", document.location);
    page_url.searchParams.set("b", book_data.id);
    for (let pn = 1; pn <= book_data.pageCount; ++pn) {
      li = log.message(`Получение страницы ${pn}/${book_data.pageCount}...`);
      page_url.searchParams.set("pg", pn);
      const page = getPageElement(await Fetcher.addJob(page_url));
      if (pn !== 1 || ! await getAuthorNotes(fb2doc, page, log)) {
        await updateChapters(fb2doc, page, log);
      }
      li.ok();
    }
    log.message("---");
    log.message("Всего глав:").text(fb2doc.chapters.length);
    li = log.message("Анализ содержимого глав...");
    fb2doc.chapters.forEach(ch => ch.normalize());
    li.ok();
    log.message("---");
    log.message("Готово!");
    //--
    book_data.fb2doc = fb2doc;
  } catch (err) {
    li && li.fail();
    throw err;
  }
}

async function getAuthorNotes(fb2doc, page, log) {
  const hdr = page.querySelector("section>subtitle");
  if (!hdr || hdr.textContent !== "Примечания автора:") return false;
  let notes = null;
  let annot = new FB2Annotation();
  annot.children.push(new FB2Paragraph("Примечания автора:"));
  for (let notes = hdr.nextElementSibling; notes; notes = notes.nextElementSibling) {
    const tname = notes.tagName;
    if (tname === "SUBTITLE") break;
    if (tname === "P") {
      if (annot.children.length) annot.children.push(new FB2EmptyLine());
      await annot.appendContentFromHTML(notes, fb2doc, log);
    }
  }
  if (!annot.children.length) return false;
  log.message("Найдены примечания автора");
  annot.normalize();
  if (fb2doc.annotation) {
    fb2doc.annotation.children.push(new FB2EmptyLine());
    annot.children.forEach(el => fb2doc.annotation.children.push(el));
  } else {
    fb2doc.annotation = annot;
  }
  return true;
}

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;
}

async function updateChapters(fb2doc, page, log) {
  // Вырезать скрипты и рекламные блоки
  Array.from(page.children).forEach(el => {
    const tn = el.tagName;
    if ((tn === "DIV" && el.textContent.trim() === "") || tn === "SCRIPT" || tn === "INS") el.remove();
  });
  // Вырезать пустые блоки в начале страницы
  while (page.firstChild && !page.firstChild.textContent.trim()) {
    page.firstChild.remove();
  }
  if (!page.childNodes.length) return;
  //--
  if (page.firstChild.nodeName === "H3") {
    // Найдено название главы
    const title = page.firstChild.textContent.trim();
    page.firstChild.remove();
    const chapter = new FB2Chapter(title);
    await chapter.setContentFromHTML(page, fb2doc, log);
    fb2doc.chapters.push(chapter);
  } else {
    if (!fb2doc.chapters.length) fb2doc.chapters.push(new FB2Chapter());
    await fb2doc.chapters[fb2doc.chapters.length - 1].appendContentFromHTML(page, fb2doc, log);
  }
    
}

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 FB2Document {
  constructor() {
    this.binaries = [];
    this.bookAuthors = [];
    this.genres = [];
    this.chapters = [];
    this.xmldoc = null;
  }

  toString() {
    this._ensureXMLDocument();
    const root = this.xmldoc.documentElement;
    this.markBinaries();
    root.appendChild(this._makeDescriptionElement());
    root.appendChild(this._makeBodyElement());
    this._makeBinaryElements().forEach(el => root.appendChild(el));
    const res = (new XMLSerializer()).serializeToString(this.xmldoc);
    this.xmldoc = null;
    return res;
  }

  markBinaries() {
    let idx = 0;
    this.binaries.forEach(img => {
      if (!img.id) img.id = "image" + (++idx) + img.suffix();
    });
  }

  createElement(name) {
    this._ensureXMLDocument();
    return this.xmldoc.createElementNS(this.xmldoc.documentElement.namespaceURI, name);
  }

  createTextNode(value) {
    this._ensureXMLDocument();
    return this.xmldoc.createTextNode(value);
  }

  _ensureXMLDocument() {
    if (!this.xmldoc) {
      this.xmldoc = new DOMParser().parseFromString(
        '<?xml version="1.0" encoding="UTF-8"?><FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0"/>',
        "application/xml"
      );
      this.xmldoc.documentElement.setAttribute("xmlns:l", "http://www.w3.org/1999/xlink");
    }
  }

  _makeDescriptionElement() {
    const desc = this.createElement("description");
    // title-info
    const t_info = this.createElement("title-info");
    desc.appendChild(t_info);
    this.genres.forEach(g => t_info.appendChild(g.xml(this)));
    (this.bookAuthors.length ? this.bookAuthors : [ new FB2Author("Неизвестный автор") ]).forEach(a => {
      t_info.appendChild(a.xml(this));
    });
    t_info.appendChild((new FB2Element("book-title", this.bookTitle)).xml(this));
    t_info.appendChild(this.annotation.xml(this));
    if (this.keywords) t_info.appendChild(this.keywords.xml(this));
    if (this.bookDate) {
      const el = this.createElement("date");
      el.setAttribute("value", this.bookDate.toAtomDate());
      el.textContent = this.bookDate.getFullYear();
      t_info.appendChild(el);
    }
    if (this.coverpage) {
      const el = this.createElement("coverpage");
      el.appendChild(this.coverpage.xml(this));
      t_info.appendChild(el);
    }
    const lang = this.createElement("lang");
    lang.textContent = "ru";
    t_info.appendChild(lang);
    if (this.sequence) {
      const el = this.createElement("sequence");
      el.setAttribute("name", this.sequence.name);
      if (this.sequence.number) el.setAttribute("number", this.sequence.number);
      t_info.appendChild(el);
    }
    // document-info
    const d_info = this.createElement("document-info");
    desc.appendChild(d_info);
    d_info.appendChild((new FB2Author("Ox90")).xml(this));
    d_info.appendChild((new FB2Element("program-used", PROGRAM_NAME + " v" + GM_info.script.version)).xml(this));
    d_info.appendChild((() => {
      const f_time = new Date();
      const el = this.createElement("date");
      el.setAttribute("value", f_time.toAtomDate());
      el.textContent = f_time.toUTCString();
      return el;
    })());
    if (this.sourceURL) {
      d_info.appendChild((new FB2Element("src-url", this.sourceURL)).xml(this));
    }
    d_info.appendChild((new FB2Element("id", this._genBookId())).xml(this));
    d_info.appendChild((new FB2Element("version", "1.0")).xml(this));
    return desc;
  }

  _makeBodyElement() {
    const body = this.createElement("body");
    const title = this.createElement("title");
    body.appendChild(title);
    if (this.bookAuthors.length) title.appendChild((new FB2Paragraph(this.bookAuthors.join(", "))).xml(this));
    title.appendChild((new FB2Paragraph(this.bookTitle)).xml(this));
    this.chapters.forEach(ch => body.appendChild(ch.xml(this)));
    return body;
  }

  _makeBinaryElements() {
    const res = this.binaries.reduce((list, img) => {
      if (img.value) {
        const el = this.createElement("binary");
        el.setAttribute("id", img.id);
        el.setAttribute("content-type", img.type);
        el.textContent = img.value;
        list.push(el);
      }
      return list;
    }, []);
    return res;
  }

  _genBookId() {
    let str = this.sourceURL;
    let hash = 0;
    const slen = str.length;
    for (let i = 0; i < slen; ++i) {
      const ch = str.charCodeAt(i);
      hash = ((hash << 5) - hash) + ch;
      hash = hash & hash; // Convert to 32bit integer
    }
    return "rbe_" + Math.abs(hash).toString() + (hash > 0 ? "1" : "");
  }
}

class FB2Element {
  constructor(name, value) {
    this.name = name;
    this.value = value !== undefined ? value : null;
    this.children = [];
  }

  static async fromHTML(node, fb2doc, log) {
    let fb2el = null;
    const names = new Map([
      [ "U", "emphasis" ], [ "EM", "emphasis" ], [ "EMPHASIS", "emphasis" ], [ "I", "emphasis" ],
      [ "S", "strike" ], [ "DEL", "strike" ], [ "STRIKE", "strike" ],
      [ "STRONG", "strong" ], [ "BLOCKQUOTE", "cite" ],
      [ "#comment", null ]
    ]);
    const node_name = node.nodeName;
    if (names.has(node_name)) {
      const name = names.get(node_name);
      if (!name) return;
      fb2el = new FB2Element(names.get(node_name));
    } else {
      switch (node_name) {
        case "#text":
          return new FB2Text(node.textContent);
        case "P":
          fb2el = new FB2Paragraph();
          break;
        case "SUBTITLE":
          fb2el = new FB2Subtitle();
          break;
        case "A":
          fb2el = new FB2Link(node.href || node.getAttribute("l:href"));
          break;
        case "BR":
          return new FB2EmptyLine();
        case "HR":
          return new FB2Paragraph("---");
        case "IMG":
          {
            const img = new FB2Image(node.src);
            if (fb2doc) fb2doc.binaries.push(img);
            let li = log.message("Загрузка изображения...");
            try {
              await img.load();
              li.ok();
            } catch (err) {
              li.fail();
              throw err;
            }
            return img;
          }
        default:
          throw new Error("Неизвестный HTML блок: " + node.nodeName);
      }
    }
    await fb2el.appendContentFromHTML(node, fb2doc, log);
    return fb2el;
  }

  hasValue() {
    return ((this.value !== undefined && this.value !== null) || !!this.children.length);
  }

  async setContentFromHTML(data, fb2doc, log) {
    this.children = [];
    await this.appendContentFromHTML(data, fb2doc, log);
  }

  async appendContentFromHTML(data, fb2doc, log) {
    for (const node of data.childNodes) {
      let fe = await FB2Element.fromHTML(node, fb2doc, log);
      if (fe) this.children.push(fe);
    }
  }

  normalize() {
    const res_list = [ this ];
    let cur_el = this;
    const children = this.children;
    this.children = [];
    children.forEach(el => {
      if (el instanceof FB2EmptyLine || el instanceof FB2Subtitle) {
        res_list.push(el);
        cur_el = new this.constructor();
        res_list.push(cur_el);
      } else {
        el.normalize().forEach(el => {
          cur_el.children.push(el);
        });
      }
    });
    return res_list;
  }

  xml(doc) {
    const el = doc.createElement(this.name);
    if (this.value !== null) el.textContent = this.value;
    this.children.forEach(ch => el.appendChild(ch.xml(doc)));
    return el;
  }
}

class FB2BlockElement extends FB2Element {
  normalize() {
    // Удалить пробельные символы в конце блока
    while (this.children.length) {
      const el = this.children[this.children.length - 1];
      if (el.name === "text" && typeof(el.value) === "string") {
        el.value = el.value.trimEnd();
        if (!el.value) {
          this.children.pop();
          continue;
        }
      }
      break;
    }
    // Удалить пробельные символы в начале блока
    while (this.children.length) {
      const el = this.children[0];
      if (el.name === "text" && typeof(el.value) === "string") {
        el.value = el.value.trimStart();
        if (!el.value) {
          this.children.shift();
          continue;
        }
      }
      break;
    }
    //--
    return super.normalize();
  }
}

/**
 * FB2 элемент верхнего уровня section
 */
class FB2Chapter extends FB2Element {
  constructor(title) {
    super("section");
    this.title = title;
  }

  normalize() {
    // Обернуть текстовые ноды в параграфы и удалить пустые элементы
    this.children = this.children.reduce((list, el) => {
      if (el instanceof FB2Text) {
        const pe = new FB2Paragraph();
        pe.children.push(el);
        el = pe;
      }
      el.normalize().forEach(el => {
        if (el.hasValue()) list.push(el);
      });
      return list;
    }, []);
    return [ this ];
  }

  xml(doc) {
    const el = super.xml(doc);
    if (this.title) {
      const t_el = doc.createElement("title");
      const p_el = doc.createElement("p");
      p_el.textContent = this.title;
      t_el.appendChild(p_el);
      el.prepend(t_el);
    }
    return el;
  }
}

/**
 * FB2 элемент верхнего уровня annotation
 */
class FB2Annotation extends FB2Element {
  constructor() {
    super("annotation");
  }

  normalize() {
    // Обернуть неформатированный текст, разделенный <br> в параграфы
    let lp = null;
    const newParagraph = list => {
      lp = new FB2Paragraph();
      list.push(lp);
    };
    this.children = this.children.reduce((list, el) => {
      if (el.name === "empty-line") {
        newParagraph(list);
      } else if (el instanceof FB2BlockElement) {
        list.push(el);
        lp = null;
      } else {
        if (!lp) newParagraph(list);
        lp.children.push(el);
      }
      return list;
    }, []);
    // Запустить собственную нормализацию дочерних элементов
    // чтобы исключить дальнейшее всплытие элементов
    this.children = this.children.reduce((list, el) => {
      el.normalize().forEach(el => {
        if (el.hasValue()) list.push(el);
      });
      return list;
    }, []);
  }
}

class FB2Subtitle extends FB2BlockElement {
  constructor(value) {
    super("subtitle", value);
  }
}

class FB2Paragraph extends FB2BlockElement {
  constructor(value) {
    super("p", value);
  }
}

class FB2EmptyLine extends FB2Element {
  constructor() {
    super("empty-line");
  }

  hasValue() {
    return true;
  }
}

class FB2Text extends FB2Element {
  constructor(value) {
    super("text", value);
  }

  xml(doc) {
    return doc.createTextNode(this.value);
  }
}

class FB2Link extends FB2Element {
  constructor(href) {
    super("a");
    this.href = href;
  }

  xml(doc) {
    const el = super.xml(doc);
    el.setAttribute("l:href", this.href);
    return el;
  }
}

class FB2Author extends FB2Element {
  constructor(s) {
    super("author");
    const a = s.split(" ");
    switch (a.length) {
      case 1:
        this.nickName = s;
        break;
      case 2:
        this.firstName = a[0];
        this.lastName = a[1];
        break;
      default:
        this.firstName = a[0];
        this.middleName = a.slice(1, -1).join(" ");
        this.lastName = a[a.length - 1];
        break;
    }
    this.homePage = null;
  }

  hasValue() {
    return (!!this.firstName || !!this.lastName || !!this.middleName);
  }

  toString() {
    if (!this.firstName) return this.nickName;
    return [ this.firstName, this.middleName, this.lastName ].reduce((list, name) => {
      if (name) list.push(name);
      return list;
    }, []).join(" ");
  }

  xml(doc) {
    let a_el = super.xml(doc);
    [
      [ "first-name", this.firstName ], [ "middle-name", this.middleName ],
      [ "last-name", this.lastName ], [ "home-page", this.homePage ],
      [ "nickname", this.nickName ]
    ].forEach(it => {
      if (it[1]) {
        const e = doc.createElement(it[0]);
        e.textContent = it[1];
        a_el.appendChild(e);
      }
    });
    return a_el;
  }
}

class FB2Image extends FB2Element {
  constructor(value) {
    super("image");
    if (typeof(value) === "string") {
      this.url = value;
    } else {
      this.value = value;
    }
  }

  async load() {
    if (this.url) {
      const bin = await Fetcher.addJob(this.url, { responseType: "binary" });
      this.type = bin.type;
      this.size = bin.size;
      return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.addEventListener("loadend", (event) => resolve(event.target.result));
        reader.readAsDataURL(bin);
      }).then(base64str => {
        this.value = base64str.substr(base64str.indexOf(",") + 1);
      }).catch(err => {
        throw new Error("Ошибка загрузки изображения");
      });
    }
  }

  xml(doc) {
    if (this.value) {
      const el = doc.createElement(this.name);
      el.setAttribute("l:href", "#" + this.id);
      return el
    }
    const el = doc.createElement("p");
    const id = this.id || "изображение";
    el.textContent = `[ ${id} ]`;
    return el;
  }

  suffix() {
    switch (this.type) {
      case "image/png":
        return ".png";
      case "image/jpeg":
        return ".jpg";
      case "image/webp":
        return ".webp";
    }
    return "";
  }
}

//---

class LogElement {
  constructor(element) {
    element.style.padding = ".5em";
    element.style.fontSize = "90%";
    element.style.border = "1px solid lightgray";
    element.style.marginBottom = "1em";
    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");
  }

  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 Fetcher {
  static async addJob(url, params) {
    params ||= {};
    const fp = {};
    fp.method = params.method || "GET";
    fp.credentials = "same-origin";
    fp.signal = Fetcher._getSignal();
    const resp = await fetch(url, fp);
    if (!resp.ok) throw new Error(`Сервер вернул ошибку (${resp.status})`);
    switch (params.responseType) {
      case "binary":
        return await resp.blob();
      default:
        return await resp.text();
    }
  }

  static abortAll() {
    if (Fetcher._controller) {
      Fetcher._controller.abort();
      Fetcher._controller = null;
    }
  }

  static _getSignal() {
    let controller = Fetcher._controller;
    if (!controller) Fetcher._controller = controller = new AbortController();
    return controller.signal;
  }
}

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

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

}());

QingJ © 2025

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