HTML2FB2Lib

This is a library for converting HTML to FB2.

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/468831/1575776/HTML2FB2Lib.js

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