HTML2FB2Lib

This is a library for converting HTML to FB2.

当前为 2023-06-29 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/468831/1212828/HTML2FB2Lib.js

  1. // ==UserScript==
  2. // @name HTML2FB2Lib
  3. // @namespace 90h.yy.zz
  4. // @version 0.5.0
  5. // @author Ox90
  6. // @match http://*
  7. // @match https://*
  8. // @description This is a library for converting HTML to FB2.
  9. // @run-at document-start
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. class FB2Parser {
  14. constructor() {
  15. this._stop = null;
  16. }
  17.  
  18. async parse(htmlNode, fromNode) {
  19. const that = this;
  20. async function _parse(node, from, fb2el, depth) {
  21. let n = from || node.firstChild;
  22. while (n) {
  23. const nn = that.startNode(n, depth);
  24. if (nn) {
  25. const f = that.processElement(FB2Element.fromHTML(nn, false), depth);
  26. if (f) {
  27. if (fb2el) fb2el.children.push(f);
  28. await _parse(nn, null, f, depth + 1);
  29. }
  30. that.endNode(nn, depth);
  31. }
  32. if (that._stop) break;
  33. n = n.nextSibling;
  34. }
  35. }
  36. await _parse(htmlNode, fromNode, null, 0);
  37. return this._stop;
  38. }
  39.  
  40. startNode(node, depth) {
  41. return node;
  42. }
  43.  
  44. processElement(fb2el, depth) {
  45. return fb2el;
  46. }
  47.  
  48. endNode(node, depth) {
  49. }
  50. }
  51.  
  52. class FB2Document {
  53. constructor() {
  54. this.binaries = [];
  55. this.bookAuthors = [];
  56. this.annotation = null;
  57. this.genres = [];
  58. this.keywords = [];
  59. this.chapters = [];
  60. this.xmldoc = null;
  61. }
  62.  
  63. toString() {
  64. this._ensureXMLDocument();
  65. const root = this.xmldoc.documentElement;
  66. this._markBinaries();
  67. root.appendChild(this._makeDescriptionElement());
  68. root.appendChild(this._makeBodyElement());
  69. this._makeBinaryElements().forEach(el => root.appendChild(el));
  70. const res = (new XMLSerializer()).serializeToString(this.xmldoc);
  71. this.xmldoc = null;
  72. return res;
  73. }
  74.  
  75. createElement(name) {
  76. this._ensureXMLDocument();
  77. return this.xmldoc.createElementNS(this.xmldoc.documentElement.namespaceURI, name);
  78. }
  79.  
  80. createTextNode(value) {
  81. this._ensureXMLDocument();
  82. return this.xmldoc.createTextNode(value);
  83. }
  84.  
  85. createDocumentFragment() {
  86. this._ensureXMLDocument();
  87. return this.xmldoc.createDocumentFragment();
  88. }
  89.  
  90. _ensureXMLDocument() {
  91. if (!this.xmldoc) {
  92. this.xmldoc = new DOMParser().parseFromString(
  93. '<?xml version="1.0" encoding="UTF-8"?><FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0"/>',
  94. "application/xml"
  95. );
  96. this.xmldoc.documentElement.setAttribute("xmlns:l", "http://www.w3.org/1999/xlink");
  97. }
  98. }
  99.  
  100. _makeDescriptionElement() {
  101. const desc = this.createElement("description");
  102. // title-info
  103. const t_info = this.createElement("title-info");
  104. desc.appendChild(t_info);
  105. //--
  106. const ch_num = t_info.children.length;
  107. this.genres.forEach(gi => {
  108. if (gi instanceof FB2Genre) {
  109. t_info.appendChild(gi.xml(this));
  110. } else if (typeof(gi) === "string") {
  111. (new FB2GenreList(gi)).forEach(g => t_info.appendChild(g.xml(this)));
  112. }
  113. });
  114. if (t_info.children.length === ch_num) t_info.appendChild((new FB2Genre("network_literature")).xml(this));
  115. //--
  116. (this.bookAuthors.length ? this.bookAuthors : [ new FB2Author("Неизвестный автор") ]).forEach(a => {
  117. t_info.appendChild(a.xml(this));
  118. });
  119. //--
  120. t_info.appendChild((new FB2Element("book-title", this.bookTitle)).xml(this));
  121. //--
  122. if (this.annotation) t_info.appendChild(this.annotation.xml(this));
  123. //--
  124. let keywords = null;
  125. if (Array.isArray(this.keywords) && this.keywords.length) {
  126. keywords = this.keywords.join(", ");
  127. } else if (typeof(this.keywords) === "string" && this.keywords.trim()) {
  128. keywords = this.keywords.trim();
  129. }
  130. if (keywords) t_info.appendChild((new FB2Element("keywords", keywords)).xml(this));
  131. //--
  132. if (this.bookDate) {
  133. const el = this.createElement("date");
  134. el.setAttribute("value", FB2Utils.dateToAtom(this.bookDate));
  135. el.textContent = this.bookDate.getFullYear();
  136. t_info.appendChild(el);
  137. }
  138. //--
  139. if (this.coverpage) {
  140. const el = this.createElement("coverpage");
  141. (Array.isArray(this.coverpage) ? this.coverpage : [ this.coverpage ]).forEach(img => {
  142. el.appendChild(img.xml(this));
  143. });
  144. t_info.appendChild(el);
  145. }
  146. //--
  147. const lang = this.createElement("lang");
  148. lang.textContent = "ru";
  149. t_info.appendChild(lang);
  150. //--
  151. if (this.sequence) {
  152. const el = this.createElement("sequence");
  153. el.setAttribute("name", this.sequence.name);
  154. if (this.sequence.number) el.setAttribute("number", this.sequence.number);
  155. t_info.appendChild(el);
  156. }
  157. // document-info
  158. const d_info = this.createElement("document-info");
  159. desc.appendChild(d_info);
  160. //--
  161. d_info.appendChild((new FB2Author("Ox90")).xml(this));
  162. //--
  163. if (this.programName) d_info.appendChild((new FB2Element("program-used", this.programName)).xml(this));
  164. //--
  165. d_info.appendChild((() => {
  166. const f_time = new Date();
  167. const el = this.createElement("date");
  168. el.setAttribute("value", FB2Utils.dateToAtom(f_time));
  169. el.textContent = f_time.toUTCString();
  170. return el;
  171. })());
  172. //--
  173. if (this.sourceURL) {
  174. d_info.appendChild((new FB2Element("src-url", this.sourceURL)).xml(this));
  175. }
  176. //--
  177. d_info.appendChild((new FB2Element("id", this._genBookId())).xml(this));
  178. //--
  179. d_info.appendChild((new FB2Element("version", "1.0")).xml(this));
  180. //--
  181. return desc;
  182. }
  183.  
  184. _makeBodyElement() {
  185. const body = this.createElement("body");
  186. if (this.bookTitle || this.bookAuthors.length) {
  187. const title = this.createElement("title");
  188. body.appendChild(title);
  189. if (this.bookAuthors.length) title.appendChild((new FB2Paragraph(this.bookAuthors.join(", "))).xml(this));
  190. if (this.bookTitle) title.appendChild((new FB2Paragraph(this.bookTitle)).xml(this));
  191. }
  192. this.chapters.forEach(ch => body.appendChild(ch.xml(this)));
  193. return body;
  194. }
  195.  
  196. _markBinaries() {
  197. let idx = 0;
  198. this.binaries.forEach(img => {
  199. if (!img.id) img.id = "image" + (++idx) + img.suffix();
  200. });
  201. }
  202.  
  203. _makeBinaryElements() {
  204. return this.binaries.reduce((list, img) => {
  205. if (img.value) list.push(img.xmlBinary(this));
  206. return list;
  207. }, []);
  208. }
  209.  
  210. _genBookId() {
  211. let str = this.sourceURL || this.bookTitle || "";
  212. let hash = 0;
  213. const slen = str.length;
  214. for (let i = 0; i < slen; ++i) {
  215. const ch = str.charCodeAt(i);
  216. hash = ((hash << 5) - hash) + ch;
  217. hash = hash & hash; // Convert to 32bit integer
  218. }
  219. return this.idPrefix || "h2f2l_" + Math.abs(hash).toString() + (hash > 0 ? "1" : "");
  220. }
  221. }
  222.  
  223. class FB2Element {
  224. constructor(name, value) {
  225. this.name = name;
  226. this.value = value !== undefined ? value : null;
  227. this.children = [];
  228. }
  229.  
  230. static fromHTML(node, recursive) {
  231. let fb2el = null;
  232. const names = new Map([
  233. [ "U", "emphasis" ], [ "EM", "emphasis" ], [ "EMPHASIS", "emphasis" ], [ "I", "emphasis" ],
  234. [ "S", "strikethrough" ], [ "DEL", "strikethrough" ], [ "STRIKE", "strikethrough" ],
  235. [ "STRONG", "strong" ], [ "BLOCKQUOTE", "cite" ],
  236. [ "SCRIPT", null ], [ "#comment", null ]
  237. ]);
  238. const node_name = node.nodeName;
  239. if (names.has(node_name)) {
  240. const name = names.get(node_name);
  241. if (!name) return null;
  242. fb2el = new FB2Element(names.get(node_name));
  243. } else {
  244. switch (node_name) {
  245. case "#text":
  246. return new FB2Text(node.textContent);
  247. case "SPAN":
  248. fb2el = new FB2Text();
  249. break;
  250. case "P":
  251. case "LI":
  252. fb2el = new FB2Paragraph();
  253. break;
  254. case "SUBTITLE":
  255. fb2el = new FB2Subtitle();
  256. break;
  257. case "A":
  258. fb2el = new FB2Link(node.href || node.getAttribute("l:href"));
  259. break;
  260. case "OL":
  261. fb2el = new FB2OrderedList();
  262. break;
  263. case "UL":
  264. fb2el = new FB2UnorderedList();
  265. break;
  266. case "BR":
  267. return new FB2EmptyLine();
  268. case "HR":
  269. return new FB2Paragraph("---");
  270. case "IMG":
  271. return new FB2Image(node.src);
  272. default:
  273. throw new FB2UnknownNodeError("Неизвестный HTML блок: " + node.nodeName);
  274. }
  275. }
  276. if (recursive) fb2el.appendContentFromHTML(node);
  277. return fb2el;
  278. }
  279.  
  280. hasValue() {
  281. return ((this.value !== undefined && this.value !== null) || !!this.children.length);
  282. }
  283.  
  284. setContentFromHTML(data, fb2doc, log) {
  285. this.children = [];
  286. this.appendContentFromHTML(data, fb2doc, log);
  287. }
  288.  
  289. appendContentFromHTML(data, fb2doc, log) {
  290. for (const node of data.childNodes) {
  291. let fe = FB2Element.fromHTML(node, true);
  292. if (fe) this.children.push(fe);
  293. }
  294. }
  295.  
  296. normalize() {
  297. const _normalize = function(list) {
  298. let done = true;
  299. let res_list = list.reduce((accum, cur_el) => {
  300. accum.push(cur_el);
  301. const tmp_ch = cur_el.children;
  302. cur_el.children = [];
  303. tmp_ch.forEach(el => {
  304. if (el instanceof FB2EmptyLine || el instanceof FB2Subtitle) {
  305. accum.push(el);
  306. const nm = cur_el.name;
  307. cur_el = new cur_el.constructor();
  308. if (!cur_el.name) cur_el.name = nm;
  309. accum.push(cur_el);
  310. done = false;
  311. } else {
  312. let cnt = 0;
  313. el.normalize().forEach(e => {
  314. if (!e.value && e.children.length === 1 && e.name === e.children[0].name) {
  315. e = e.children[0];
  316. }
  317. if (e !== el) done = false;
  318. if (e.hasValue()) cur_el.children.push(e);
  319. });
  320. }
  321. });
  322. return accum;
  323. }, []);
  324. return { list: res_list, done: done };
  325. }
  326. //--
  327. let result = _normalize([ this ]);
  328. while (!result.done) {
  329. result = _normalize(result.list);
  330. }
  331. return result.list;
  332. }
  333.  
  334. xml(doc) {
  335. const el = doc.createElement(this.name);
  336. if (this.value !== null) el.textContent = this.value;
  337. this.children.forEach(ch => el.appendChild(ch.xml(doc)));
  338. return el;
  339. }
  340. }
  341.  
  342. class FB2BlockElement extends FB2Element {
  343. normalize() {
  344. // Предварительная нормализация
  345. this.children = this.children.reduce((list, ch) => {
  346. ch.normalize().forEach(cc => list.push(cc));
  347. return list;
  348. }, []);
  349. // Удалить пустоты справа
  350. while (this.children.length) {
  351. const el = this.children[this.children.length - 1];
  352. if (el instanceof FB2Text) el.trimRight();
  353. if (!el.hasValue()) {
  354. this.children.pop();
  355. continue;
  356. }
  357. break;
  358. }
  359. // Удалить пустоты слева
  360. while (this.children.length) {
  361. const el = this.children[0];
  362. if (el instanceof FB2Text) el.trimLeft();
  363. if (!el.hasValue()) {
  364. this.children.shift();
  365. continue;
  366. }
  367. break;
  368. }
  369. // Окончательная нормализация
  370. return super.normalize();
  371. }
  372. }
  373.  
  374. /**
  375. * FB2 элемент верхнего уровня section
  376. */
  377. class FB2Chapter extends FB2Element {
  378. constructor(title) {
  379. super("section");
  380. this.title = title;
  381. }
  382.  
  383. normalize() {
  384. // Обернуть текстовые ноды в параграфы и удалить пустые элементы
  385. this.children = this.children.reduce((list, el) => {
  386. if (el instanceof FB2Text) {
  387. const pe = new FB2Paragraph();
  388. pe.children.push(el);
  389. el = pe;
  390. }
  391. el.normalize().forEach(el => {
  392. if (el.hasValue()) list.push(el);
  393. });
  394. return list;
  395. }, []);
  396. return [ this ];
  397. }
  398.  
  399. xml(doc) {
  400. const el = super.xml(doc);
  401. if (this.title) {
  402. const t_el = doc.createElement("title");
  403. const p_el = doc.createElement("p");
  404. p_el.textContent = this.title;
  405. t_el.appendChild(p_el);
  406. el.prepend(t_el);
  407. }
  408. return el;
  409. }
  410. }
  411.  
  412. /**
  413. * FB2 элемент верхнего уровня annotation
  414. */
  415. class FB2Annotation extends FB2Element {
  416. constructor() {
  417. super("annotation");
  418. }
  419.  
  420. normalize() {
  421. // Обернуть неформатированный текст, разделенный <br> в параграфы
  422. let lp = null;
  423. const newParagraph = list => {
  424. lp = new FB2Paragraph();
  425. list.push(lp);
  426. };
  427. this.children = this.children.reduce((list, el) => {
  428. if (el.name === "empty-line") {
  429. newParagraph(list);
  430. } else if (el instanceof FB2BlockElement) {
  431. list.push(el);
  432. lp = null;
  433. } else {
  434. if (!lp) newParagraph(list);
  435. lp.children.push(el);
  436. }
  437. return list;
  438. }, []);
  439. // Запустить собственную нормализацию дочерних элементов
  440. // чтобы предотвратить их дальнейшее всплытие
  441. this.children = this.children.reduce((list, el) => {
  442. el.normalize().forEach(el => {
  443. if (el.hasValue()) list.push(el);
  444. });
  445. return list;
  446. }, []);
  447. }
  448. }
  449.  
  450. class FB2Subtitle extends FB2BlockElement {
  451. constructor(value) {
  452. super("subtitle", value);
  453. }
  454. }
  455.  
  456. class FB2Paragraph extends FB2BlockElement {
  457. constructor(value) {
  458. super("p", value);
  459. }
  460. }
  461.  
  462. class FB2EmptyLine extends FB2Element {
  463. constructor() {
  464. super("empty-line");
  465. }
  466.  
  467. hasValue() {
  468. return true;
  469. }
  470. }
  471.  
  472. class FB2Text extends FB2Element {
  473. constructor(value) {
  474. super("text", value);
  475. }
  476.  
  477. trimLeft() {
  478. if (typeof(this.value) === "string") this.value = this.value.trimLeft() || null;
  479. if (!this.value) {
  480. while (this.children.length) {
  481. const first_child = this.children[0];
  482. if (first_child instanceof FB2Text) first_child.trimLeft();
  483. if (first_child.hasValue()) break;
  484. this.children.shift();
  485. }
  486. }
  487. }
  488.  
  489. trimRight() {
  490. while (this.children.length) {
  491. const last_child = this.children[this.children.length - 1];
  492. if (last_child instanceof FB2Text) last_child.trimRight();
  493. if (last_child.hasValue()) break;
  494. this.children.pop();
  495. }
  496. if (!this.children.length && typeof(this.value) === "string") {
  497. this.value = this.value.trimRight() || null;
  498. }
  499. }
  500.  
  501. xml(doc) {
  502. if (!this.value && this.children.length) {
  503. let fr = doc.createDocumentFragment();
  504. for (const ch of this.children) {
  505. fr.appendChild(ch.xml(doc));
  506. }
  507. return fr;
  508. }
  509. return doc.createTextNode(this.value);
  510. }
  511. }
  512.  
  513. class FB2Link extends FB2Element {
  514. constructor(href) {
  515. super("a");
  516. this.href = href;
  517. }
  518.  
  519. xml(doc) {
  520. const el = super.xml(doc);
  521. el.setAttribute("l:href", this.href);
  522. return el;
  523. }
  524. }
  525.  
  526. class FB2OrderedList extends FB2Element {
  527. constructor() {
  528. super("list");
  529. }
  530.  
  531. xml(doc) {
  532. const fr = doc.createDocumentFragment();
  533. let pos = 0;
  534. for (const ch of this.children) {
  535. const ch_el = ch.xml(doc);
  536. if (ch.hasValue()) {
  537. ++pos;
  538. ch_el.prepend(`${pos}. `);
  539. }
  540. fr.appendChild(ch_el);
  541. }
  542. return fr;
  543. }
  544. }
  545.  
  546. class FB2UnorderedList extends FB2Element {
  547. constructor() {
  548. super("list");
  549. }
  550.  
  551. xml(doc) {
  552. const fr = doc.createDocumentFragment();
  553. for (const ch of this.children) {
  554. const ch_el = ch.xml(doc);
  555. if (ch.hasValue()) ch_el.prepend("- ");
  556. fr.appendChild(ch_el);
  557. }
  558. return fr;
  559. }
  560. }
  561.  
  562. class FB2Author extends FB2Element {
  563. constructor(s) {
  564. super("author");
  565. const a = s.split(" ");
  566. switch (a.length) {
  567. case 1:
  568. this.nickName = s;
  569. break;
  570. case 2:
  571. this.firstName = a[0];
  572. this.lastName = a[1];
  573. break;
  574. default:
  575. this.firstName = a[0];
  576. this.middleName = a.slice(1, -1).join(" ");
  577. this.lastName = a[a.length - 1];
  578. break;
  579. }
  580. this.homePage = null;
  581. }
  582.  
  583. hasValue() {
  584. return (!!this.firstName || !!this.lastName || !!this.middleName);
  585. }
  586.  
  587. toString() {
  588. if (!this.firstName) return this.nickName;
  589. return [ this.firstName, this.middleName, this.lastName ].reduce((list, name) => {
  590. if (name) list.push(name);
  591. return list;
  592. }, []).join(" ");
  593. }
  594.  
  595. xml(doc) {
  596. let a_el = super.xml(doc);
  597. [
  598. [ "first-name", this.firstName ], [ "middle-name", this.middleName ],
  599. [ "last-name", this.lastName ], [ "home-page", this.homePage ],
  600. [ "nickname", this.nickName ]
  601. ].forEach(it => {
  602. if (it[1]) {
  603. const e = doc.createElement(it[0]);
  604. e.textContent = it[1];
  605. a_el.appendChild(e);
  606. }
  607. });
  608. return a_el;
  609. }
  610. }
  611.  
  612. class FB2Image extends FB2Element {
  613. constructor(value) {
  614. super("image");
  615. if (typeof(value) === "string") {
  616. this.url = value;
  617. } else {
  618. this.value = value;
  619. }
  620. }
  621.  
  622. async load(onprogress) {
  623. if (this.url) {
  624. const bin = await this._load(this.url, { responseType: "binary", onprogress: onprogress });
  625. this.type = bin.type;
  626. this.size = bin.size;
  627. return new Promise((resolve, reject) => {
  628. const reader = new FileReader();
  629. reader.addEventListener("loadend", (event) => resolve(event.target.result));
  630. reader.readAsDataURL(bin);
  631. }).then(base64str => {
  632. this.value = base64str.substr(base64str.indexOf(",") + 1);
  633. }).catch(err => {
  634. throw new Error("Ошибка загрузки изображения");
  635. });
  636. }
  637. }
  638.  
  639. hasValue() {
  640. return true;
  641. }
  642.  
  643. xml(doc) {
  644. if (this.value) {
  645. const el = doc.createElement(this.name);
  646. el.setAttribute("l:href", "#" + this.id);
  647. return el
  648. }
  649. const id = this.id || "изображение";
  650. return doc.createTextNode(`[ ${id} ]`);
  651. }
  652.  
  653. xmlBinary(doc) {
  654. const el = doc.createElement("binary");
  655. el.setAttribute("id", this.id);
  656. el.setAttribute("content-type", this.type);
  657. el.textContent = this.value
  658. return el;
  659. }
  660.  
  661. suffix() {
  662. switch (this.type) {
  663. case "image/png":
  664. return ".png";
  665. case "image/jpeg":
  666. return ".jpg";
  667. case "image/gif":
  668. return ".gif";
  669. case "image/webp":
  670. return ".webp";
  671. }
  672. return "";
  673. }
  674.  
  675. async _load(...args) {
  676. return FB2Loader.addJob(...args);
  677. }
  678. }
  679.  
  680. class FB2Genre extends FB2Element {
  681. constructor(value) {
  682. super("genre", value);
  683. }
  684. }
  685.  
  686. class FB2GenreList extends Array {
  687. constructor(value) {
  688. super();
  689. if (value === undefined) return;
  690. const keys = FB2GenreList._keys;
  691. const gmap = new Map();
  692. const addWeight = (name, weight) => gmap.set(name, (gmap.get(name) || 0) + weight);
  693.  
  694. (Array.isArray(value) ? value : [ value ]).forEach(p_str => {
  695. p_str = p_str.toLowerCase();
  696. let words = p_str.split(/[\s,.;]+/);
  697. if (words.length === 1) words = [];
  698. for (const it of keys) {
  699. if (it[0] === p_str || it[1] === p_str) {
  700. addWeight(it[0], 3); // Exact match
  701. break;
  702. }
  703. // Scan each word
  704. let weight = words.includes(it[1]) ? 2 : 0;
  705. it[2] && it[2].forEach(k => {
  706. if (words.includes(k)) ++weight;
  707. });
  708. if (weight >= 2) addWeight(it[0], weight);
  709. }
  710. });
  711.  
  712. const res = [];
  713. gmap.forEach((weight, name) => res.push([ name, weight]));
  714. if (!res.length) return;
  715. res.sort((a, b) => b[1] > a[1]);
  716.  
  717. // Add at least five genres with maximum weight
  718. let cur_w = 0;
  719. for (const it of res) {
  720. if (it[1] !== cur_w && this.length >= 5) break;
  721. cur_w = it[1];
  722. this.push(new FB2Genre(it[0]));
  723. }
  724. }
  725. }
  726.  
  727. FB2GenreList._keys = [
  728. [ "adv_animal", "природа и животные", [ "приключения", "животные", "природа" ] ],
  729. [ "adventure", "приключения" ],
  730. [ "adv_geo", "путешествия и география", [ "приключения", "география", "путешествие" ] ],
  731. [ "adv_history", "исторические приключения", [ "история", "приключения" ] ],
  732. [ "adv_indian", "вестерн, про индейцев", [ "индейцы", "вестерн" ] ],
  733. [ "adv_maritime", "морские приключения", [ "приключения", "море" ] ],
  734. [ "adv_modern", "приключения в современном мире", [ "современный", "мир" ] ],
  735. [ "adv_story", "авантюрный роман" ],
  736. [ "antique", "старинное" ],
  737. [ "antique_ant", "античная литература", [ "старинное", "античность" ] ],
  738. [ "antique_east", "древневосточная литература", [ "старинное", "восток" ] ],
  739. [ "antique_european", "европейская старинная литература", [ "старинное", "европа" ] ],
  740. [ "antique_myths", "мифы. легенды. эпос", [ "мифы", "легенды", "эпос", "фольклор" ] ],
  741. [ "antique_russian", "древнерусская литература", [ "древнерусское", "старинное" ] ],
  742. [ "aphorism_quote", "афоризмы, цитаты", [ "афоризмы", "цитаты", "проза" ] ],
  743. [ "architecture_book", "скульптура и архитектура", [ "дизайн" ] ],
  744. [ "art_criticism", "искусствоведение" ],
  745. [ "art_world_culture", "мировая художественная культура", [ "искусство", "искусствоведение" ] ],
  746. [ "astrology", "астрология и хиромантия", [ "астрология", "хиромантия" ] ],
  747. [ "auto_business", "автодело" ],
  748. [ "auto_regulations", "автомобили и ПДД", [ "дорожного", "движения", "дорожное", "движение" ] ],
  749. [ "banking", "финансы", [ "банки", "деньги" ] ],
  750. [ "child_adv", "приключения для детей и подростков" ],
  751. [ "child_classical", "классическая детская литература" ],
  752. [ "child_det", "детская остросюжетная литература" ],
  753. [ "child_education", "детская образовательная литература" ],
  754. [ "child_folklore", "детский фольклор" ],
  755. [ "child_prose", "проза для детей" ],
  756. [ "children", "детская литература", [ "детское" ] ],
  757. [ "child_sf", "фантастика для детей" ],
  758. [ "child_tale", "сказки народов мира" ],
  759. [ "child_tale_rus", "русские сказки" ],
  760. [ "child_verse", "стихи для детей" ],
  761. [ "cine", "кино" ],
  762. [ "comedy", "комедия" ],
  763. [ "comics", "комиксы" ],
  764. [ "comp_db", "программирование, программы, базы данных", [ "программирование", "базы", "программы" ] ],
  765. [ "comp_hard", "компьютерное железо", [ "аппаратное" ] ],
  766. [ "comp_soft", "программное обеспечение" ],
  767. [ "computers", "компьютеры" ],
  768. [ "comp_www", "ос и сети, интернет", [ "ос", "сети", "интернет" ] ],
  769. [ "design", "дизайн" ],
  770. [ "det_action", "боевики", [ "боевик", "триллер" ] ],
  771. [ "det_classic", "классический детектив" ],
  772. [ "det_crime", "криминальный детектив", [ "криминал" ] ],
  773. [ "det_espionage", "шпионский детектив", [ "шпион", "шпионы", "детектив" ] ],
  774. [ "det_hard", "крутой детектив" ],
  775. [ "det_history", "исторический детектив", [ "история" ] ],
  776. [ "det_irony", "иронический детектив" ],
  777. [ "det_maniac", "про маньяков", [ "маньяки", "детектив" ] ],
  778. [ "det_police", "полицейский детектив", [ "полиция", "детектив" ] ],
  779. [ "det_political", "политический детектив", [ "политика", "детектив" ] ],
  780. [ "det_su", "советский детектив", [ "ссср", "детектив" ] ],
  781. [ "detective", "детектив", [ "детективы" ] ],
  782. [ "drama", "драма" ],
  783. [ "drama_antique", "античная драма" ],
  784. [ "dramaturgy", "драматургия" ],
  785. [ "economics", "экономика" ],
  786. [ "economics_ref", "деловая литература" ],
  787. [ "epic", "былины, эпопея", [ "былины", "эпопея" ] ],
  788. [ "epistolary_fiction", "эпистолярная проза" ],
  789. [ "equ_history", "история техники" ],
  790. [ "fairy_fantasy", "мифологическое фэнтези", [ "мифология", "фантастика" ] ],
  791. [ "family", "семейные отношения", [ "дом", "семья" ] ],
  792. [ "fanfiction", "фанфик" ],
  793. [ "folklore", "фольклор, загадки" ],
  794. [ "folk_songs", "народные песни" ],
  795. [ "folk_tale", "народные сказки" ],
  796. [ "foreign_antique", "средневековая классическая проза" ],
  797. [ "foreign_children", "зарубежная литература для детей" ],
  798. [ "foreign_prose", "зарубежная классическая проза" ],
  799. [ "geo_guides", "путеводители, карты, атласы", [ "география", "атласы", "карты", "путеводители" ] ],
  800. [ "gothic_novel", "готический роман" ],
  801. [ "great_story", "роман", [ "повесть" ] ],
  802. [ "home", "домоводство", [ "дом", "семья" ] ],
  803. [ "home_collecting", "коллекционирование" ],
  804. [ "home_cooking", "кулинария", [ "домашняя", "еда" ] ],
  805. [ "home_crafts", "хобби и ремесла" ],
  806. [ "home_diy", "сделай сам" ],
  807. [ "home_entertain", "развлечения" ],
  808. [ "home_garden", "сад и огород" ],
  809. [ "home_health", "здоровье" ],
  810. [ "home_pets", "домашние животные" ],
  811. [ "home_sex", "семейные отношения, секс" ],
  812. [ "home_sport", "боевые исскусства, спорт" ],
  813. [ "hronoopera", "хроноопера" ],
  814. [ "humor", "юмор" ],
  815. [ "humor_anecdote", "анекдоты" ],
  816. [ "humor_prose", "юмористическая проза" ],
  817. [ "humor_satire", "сатира" ],
  818. [ "humor_verse", "юмористические стихи, басни", [ "юмор", "стихи", "басни" ] ],
  819. [ "limerick", [ "частушки", "прибаутки", "потешки" ] ],
  820. [ "literature_18", "классическая проза XVII-XVIII веков" ],
  821. [ "literature_19", "классическая проза ХIX века" ],
  822. [ "literature_20", "классическая проза ХX века" ],
  823. [ "love", "любовные романы" ],
  824. [ "love_contemporary", "современные любовные романы" ],
  825. [ "love_detective", "остросюжетные любовные романы", [ "детектив", "любовь" ] ],
  826. [ "love_erotica", "эротика", [ "эротическая", "литература" ] ],
  827. [ "love_hard", "порно" ],
  828. [ "love_history", "исторические любовные романы", [ "история", "любовь" ] ],
  829. [ "love_sf", "любовное фэнтези" ],
  830. [ "love_short", "короткие любовные романы" ],
  831. [ "lyrics", "лирика" ],
  832. [ "military_history", "военная история", [ "война", "история" ] ],
  833. [ "military_special", "военное дело" ],
  834. [ "military_weapon", "военная техника и вооружение", [ "военная", "вооружение", "техника" ] ],
  835. [ "modern_tale", "современная сказка" ],
  836. [ "music", "музыка" ],
  837. [ "network_literature", "сетевая литература" ],
  838. [ "nonf_biography", "биографии и мемуары", [ "биография", "биографии", "мемуары" ] ],
  839. [ "nonf_criticism", "критика" ],
  840. [ "nonfiction", "документальная литература" ],
  841. [ "nonf_military", "военная документалистика и аналитика" ],
  842. [ "nonf_publicism", "публицистика" ],
  843. [ "notes:", "партитуры" ],
  844. [ "org_behavior", "маркентиг, pr", [ "организации" ] ],
  845. [ "painting", "живопись", [ "альбомы", "иллюстрированные", "каталоги" ] ],
  846. [ "palindromes", "визуальная и экспериментальная поэзия", [ "верлибры", "палиндромы", "поэзия" ] ],
  847. [ "periodic", "журналы, газеты", [ "журналы", "газеты" ]],
  848. [ "poem", "поэма", [ "эпическая", "поэзия" ] ],
  849. [ "poetry", "поэзия" ],
  850. [ "poetry_classical", "классическая поэзия" ],
  851. [ "poetry_east", "поэзия востока" ],
  852. [ "poetry_for_classical", "классическая зарубежная поэзия" ],
  853. [ "poetry_for_modern", "современная зарубежная поэзия" ],
  854. [ "poetry_modern", "современная поэзия" ],
  855. [ "poetry_rus_classical", "классическая русская поэзия" ],
  856. [ "poetry_rus_modern", "современная русская поэзия", [ "русская", "поэзия" ] ],
  857. [ "popadanec", "попаданцы", [ "попаданец" ] ],
  858. [ "popular_business", "карьера, кадры", [ "карьера", "дело", "бизнес" ] ],
  859. [ "prose", "проза" ],
  860. [ "prose_abs", "фантасмагория, абсурдистская проза" ],
  861. [ "prose_classic", "классическая проза" ],
  862. [ "prose_contemporary", "современная русская и зарубежная проза", [ "современная", "проза" ] ],
  863. [ "prose_counter", "контркультура" ],
  864. [ "prose_game", "игры, упражнения для детей", [ "игры", "упражнения" ] ],
  865. [ "prose_history", "историческая проза", [ "история", "проза" ] ],
  866. [ "prose_magic", "магический реализм", [ "магия", "проза" ] ],
  867. [ "prose_military", "проза о войне" ],
  868. [ "prose_neformatny", "неформатная проза", [ "экспериментальная", "проза" ] ],
  869. [ "prose_rus_classic", "русская классическая проза" ],
  870. [ "prose_su_classics", "советская классическая проза" ],
  871. [ "proverbs", "пословицы", [ "поговорки" ] ],
  872. [ "ref_dict", "словари", [ "справочник" ] ],
  873. [ "ref_encyc", "энциклопедии", [ "энциклопедия" ] ],
  874. [ "ref_guide", "руководства", [ "руководство", "справочник" ] ],
  875. [ "ref_ref", "справочники", [ "справочник" ] ],
  876. [ "reference", "справочная литература" ],
  877. [ "religion", "религия", [ "духовность", "эзотерика" ] ],
  878. [ "religion_budda", "буддизм" ],
  879. [ "religion_catholicism", "католицизм" ],
  880. [ "religion_christianity", "христианство" ],
  881. [ "religion_esoterics", "эзотерическая литература", [ "эзотерика" ] ],
  882. [ "religion_hinduism", "индуизм" ],
  883. [ "religion_islam", "ислам" ],
  884. [ "religion_judaism", "иудаизм" ],
  885. [ "religion_orthdoxy", "православие" ],
  886. [ "religion_paganism", "язычество" ],
  887. [ "religion_protestantism", "протестантизм" ],
  888. [ "religion_self", "самосовершенствование" ],
  889. [ "russian_fantasy", "славянское фэнтези", [ "русское", "фэнтези" ] ],
  890. [ "sci_biology", "биология", [ "биофизика", "биохимия" ] ],
  891. [ "sci_botany", "ботаника" ],
  892. [ "sci_build", "строительство и сопромат", [ "строительтво", "сопромат" ] ],
  893. [ "sci_chem", "химия" ],
  894. [ "sci_cosmos", "астрономия и космос", [ "астрономия", "космос" ] ],
  895. [ "sci_culture", "культурология" ],
  896. [ "sci_ecology", "экология" ],
  897. [ "sci_economy", "экономика" ],
  898. [ "science", "научная литература" ],
  899. [ "sci_geo", "геология и география" ],
  900. [ "sci_history", "история" ],
  901. [ "sci_juris", "юриспруденция" ],
  902. [ "sci_linguistic", "языкознание", [ "иностранный", "язык" ] ],
  903. [ "sci_math", "математика" ],
  904. [ "sci_medicine_alternative", "альтернативная медицина" ],
  905. [ "sci_medicine", "медицина" ],
  906. [ "sci_metal", "металлургия" ],
  907. [ "sci_oriental", "востоковедение" ],
  908. [ "sci_pedagogy", "педагогика, воспитание детей, литература для родителей", [ "воспитание", "детей" ] ],
  909. [ "sci_philology", "литературоведение" ],
  910. [ "sci_philosophy", "философия" ],
  911. [ "sci_phys", "физика" ],
  912. [ "sci_politics", "политика" ],
  913. [ "sci_popular", "зарубежная образовательная литература", [ "зарубежная", "научно-популярная" ] ],
  914. [ "sci_psychology", "психология и психотерапия" ],
  915. [ "sci_radio", "радиоэлектроника" ],
  916. [ "sci_religion", "религиоведение", [ "религия", "духовность" ] ],
  917. [ "sci_social_studies", "обществознание", [ "социология" ] ],
  918. [ "sci_state", "государство и право" ],
  919. [ "sci_tech", "технические науки", [ "техника", "наука" ] ],
  920. [ "sci_textbook", "учебники и пособия" ],
  921. [ "sci_theories", "альтернативные науки и научные теории" ],
  922. [ "sci_transport", "транспорт и авиация" ],
  923. [ "sci_veterinary", "ветеринария" ],
  924. [ "sci_zoo", "зоология" ],
  925. [ "science", "научная литература", [ "образование" ] ],
  926. [ "screenplays", "сценарии", [ "сценарий" ] ],
  927. [ "sf", "научная фантастика", [ "наука", "фантастика" ] ],
  928. [ "sf_action", "боевая фантастика" ],
  929. [ "sf_cyberpunk", "киберпанк" ],
  930. [ "sf_detective", "детективная фантастика", [ "детектив", "фантастика" ] ],
  931. [ "sf_epic", "эпическая фантастика", [ "эпическое", "фэнтези" ] ],
  932. [ "sf_etc", "фантастика" ],
  933. [ "sf_fantasy", "фэнтези" ],
  934. [ "sf_fantasy_city", "городское фэнтези" ],
  935. [ "sf_heroic", "героическая фантастика", [ "героическое", "герой", "фэнтези" ] ],
  936. [ "sf_history", "альтернативная история", [ "историческое", "фэнтези" ] ],
  937. [ "sf_horror", "ужасы", [ "фантастика" ] ],
  938. [ "sf_humor", "юмористическая фантастика", [ "юмор", "фантастика" ] ],
  939. [ "sf_litrpg", "гитрпг", [ "litrpg", "рпг" ] ],
  940. [ "sf_mystic", "мистика", [ "мистическая", "фантастика" ] ],
  941. [ "sf_postapocalyptic", "постапокалипсис" ],
  942. [ "sf_realrpg", "реалрпг", [ "realrpg" ] ],
  943. [ "sf_social", "Социально-психологическая фантастика", [ "социум", "психология", "фантастика" ] ],
  944. [ "sf_space", "космическая фантастика", [ "космос", "фантастика" ] ],
  945. [ "sf_stimpank", "стимпанк" ],
  946. [ "sf_technofantasy", "технофэнтези" ],
  947. [ "song_poetry", "песенная поэзия" ],
  948. [ "story", "рассказ", [ "рассказы", "эссе", "новеллы", "новелла", "феерия", "сборник", "рассказов" ] ],
  949. [ "tale_chivalry", "рыцарский роман", [ "рыцари", "приключения" ] ],
  950. [ "tbg_computers", "учебные пособия, самоучители", [ "пособия", "самоучители" ] ],
  951. [ "tbg_higher", "учебники и пособия ВУЗов", [ "учебники", "пособия" ] ],
  952. [ "tbg_school", "школьные учебники и пособия, рефераты, шпаргалки", [ "школьные", "учебники", "шпаргалки", "рефераты" ] ],
  953. [ "tbg_secondary", "учебники и пособия для среднего и специального образования", [ "учебники", "пособия", "образование" ] ],
  954. [ "theatre", "театр" ],
  955. [ "thriller", "триллер", [ "триллеры", "детектив", "детективы" ] ],
  956. [ "tragedy", "трагедия", [ "драматургия" ] ],
  957. [ "travel_notes", " география, путевые заметки", [ "география", "заметки" ] ],
  958. [ "vaudeville", "мистерия", [ "буффонада", "водевиль" ] ],
  959. ];
  960.  
  961. class FB2Loader {
  962. static async addJob(url, params) {
  963. params ||= {};
  964. const fp = {};
  965. fp.method = params.method || "GET";
  966. fp.credentials = "same-origin";
  967. fp.signal = this._getSignal();
  968. const resp = await fetch(url, fp);
  969. if (!resp.ok) throw new Error(`Сервер вернул ошибку (${resp.status})`);
  970. const reader = resp.body.getReader();
  971. const type = resp.headers.get("Content-Type");
  972. const total = +resp.headers.get("Content-Length");
  973. let loaded = 0;
  974. const chunks = [];
  975. const onprogress = (total && typeof(params.onprogress) === "function") ? params.onprogress : null;
  976. while (true) {
  977. const { done, value } = await reader.read();
  978. if (done) break;
  979. chunks.push(value);
  980. loaded += value.length;
  981. if (onprogress) onprogress(loaded, total);
  982. }
  983. switch (params.responseType) {
  984. case "binary":
  985. return new Blob(chunks, { type: type });
  986. default:
  987. {
  988. let pos = 0;
  989. const data = new Uint8Array(loaded);
  990. for (let ch of chunks) {
  991. data.set(ch, pos);
  992. pos += ch.length;
  993. }
  994. return (new TextDecoder("utf-8")).decode(data);
  995. }
  996. }
  997. }
  998.  
  999. static abortAll() {
  1000. if (this._controller) {
  1001. this._controller.abort();
  1002. this._controller = null;
  1003. }
  1004. }
  1005.  
  1006. static _getSignal() {
  1007. let controller = this._controller;
  1008. if (!controller) this._controller = controller = new AbortController();
  1009. return controller.signal;
  1010. }
  1011. }
  1012.  
  1013. class FB2Utils {
  1014. static dateToAtom(date) {
  1015. const m = date.getMonth() + 1;
  1016. const d = date.getDate();
  1017. return "" + date.getFullYear() + '-' + (m < 10 ? "0" : "") + m + "-" + (d < 10 ? "0" : "") + d;
  1018. }
  1019. }
  1020.  
  1021. class FB2UnknownNodeError extends Error {
  1022. constructor(message) {
  1023. super(message);
  1024. this.name = "UnknownNodeError";
  1025. }
  1026. }
  1027.