// ==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();
}());