HTML2FB2Lib

This library is designed to convert HTML to FB2.

目前為 2023-09-10 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.gf.qytechs.cn/scripts/468831/1248498/HTML2FB2Lib.js

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

QingJ © 2025

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