This is a library for converting HTML to FB2.
此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.greasyfork.org/scripts/468831/1478439/HTML2FB2Lib.js
// ==UserScript== // @name HTML2FB2Lib // @name:ru HTML2FB2Lib // @namespace 90h.yy.zz // @version 0.10.4 // @author Ox90 // @description This library is designed to convert HTML to FB2. // @description:ru Эта библиотека предназначена для конвертирования HTML в FB2. // @license MIT // ==/UserScript== class FB2Parser { run(fb2doc, htmlNode, fromNode) { this._stop = null; this._notes = []; const res = this.parse(htmlNode, fromNode); this._notes.forEach(note => fb2doc.notes.push(note)); delete this._notes; return res; } parse(htmlNode, fromNode) { const that = this; function _parse(node, from, fb2el, depth) { let n = from || node.firstChild; while (n) { const nn = that.startNode(n, depth, fb2el); if (nn) { const f = that.processElement(FB2Element.fromHTML(nn, false), depth); if (f) { if (fb2el) fb2el.children.push(f); _parse(nn, null, f, depth + 1); } that.endNode(nn, depth); } if (that._stop) break; n = n.nextSibling; } } _parse(htmlNode, fromNode, null, 0); return this._stop; } startNode(node, depth, fb2to) { return node; } processElement(fb2el, depth) { if (fb2el instanceof FB2Note) this._notes.push(fb2el); return fb2el; } endNode(node, depth) { } } class FB2AnnotationParser extends FB2Parser { run(fb2doc, htmlNode, fromNode) { this._binaries = []; const res = super.run(fb2doc, htmlNode, fromNode); fb2doc.annotation = this._annotation; if (fb2doc.annotation) { fb2doc.annotation.normalize(); this._binaries.forEach(bin => fb2doc.binaries.push(bin)); this._binaries = null; } return res; } parse(htmlNode, fromNode) { this._annotation = new FB2Annotation(); const res = super.parse(htmlNode, fromNode); if (!this._annotation.children.length) this._annotation = null; return res; } processElement(fb2el, depth) { if (fb2el) { if (depth === 0) this._annotation.children.push(fb2el); if (fb2el instanceof FB2Image) this._binaries.push(fb2el); } return super.processElement(fb2el, depth); } } class FB2ChapterParser extends FB2Parser { run(fb2doc, htmlNode, title, fromNode) { this._binaries = []; const res = this.parse(title, htmlNode, fromNode); this._chapter.normalize(); fb2doc.chapters.push(this._chapter); this._binaries.forEach(bin => fb2doc.binaries.push(bin)); this._binaries = null; return res; } parse(title, htmlNode, fromNode) { this._chapter = new FB2Chapter(title); return super.parse(htmlNode, fromNode); } processElement(fb2el, depth) { if (fb2el) { if (depth === 0) this._chapter.children.push(fb2el); if (fb2el instanceof FB2Image) this._binaries.push(fb2el); } return super.processElement(fb2el, depth); } } class FB2Document { constructor() { this.notes = []; this.binaries = []; this.bookAuthors = []; this.annotation = null; this.genres = []; this.keywords = []; this.chapters = []; this.history = []; this.xmldoc = null; this._parsers = new Map(); } toString() { this._ensureXMLDocument(); const root = this.xmldoc.documentElement; this._markNotes(); this._markBinaries(); root.appendChild(this._makeDescriptionElement()); root.appendChild(this._makeBodyElement()); if (this.notes.length) root.appendChild(this._makeNotesElement()); this._makeBinaryElements().forEach(el => root.appendChild(el)); const res = (new XMLSerializer()).serializeToString(this.xmldoc); this.xmldoc = null; return res; } createElement(name) { this._ensureXMLDocument(); return this.xmldoc.createElementNS(this.xmldoc.documentElement.namespaceURI, name); } createTextNode(value) { this._ensureXMLDocument(); return this.xmldoc.createTextNode(value); } createDocumentFragment() { this._ensureXMLDocument(); return this.xmldoc.createDocumentFragment(); } bindParser(parserId, parser) { if (!parser && !parserId) { this._parsers.clear(); return; } this._parsers.set(parserId, parser); } parse(parserId, ...args) { const parser = this._parsers.get(parserId); if (!parser) throw new Error(`Unknown parser id: ${parserId}`); return parser.run(this, ...args); } _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); //-- const ch_num = t_info.children.length; this.genres.forEach(gi => { if (gi instanceof FB2Genre) { t_info.appendChild(gi.xml(this)); } else if (typeof(gi) === "string") { (new FB2GenreList(gi)).forEach(g => t_info.appendChild(g.xml(this))); } }); if (t_info.children.length === ch_num) t_info.appendChild((new FB2Genre("network_literature")).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)); //-- if (this.annotation) t_info.appendChild(this.annotation.xml(this)); //-- let keywords = null; if (Array.isArray(this.keywords) && this.keywords.length) { keywords = this.keywords.join(", "); } else if (typeof(this.keywords) === "string" && this.keywords.trim()) { keywords = this.keywords.trim(); } if (keywords) t_info.appendChild((new FB2Element("keywords", keywords)).xml(this)); //-- if (this.bookDate) { const el = this.createElement("date"); el.setAttribute("value", FB2Utils.dateToAtom(this.bookDate)); el.textContent = this.bookDate.getFullYear(); t_info.appendChild(el); } //-- if (this.coverpage) { const el = this.createElement("coverpage"); (Array.isArray(this.coverpage) ? this.coverpage : [ this.coverpage ]).forEach(img => { el.appendChild(img.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)); //-- if (this.programName) d_info.appendChild((new FB2Element("program-used", this.programName)).xml(this)); //-- d_info.appendChild((() => { const f_time = new Date(); const el = this.createElement("date"); el.setAttribute("value", FB2Utils.dateToAtom(f_time)); 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)); //-- if (this.history.length) { const hs = this.createElement("history"); d_info.appendChild(hs); this.history.forEach(it => hs.appendChild((new FB2Paragraph(it)).xml(this))); } //-- return desc; } _makeBodyElement() { const body = this.createElement("body"); if (this.bookTitle || this.bookAuthors.length) { const title = this.createElement("title"); body.appendChild(title); if (this.bookAuthors.length) title.appendChild((new FB2Paragraph(this.bookAuthors.join(", "))).xml(this)); if (this.bookTitle) title.appendChild((new FB2Paragraph(this.bookTitle)).xml(this)); } this.chapters.forEach(ch => body.appendChild(ch.xml(this))); return body; } _markNotes() { let idx = 0; this.notes.forEach(note => { if (!note.id) note.id = "note" + (++idx); if (!note.title) note.title = idx.toString(); }); } _makeNotesElement() { const body = this.createElement("body"); body.setAttribute("name", "notes"); const title = this.createElement("title"); title.appendChild(this.createElement("p")).textContent = "Примечания"; body.append(title); this.notes.forEach(note => body.append(note.xmlSection(this))); return body; } _markBinaries() { let idx = 0; this.binaries.forEach(img => { if (!img.id) img.id = "image" + (++idx) + img.suffix(); }); } _makeBinaryElements() { return this.binaries.reduce((list, img) => { if (img.value) list.push(img.xmlBinary(this)); return list; }, []); } _genBookId() { let str = this.sourceURL || this.bookTitle || ""; 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 (this.idPrefix || "h2f2l_") + Math.abs(hash).toString() + (hash > 0 ? "1" : ""); } } class FB2Element { constructor(name, value) { this.name = name; this.value = value !== undefined ? value : null; this.children = []; } static fromHTML(node, recursive) { let fb2el = null; const names = new Map([ [ "U", "emphasis" ], [ "EM", "emphasis" ], [ "EMPHASIS", "emphasis" ], [ "I", "emphasis" ], [ "S", "strikethrough" ], [ "DEL", "strikethrough" ], [ "STRIKE", "strikethrough" ], [ "STRONG", "strong" ], [ "B", "strong" ], [ "BLOCKQUOTE", "cite" ], [ "SUB", "sub" ], [ "SUP", "sup" ], [ "SCRIPT", null ], [ "#comment", null ] ]); const node_name = node.nodeName; if (names.has(node_name)) { const name = names.get(node_name); if (!name) return null; fb2el = new FB2Element(names.get(node_name)); } else { switch (node_name) { case "#text": return new FB2Text(node.textContent); case "SPAN": fb2el = new FB2Text(); break; case "P": case "LI": fb2el = new FB2Paragraph(); break; case "SUBTITLE": fb2el = new FB2Subtitle(); break; case "A": fb2el = new FB2Link(node.href || node.getAttribute("l:href")); break; case "OL": fb2el = new FB2OrderedList(); break; case "UL": fb2el = new FB2UnorderedList(); break; case "BR": return new FB2EmptyLine(); case "HR": return new FB2Paragraph("---"); case "IMG": return new FB2Image(node.src); default: return new FB2UnknownNode(node); } } if (recursive) fb2el.appendContentFromHTML(node); return fb2el; } hasValue() { return ((this.value !== undefined && this.value !== null) || !!this.children.length); } setContentFromHTML(data, fb2doc, log) { this.children = []; this.appendContentFromHTML(data, fb2doc, log); } appendContentFromHTML(data, fb2doc, log) { for (const node of data.childNodes) { let fe = FB2Element.fromHTML(node, true); if (fe) this.children.push(fe); } } normalize() { const _normalize = function(list) { let done = true; let res_list = list.reduce((accum, cur_el) => { accum.push(cur_el); const tmp_ch = cur_el.children; cur_el.children = []; tmp_ch.forEach(el => { if ( ( (el instanceof FB2Paragraph || el instanceof FB2EmptyLine) && (!(el instanceof FB2Chapter || el instanceof FB2Annotation || el.name === "cite" || el.name === "title")) ) || ( (el.name === "cite") && (!(el instanceof FB2Chapter || el instanceof FB2Annotation)) ) || ( (el instanceof FB2Subtitle) && (!(el instanceof FB2Chapter || el.name === "cite")) ) ) { // Вытолкнуть элемент вверх, разбив текущий элемент на две части accum.push(el); const nm = cur_el.name; cur_el = new cur_el.constructor(); if (!cur_el.name) cur_el.name = nm; accum.push(cur_el); done = false; } else { let cnt = 0; el.normalize().forEach(e => { // Убрать избыточную вложенность: <el><el>value</el></el> --> <el>value</el> if (!e.value && e.children.length === 1 && e.name === e.children[0].name) { e = e.children[0]; } if (e !== el) done = false; if (e.hasValue()) cur_el.children.push(e); }); } }); return accum; }, []); return { list: res_list, done: done }; } //-- let result = _normalize([ this ]); while (!result.done) { result = _normalize(result.list); } return result.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() { // Предварительная нормализация this.children = this.children.reduce((list, ch) => { ch.normalize().forEach(cc => list.push(cc)); return list; }, []); // Удалить пустоты справа while (this.children.length) { const el = this.children[this.children.length - 1]; if (el instanceof FB2Text) el.trimRight(); if (!el.hasValue()) { this.children.pop(); continue; } break; } // Удалить пустоты слева while (this.children.length) { const el = this.children[0]; if (el instanceof FB2Text) el.trimLeft(); if (!el.hasValue()) { this.children.shift(); continue; } break; } // Удалить пустоты в содержимом элемента if (!this.children.length && typeof(this.value) === "string") { this.value = this.value.trim(); } // Окончательная нормализация 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 (![ "p", "subtitle", "image", "empty-line", "cite" ].includes(el.name)) { 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 ([ "p", "subtitle", "cite" ].includes(el.name)) { list.push(el); lp = null; } else if (el.name === "empty-line") { if (!lp) { // Перенос между блоками if (list.length) list.push(new FB2EmptyLine); } else if (!lp.children.length) { // Более одного переноса подряд между inline элементами list.pop(); list.push(new FB2EmptyLine()); list.push(lp); } else { // Перенос между inline элементами newParagraph(list); } } 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; }, []); // Удалить конечные пустые строки for (let len = this.children.length; len; ) { if (this.children[len - 1].name !== "empty-line") break; this.children.pop(); --len; } } } 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); } trimLeft() { if (typeof(this.value) === "string") this.value = this.value.trimLeft() || null; if (!this.value) { while (this.children.length) { const first_child = this.children[0]; if (first_child instanceof FB2Text) first_child.trimLeft(); if (first_child.hasValue()) break; this.children.shift(); } } } trimRight() { while (this.children.length) { const last_child = this.children[this.children.length - 1]; if (last_child instanceof FB2Text) last_child.trimRight(); if (last_child.hasValue()) break; this.children.pop(); } if (!this.children.length && typeof(this.value) === "string") { this.value = this.value.trimRight() || null; } } xml(doc) { if (!this.value && this.children.length) { let fr = doc.createDocumentFragment(); for (const ch of this.children) { fr.appendChild(ch.xml(doc)); } return fr; } 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 FB2List extends FB2Element { constructor() { super("list"); } xml(doc) { const fr = doc.createDocumentFragment(); for (const ch of this.children) { if (ch.hasValue()) { let ch_el = null; if (ch instanceof FB2BlockElement) { ch_el = ch.xml(doc); } else { const par = new FB2Paragraph(); par.children.push(ch); ch_el = par.xml(doc); } if (ch_el.textContent.trim() !== "") fr.appendChild(ch_el); } } return fr; } } class FB2OrderedList extends FB2List { xml(doc) { let pos = 0; const fr = super.xml(doc); for (const el of fr.children) { ++pos; el.prepend(`${pos}. `); } return fr; } } class FB2UnorderedList extends FB2List { xml(doc) { const fr = super.xml(doc); for (const el of fr.children) { el.prepend("- "); } return fr; } } 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 ], [ "nickname", this.nickName ], [ "home-page", this.homePage ] ].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(onprogress) { if (this.url) { const bin = await this._load(this.url, { responseType: "binary", onprogress: onprogress }); this.type = bin.type; this.size = bin.size; if (!this.suffix()) throw new Error("Неизвестный формат изображения"); return new Promise((resolve, reject) => { const reader = new FileReader(); reader.addEventListener("loadend", (event) => resolve(event.target.result)); reader.readAsDataURL(bin); }).then(base64str => { this.value = this._getBase64String(base64str); }).catch(err => { throw new Error("Ошибка загрузки изображения"); }); } } hasValue() { return true; } xml(doc) { if (this.value) { const el = doc.createElement(this.name); el.setAttribute("l:href", "#" + this.id); return el } const id = this.id || "изображение"; return doc.createTextNode(`[ ${id} ]`); } xmlBinary(doc) { const el = doc.createElement("binary"); el.setAttribute("id", this.id); el.setAttribute("content-type", this.type); el.textContent = this.value return el; } suffix() { switch (this.type) { case "image/png": return ".png"; case "image/jpeg": return ".jpg"; case "image/gif": return ".gif"; case "image/webp": return ".webp"; } return ""; } async convert(targetType) { return new Promise((resolve, reject) => { const img = new Image(); img.addEventListener("load", () => { const cvs = document.createElement("canvas"); cvs.width = img.width; cvs.height = img.height; cvs.getContext("2d", { alpha: false }).drawImage(img, 0, 0); this.value = this._getBase64String(cvs.toDataURL(targetType)); this.type = targetType; resolve(); }); img.addEventListener("error", () => reject(new Error("Некорректный формат изображения"))); img.src = `data:${this.type};base64,` + this.value; }); } async _load(...args) { return FB2Loader.addJob(...args); } _getBase64String(data) { return data.substr(data.indexOf(",") + 1); } } class FB2Note extends FB2Element { constructor(value, title) { super("note"); this.value = value; this.title = title; } xml(doc) { const el = doc.createElement("a"); el.setAttribute("l:href", "#" + this.id); el.setAttribute("type", "note"); el.textContent = `[${this.title}]`; return el; } xmlSection(doc) { const sec = new FB2Chapter(this.title); sec.children.push(new FB2Paragraph(this.value)); const el = sec.xml(doc); el.setAttribute("id", this.id); return el; } } class FB2Genre extends FB2Element { constructor(value) { super("genre", value); } } class FB2UnknownNode extends FB2Element { constructor(value) { super("unknown", value); } xml(doc) { return doc.createTextNode(this.value && this.value.textContent || ""); } } class FB2GenreList extends Array { constructor(...args) { if (args.length === 1 && typeof(args[0]) === "number") { super(args[0]); return; } const list = (args.length === 1) ? (Array.isArray(args[0]) ? args[0] : [ args[0] ]) : args; super(); if (!list.length) return; const keys = FB2GenreList._keys; const gmap = new Map(); const addWeight = (name, weight) => gmap.set(name, (gmap.get(name) || 0) + weight); list.forEach(p_str => { p_str = p_str.toLowerCase(); let words = p_str.split(/[\s,.;]+/); if (words.length === 1) words = []; for (const it of keys) { const exact_names = Array.isArray(it[1]) ? it[1] : [ it[1] ]; if (it[0] === p_str || exact_names.includes(p_str)) { addWeight(it[0], 3); // Exact match break; } // Scan each word let weight = words.some(w => exact_names.includes(w)) ? 2 : 0; it[2] && it[2].forEach(k => { if (words.includes(k)) ++weight; }); if (weight >= 2) addWeight(it[0], weight); } }); const res = []; gmap.forEach((weight, name) => res.push([ name, weight])); if (!res.length) return; res.sort((a, b) => b[1] > a[1]); // Add at least five genres with maximum weight let cur_w = 0; for (const it of res) { if (it[1] !== cur_w && this.length >= 5) break; cur_w = it[1]; this.push(new FB2Genre(it[0])); } } } FB2GenreList._keys = [ [ "adv_animal", "природа и животные", [ "приключения", "животные", "природа" ] ], [ "adventure", "приключения" ], [ "adv_geo", "путешествия и география", [ "приключения", "география", "путешествие" ] ], [ "adv_history", "исторические приключения", [ "история", "приключения" ] ], [ "adv_indian", "вестерн, про индейцев", [ "индейцы", "вестерн" ] ], [ "adv_maritime", "морские приключения", [ "приключения", "море" ] ], [ "adv_modern", "приключения в современном мире", [ "современный", "мир" ] ], [ "adv_story", "авантюрный роман" ], [ "antique", "старинное" ], [ "antique_ant", "античная литература", [ "старинное", "античность" ] ], [ "antique_east", "древневосточная литература", [ "старинное", "восток" ] ], [ "antique_european", "европейская старинная литература", [ "старинное", "европа" ] ], [ "antique_myths", "мифы. легенды. эпос", [ "мифы", "легенды", "эпос", "фольклор" ] ], [ "antique_russian", "древнерусская литература", [ "древнерусское", "старинное" ] ], [ "aphorism_quote", "афоризмы, цитаты", [ "афоризмы", "цитаты", "проза" ] ], [ "architecture_book", "скульптура и архитектура", [ "дизайн" ] ], [ "art_criticism", "искусствоведение" ], [ "art_world_culture", "мировая художественная культура", [ "искусство", "искусствоведение" ] ], [ "astrology", "астрология и хиромантия", [ "астрология", "хиромантия" ] ], [ "auto_business", "автодело" ], [ "auto_regulations", "автомобили и ПДД", [ "дорожного", "движения", "дорожное", "движение" ] ], [ "banking", "финансы", [ "банки", "деньги" ] ], [ "child_adv", "приключения для детей и подростков" ], [ "child_classical", "классическая детская литература" ], [ "child_det", "детская остросюжетная литература" ], [ "child_education", "детская образовательная литература" ], [ "child_folklore", "детский фольклор" ], [ "child_prose", "проза для детей" ], [ "children", "детская литература", [ "детское" ] ], [ "child_sf", "фантастика для детей" ], [ "child_tale", "сказки народов мира" ], [ "child_tale_rus", "русские сказки" ], [ "child_verse", "стихи для детей" ], [ "cine", "кино" ], [ "comedy", "комедия" ], [ "comics", "комиксы" ], [ "comp_db", "программирование, программы, базы данных", [ "программирование", "базы", "программы" ] ], [ "comp_hard", "компьютерное железо", [ "аппаратное" ] ], [ "comp_soft", "программное обеспечение" ], [ "computers", "компьютеры" ], [ "comp_www", "ос и сети, интернет", [ "ос", "сети", "интернет" ] ], [ "design", "дизайн" ], [ "det_action", [ "боевики", "боевик" ], [ "триллер" ] ], [ "det_classic", "классический детектив" ], [ "det_crime", "криминальный детектив", [ "криминал" ] ], [ "det_espionage", "шпионский детектив", [ "шпион", "шпионы", "детектив" ] ], [ "det_hard", "крутой детектив" ], [ "det_history", "исторический детектив", [ "история" ] ], [ "det_irony", "иронический детектив" ], [ "det_maniac", "про маньяков", [ "маньяки", "детектив" ] ], [ "det_police", "полицейский детектив", [ "полиция", "детектив" ] ], [ "det_political", "политический детектив", [ "политика", "детектив" ] ], [ "det_su", "советский детектив", [ "ссср", "детектив" ] ], [ "detective", "детектив", [ "детективы" ] ], [ "drama", "драма" ], [ "drama_antique", "античная драма" ], [ "dramaturgy", "драматургия" ], [ "economics", "экономика" ], [ "economics_ref", "деловая литература" ], [ "epic", "былины, эпопея", [ "былины", "эпопея" ] ], [ "epistolary_fiction", "эпистолярная проза" ], [ "equ_history", "история техники" ], [ "fairy_fantasy", "мифологическое фэнтези", [ "мифология", "фантастика" ] ], [ "family", "семейные отношения", [ "дом", "семья" ] ], [ "fanfiction", "фанфик" ], [ "folklore", "фольклор, загадки" ], [ "folk_songs", "народные песни" ], [ "folk_tale", "народные сказки" ], [ "foreign_antique", "средневековая классическая проза" ], [ "foreign_children", "зарубежная литература для детей" ], [ "foreign_prose", "зарубежная классическая проза" ], [ "geo_guides", "путеводители, карты, атласы", [ "география", "атласы", "карты", "путеводители" ] ], [ "gothic_novel", "готический роман" ], [ "great_story", "роман", [ "повесть" ] ], [ "home", "домоводство", [ "дом", "семья" ] ], [ "home_collecting", "коллекционирование" ], [ "home_cooking", "кулинария", [ "домашняя", "еда" ] ], [ "home_crafts", "хобби и ремесла" ], [ "home_diy", "сделай сам" ], [ "home_entertain", "развлечения" ], [ "home_garden", "сад и огород" ], [ "home_health", "здоровье" ], [ "home_pets", "домашние животные" ], [ "home_sex", "семейные отношения, секс" ], [ "home_sport", "боевые исскусства, спорт" ], [ "hronoopera", "хроноопера" ], [ "humor", "юмор" ], [ "humor_anecdote", "анекдоты" ], [ "humor_prose", "юмористическая проза" ], [ "humor_satire", "сатира" ], [ "humor_verse", "юмористические стихи, басни", [ "юмор", "стихи", "басни" ] ], [ "limerick", [ "частушки", "прибаутки", "потешки" ] ], [ "literature_18", "классическая проза XVII-XVIII веков" ], [ "literature_19", "классическая проза ХIX века" ], [ "literature_20", "классическая проза ХX века" ], [ "love", "любовные романы" ], [ "love_contemporary", "современные любовные романы" ], [ "love_detective", "остросюжетные любовные романы", [ "детектив", "любовь" ] ], [ "love_erotica", "эротика", [ "эротическая", "литература" ] ], [ "love_hard", "порно" ], [ "love_history", "исторические любовные романы", [ "история", "любовь" ] ], [ "love_sf", "любовное фэнтези" ], [ "love_short", "короткие любовные романы" ], [ "lyrics", "лирика" ], [ "military_history", "военная история", [ "война", "история" ] ], [ "military_special", "военное дело" ], [ "military_weapon", "военная техника и вооружение", [ "военная", "вооружение", "техника" ] ], [ "modern_tale", "современная сказка" ], [ "music", "музыка" ], [ "network_literature", "сетевая литература" ], [ "nonf_biography", "биографии и мемуары", [ "биография", "биографии", "мемуары" ] ], [ "nonf_criticism", "критика" ], [ "nonfiction", "документальная литература" ], [ "nonf_military", "военная документалистика и аналитика" ], [ "nonf_publicism", "публицистика" ], [ "notes:", "партитуры" ], [ "org_behavior", "маркентиг, pr", [ "организации" ] ], [ "painting", "живопись", [ "альбомы", "иллюстрированные", "каталоги" ] ], [ "palindromes", "визуальная и экспериментальная поэзия", [ "верлибры", "палиндромы", "поэзия" ] ], [ "periodic", "журналы, газеты", [ "журналы", "газеты" ]], [ "poem", "поэма", [ "эпическая", "поэзия" ] ], [ "poetry", "поэзия" ], [ "poetry_classical", "классическая поэзия" ], [ "poetry_east", "поэзия востока" ], [ "poetry_for_classical", "классическая зарубежная поэзия" ], [ "poetry_for_modern", "современная зарубежная поэзия" ], [ "poetry_modern", "современная поэзия" ], [ "poetry_rus_classical", "классическая русская поэзия" ], [ "poetry_rus_modern", "современная русская поэзия", [ "русская", "поэзия" ] ], [ "popadanec", "попаданцы", [ "попаданец" ] ], [ "popular_business", "карьера, кадры", [ "карьера", "дело", "бизнес" ] ], [ "prose", "проза" ], [ "prose_abs", "фантасмагория, абсурдистская проза" ], [ "prose_classic", "классическая проза" ], [ "prose_contemporary", "современная русская и зарубежная проза", [ "современная", "проза" ] ], [ "prose_counter", "контркультура" ], [ "prose_game", "игры, упражнения для детей", [ "игры", "упражнения" ] ], [ "prose_history", "историческая проза", [ "история", "проза" ] ], [ "prose_magic", "магический реализм", [ "магия", "проза" ] ], [ "prose_military", "проза о войне" ], [ "prose_neformatny", "неформатная проза", [ "экспериментальная", "проза" ] ], [ "prose_rus_classic", "русская классическая проза" ], [ "prose_su_classics", "советская классическая проза" ], [ "proverbs", "пословицы", [ "поговорки" ] ], [ "ref_dict", "словари", [ "справочник" ] ], [ "ref_encyc", "энциклопедии", [ "энциклопедия" ] ], [ "ref_guide", "руководства", [ "руководство", "справочник" ] ], [ "ref_ref", "справочники", [ "справочник" ] ], [ "reference", "справочная литература" ], [ "religion", "религия", [ "духовность", "эзотерика" ] ], [ "religion_budda", "буддизм" ], [ "religion_catholicism", "католицизм" ], [ "religion_christianity", "христианство" ], [ "religion_esoterics", "эзотерическая литература", [ "эзотерика" ] ], [ "religion_hinduism", "индуизм" ], [ "religion_islam", "ислам" ], [ "religion_judaism", "иудаизм" ], [ "religion_orthdoxy", "православие" ], [ "religion_paganism", "язычество" ], [ "religion_protestantism", "протестантизм" ], [ "religion_self", "самосовершенствование" ], [ "russian_fantasy", "славянское фэнтези", [ "русское", "фэнтези" ] ], [ "sci_biology", "биология", [ "биофизика", "биохимия" ] ], [ "sci_botany", "ботаника" ], [ "sci_build", "строительство и сопромат", [ "строительтво", "сопромат" ] ], [ "sci_chem", "химия" ], [ "sci_cosmos", "астрономия и космос", [ "астрономия", "космос" ] ], [ "sci_culture", "культурология" ], [ "sci_ecology", "экология" ], [ "sci_economy", "экономика" ], [ "science", "научная литература" ], [ "sci_geo", "геология и география" ], [ "sci_history", "история" ], [ "sci_juris", "юриспруденция" ], [ "sci_linguistic", "языкознание", [ "иностранный", "язык" ] ], [ "sci_math", "математика" ], [ "sci_medicine_alternative", "альтернативная медицина" ], [ "sci_medicine", "медицина" ], [ "sci_metal", "металлургия" ], [ "sci_oriental", "востоковедение" ], [ "sci_pedagogy", "педагогика, воспитание детей, литература для родителей", [ "воспитание", "детей" ] ], [ "sci_philology", "литературоведение" ], [ "sci_philosophy", "философия" ], [ "sci_phys", "физика" ], [ "sci_politics", "политика" ], [ "sci_popular", "зарубежная образовательная литература", [ "зарубежная", "научно-популярная" ] ], [ "sci_psychology", "психология и психотерапия" ], [ "sci_radio", "радиоэлектроника" ], [ "sci_religion", "религиоведение", [ "религия", "духовность" ] ], [ "sci_social_studies", "обществознание", [ "социология" ] ], [ "sci_state", "государство и право" ], [ "sci_tech", "технические науки", [ "техника", "наука" ] ], [ "sci_textbook", "учебники и пособия" ], [ "sci_theories", "альтернативные науки и научные теории" ], [ "sci_transport", "транспорт и авиация" ], [ "sci_veterinary", "ветеринария" ], [ "sci_zoo", "зоология" ], [ "science", "научная литература", [ "образование" ] ], [ "screenplays", "сценарии", [ "сценарий" ] ], [ "sf", "научная фантастика", [ "наука", "фантастика" ] ], [ "sf_action", "боевая фантастика" ], [ "sf_cyberpunk", "киберпанк" ], [ "sf_detective", "детективная фантастика", [ "детектив", "фантастика" ] ], [ "sf_epic", "эпическая фантастика", [ "эпическое", "фэнтези" ] ], [ "sf_etc", "фантастика" ], [ "sf_fantasy", "фэнтези" ], [ "sf_fantasy_city", "городское фэнтези" ], [ "sf_heroic", "героическая фантастика", [ "героическое", "герой", "фэнтези" ] ], [ "sf_history", "альтернативная история", [ "историческое", "фэнтези" ] ], [ "sf_horror", "ужасы", [ "фантастика" ] ], [ "sf_humor", "юмористическая фантастика", [ "юмор", "фантастика" ] ], [ "sf_litrpg", "литрпг", [ "litrpg", "рпг" ] ], [ "sf_mystic", "мистика", [ "мистическая", "фантастика" ] ], [ "sf_postapocalyptic", "постапокалипсис" ], [ "sf_realrpg", "реалрпг", [ "realrpg" ] ], [ "sf_social", "Социально-психологическая фантастика", [ "социум", "психология", "фантастика" ] ], [ "sf_space", "космическая фантастика", [ "космос", "фантастика" ] ], [ "sf_stimpank", "стимпанк" ], [ "sf_technofantasy", "технофэнтези" ], [ "song_poetry", "песенная поэзия" ], [ "story", "рассказ", [ "рассказы", "эссе", "новеллы", "новелла", "феерия", "сборник", "рассказов" ] ], [ "tale_chivalry", "рыцарский роман", [ "рыцари", "приключения" ] ], [ "tbg_computers", "учебные пособия, самоучители", [ "пособия", "самоучители" ] ], [ "tbg_higher", "учебники и пособия ВУЗов", [ "учебники", "пособия" ] ], [ "tbg_school", "школьные учебники и пособия, рефераты, шпаргалки", [ "школьные", "учебники", "шпаргалки", "рефераты" ] ], [ "tbg_secondary", "учебники и пособия для среднего и специального образования", [ "учебники", "пособия", "образование" ] ], [ "theatre", "театр" ], [ "thriller", "триллер", [ "триллеры", "детектив", "детективы" ] ], [ "tragedy", "трагедия", [ "драматургия" ] ], [ "travel_notes", " география, путевые заметки", [ "география", "заметки" ] ], [ "vaudeville", "мистерия", [ "буффонада", "водевиль" ] ], ]; class FB2Loader { static async addJob(url, params) { params ||= {}; const fp = {}; fp.method = params.method || "GET"; fp.credentials = "same-origin"; fp.signal = this._getSignal(); const resp = await fetch(url, fp); if (!resp.ok) throw new Error(`Сервер вернул ошибку (${resp.status})`); const reader = resp.body.getReader(); const type = resp.headers.get("Content-Type"); const total = +resp.headers.get("Content-Length"); let loaded = 0; const chunks = []; const onprogress = (total && typeof(params.onprogress) === "function") ? params.onprogress : null; while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); loaded += value.length; if (onprogress) onprogress(loaded, total); } let result = null; switch (params.responseType) { case "binary": result = new Blob(chunks, { type: type }); break; default: { let pos = 0; const data = new Uint8Array(loaded); for (let ch of chunks) { data.set(ch, pos); pos += ch.length; } result = (new TextDecoder("utf-8")).decode(data); } break; } return params.extended ? { headers: resp.headers, response: result } : result; } static abortAll() { if (this._controller) { this._controller.abort(); this._controller = null; } } static _getSignal() { let controller = this._controller; if (!controller) this._controller = controller = new AbortController(); return controller.signal; } } class FB2Utils { static dateToAtom(date) { const m = date.getMonth() + 1; const d = date.getDate(); return "" + date.getFullYear() + '-' + (m < 10 ? "0" : "") + m + "-" + (d < 10 ? "0" : "") + d; } }