Browse Source

Merge branch 'release/1.2.1'

Book Pauk 2 years ago
parent
commit
b4da07e924

+ 8 - 8
client/components/Search/BaseList.js

@@ -1,4 +1,4 @@
-import moment from 'moment';
+import dayjs from 'dayjs';
 import _ from 'lodash';
 
 import authorBooksStorage from './authorBooksStorage';
@@ -467,13 +467,13 @@ export default class BaseList {
             */
             const sqlFormat = 'YYYY-MM-DD';
             switch (date) {
-                case 'today': date = utils.dateFormat(moment(), sqlFormat); break;
-                case '3days': date = utils.dateFormat(moment().subtract(3, 'days'), sqlFormat); break;
-                case 'week': date = utils.dateFormat(moment().subtract(1, 'weeks'), sqlFormat); break;
-                case '2weeks': date = utils.dateFormat(moment().subtract(2, 'weeks'), sqlFormat); break;
-                case 'month': date = utils.dateFormat(moment().subtract(1, 'months'), sqlFormat); break;
-                case '2months': date = utils.dateFormat(moment().subtract(2, 'months'), sqlFormat); break;
-                case '3months': date = utils.dateFormat(moment().subtract(3, 'months'), sqlFormat); break;
+                case 'today': date = utils.dateFormat(dayjs(), sqlFormat); break;
+                case '3days': date = utils.dateFormat(dayjs().subtract(3, 'days'), sqlFormat); break;
+                case 'week': date = utils.dateFormat(dayjs().subtract(1, 'weeks'), sqlFormat); break;
+                case '2weeks': date = utils.dateFormat(dayjs().subtract(2, 'weeks'), sqlFormat); break;
+                case 'month': date = utils.dateFormat(dayjs().subtract(1, 'months'), sqlFormat); break;
+                case '2months': date = utils.dateFormat(dayjs().subtract(2, 'months'), sqlFormat); break;
+                case '3months': date = utils.dateFormat(dayjs().subtract(3, 'months'), sqlFormat); break;
                 default:
                     date = '';
             }

+ 13 - 11
client/components/Search/BookInfoDialog/BookInfoDialog.vue

@@ -185,7 +185,7 @@ class BookInfoDialog {
                 return utils.sqlDateFormat(value);
 
             if (nodePath == 'fileInfo/del')
-                return (value ? 'Да' : 'Нет');
+                return (value ? 'Да' : '');
 
             if (nodePath == 'titleInfo/author')
                 return value.split(',').join(', ');
@@ -239,7 +239,6 @@ class BookInfoDialog {
 
     parseBookInfo() {
         const bookInfo = this.bookInfo;
-        const parser = new Fb2Parser();
 
         //cover
         if (bookInfo.cover)
@@ -247,16 +246,10 @@ class BookInfoDialog {
 
         //fb2
         if (bookInfo.fb2) {
-            this.fb2 = parser.bookInfoList(bookInfo.fb2, {
-                valueToString(value, nodePath, origVTS) {//eslint-disable-line no-unused-vars
-                    if (nodePath == 'documentInfo/historyHtml' && value)
-                        return value.replace(/<p>/g, `<p class="p-history">`);
+            const parser = new Fb2Parser(bookInfo.fb2);
+
+            const infoObj = parser.bookInfo();
 
-                    return origVTS(value, nodePath);
-                },
-            });
-            
-            const infoObj = parser.bookInfo(bookInfo.fb2);
             if (infoObj.titleInfo) {
                 let ann = infoObj.titleInfo.annotationHtml;
                 if (ann) {
@@ -264,6 +257,15 @@ class BookInfoDialog {
                     this.annotation = ann;
                 }
             }
+
+            this.fb2 = parser.bookInfoList(infoObj, {
+                valueToString(value, nodePath, origVTS) {//eslint-disable-line no-unused-vars
+                    if (nodePath == 'documentInfo/historyHtml' && value)
+                        return value.replace(/<p>/g, `<p class="p-history">`);
+
+                    return origVTS(value, nodePath);
+                },
+            });
         }
 
         //book

+ 2 - 2
client/components/Search/BookView/BookView.vue

@@ -55,8 +55,8 @@
                     {{ bookSize }}, {{ book.ext }}
                 </div>
 
-                <div v-if="showInfo" class="row items-center q-ml-sm clickable" @click="emit('bookInfo')">
-                    [ . . . ]
+                <div v-if="showInfo" class="q-ml-sm clickable" @click="emit('bookInfo')">
+                    (инфо)
                 </div>
 
                 <div class="q-ml-sm clickable" @click="emit('download')">

+ 2 - 2
client/components/Search/Search.vue

@@ -204,7 +204,7 @@
             </div>
 
             <div class="row justify-center">
-                <div class="q-mb-sm q-px-sm q-py-xs bg-cyan-2 clickable2" style="border: 1px solid #aaaaaa; border-radius: 6px; white-space: nowrap;" @click="openReleasePage">
+                <div class="q-mb-xl q-px-sm q-py-xs bg-cyan-2 clickable2" style="border: 1px solid #aaaaaa; border-radius: 6px; white-space: nowrap;" @click="openReleasePage">
                     {{ projectName }}
                 </div>
             </div>
@@ -232,7 +232,7 @@
 
                 <q-checkbox v-model="showCounts" size="36px" label="Показывать количество" />                
                 <q-checkbox v-model="showRates" size="36px" label="Показывать оценки" />
-                <q-checkbox v-model="showInfo" size="36px" label="Показывать кнопку 'инфо'" />
+                <q-checkbox v-model="showInfo" size="36px" label="Показывать кнопку (инфо)" />
                 <q-checkbox v-model="showGenres" size="36px" label="Показывать жанры" />
                 <q-checkbox v-model="showDates" size="36px" label="Показывать даты поступления" />
                 <q-checkbox v-model="showDeleted" size="36px" label="Показывать удаленные" />

+ 3 - 3
client/share/utils.js

@@ -1,4 +1,4 @@
-import moment from 'moment';
+import dayjs from 'dayjs';
 import {Buffer} from 'safe-buffer';
 //import _ from 'lodash';
 
@@ -121,11 +121,11 @@ export function isDigit(c) {
 }
 
 export function dateFormat(date, format = 'DD.MM.YYYY') {
-    return moment(date).format(format);
+    return dayjs(date).format(format);
 }
 
 export function sqlDateFormat(date, format = 'DD.MM.YYYY') {
-    return moment(date, 'YYYY-MM-DD').format(format);
+    return dayjs(date, 'YYYY-MM-DD').format(format);
 }
 
 export function isManualDate(date) {

+ 13 - 16
package-lock.json

@@ -1,18 +1,19 @@
 {
   "name": "inpx-web",
-  "version": "1.2.0",
+  "version": "1.2.1",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "inpx-web",
-      "version": "1.2.0",
+      "version": "1.2.1",
       "hasInstallScript": true,
       "license": "CC0-1.0",
       "dependencies": {
         "@quasar/extras": "^1.15.0",
         "axios": "^0.27.2",
         "chardet": "^1.5.0",
+        "dayjs": "^1.11.6",
         "express": "^4.18.1",
         "fs-extra": "^10.1.0",
         "iconv-lite": "^0.6.3",
@@ -20,7 +21,6 @@
         "localforage": "^1.10.0",
         "lodash": "^4.17.21",
         "minimist": "^1.2.6",
-        "moment": "^2.29.4",
         "node-stream-zip": "^1.15.0",
         "quasar": "^2.7.5",
         "safe-buffer": "^5.2.1",
@@ -3433,6 +3433,11 @@
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.20.tgz",
       "integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA=="
     },
+    "node_modules/dayjs": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.6.tgz",
+      "integrity": "sha512-zZbY5giJAinCG+7AGaw0wIhNZ6J8AhWuSXKvuc1KAyMiRsvGQWqh4L+MomvhdAYjN+lqvVCMq1I41e3YHvXkyQ=="
+    },
     "node_modules/debug": {
       "version": "4.3.4",
       "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -5519,14 +5524,6 @@
       "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
       "dev": true
     },
-    "node_modules/moment": {
-      "version": "2.29.4",
-      "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
-      "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
-      "engines": {
-        "node": "*"
-      }
-    },
     "node_modules/ms": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -11324,6 +11321,11 @@
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.20.tgz",
       "integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA=="
     },
+    "dayjs": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.6.tgz",
+      "integrity": "sha512-zZbY5giJAinCG+7AGaw0wIhNZ6J8AhWuSXKvuc1KAyMiRsvGQWqh4L+MomvhdAYjN+lqvVCMq1I41e3YHvXkyQ=="
+    },
     "debug": {
       "version": "4.3.4",
       "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -12879,11 +12881,6 @@
       "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
       "dev": true
     },
-    "moment": {
-      "version": "2.29.4",
-      "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
-      "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
-    },
     "ms": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",

+ 2 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "inpx-web",
-  "version": "1.2.0",
+  "version": "1.2.1",
   "author": "Book Pauk <bookpauk@gmail.com>",
   "license": "CC0-1.0",
   "repository": "bookpauk/inpx-web",
@@ -52,6 +52,7 @@
     "@quasar/extras": "^1.15.0",
     "axios": "^0.27.2",
     "chardet": "^1.5.0",
+    "dayjs": "^1.11.6",
     "express": "^4.18.1",
     "fs-extra": "^10.1.0",
     "iconv-lite": "^0.6.3",
@@ -59,7 +60,6 @@
     "localforage": "^1.10.0",
     "lodash": "^4.17.21",
     "minimist": "^1.2.6",
-    "moment": "^2.29.4",
     "node-stream-zip": "^1.15.0",
     "quasar": "^2.7.5",
     "safe-buffer": "^5.2.1",

+ 15 - 10
server/core/WebWorker.js

@@ -369,7 +369,7 @@ class WebWorker {
 
         const link = `${this.config.filesPathStatic}/${hash}`;
         const bookFile = `${this.config.filesDir}/${hash}`;
-        const bookFileDesc = `${bookFile}.json`;
+        const bookFileDesc = `${bookFile}.d.json`;
 
         if (!await fs.pathExists(bookFile) || !await fs.pathExists(bookFileDesc)) {
             if (!await fs.pathExists(bookFile) && extractedFile) {
@@ -435,7 +435,7 @@ class WebWorker {
             if (rows.length) {//хеш найден по bookPath
                 const hash = rows[0].hash;
                 const bookFile = `${this.config.filesDir}/${hash}`;
-                const bookFileDesc = `${bookFile}.json`;
+                const bookFileDesc = `${bookFile}.d.json`;
 
                 if (await fs.pathExists(bookFile) && await fs.pathExists(bookFileDesc)) {
                     link = `${this.config.filesPathStatic}/${hash}`;
@@ -467,9 +467,9 @@ class WebWorker {
             let bookInfo = await this.getBookLink(bookId);
             const hash = path.basename(bookInfo.link);
             const bookFile = `${this.config.filesDir}/${hash}`;
-            const bookFileInfo = `${bookFile}.info`;
+            const bookFileInfo = `${bookFile}.i.json`;
 
-            const restoreBookInfo = async() => {
+            const restoreBookInfo = async(info) => {
                 const result = {};
 
                 const rows = await db.select({table: 'book', where: `@@id(${db.esc(bookId)})`});
@@ -478,10 +478,12 @@ class WebWorker {
                 result.book = book;
                 result.cover = '';
                 result.fb2 = false;
+                let parser = null;
 
                 if (book.ext == 'fb2') {
                     const {fb2, cover, coverExt} = await this.fb2Helper.getDescAndCover(bookFile);
-                    result.fb2 = fb2;
+                    parser = fb2;
+                    result.fb2 = fb2.rawNodes;
 
                     if (cover) {
                         result.cover = `${this.config.filesPathStatic}/${hash}${coverExt}`;
@@ -489,12 +491,16 @@ class WebWorker {
                     }
                 }
 
-                return result;
+                Object.assign(info ,result);
+                await fs.writeFile(bookFileInfo, JSON.stringify(info));
+
+                if (this.config.branch === 'development') {
+                    await fs.writeFile(`${bookFile}.dev`, `${JSON.stringify(info, null, 2)}\n\n${parser ? parser.toString({format: true}) : ''}`);
+                }
             };
 
             if (!await fs.pathExists(bookFileInfo)) {
-                Object.assign(bookInfo, await restoreBookInfo());
-                await fs.writeFile(bookFileInfo, JSON.stringify(bookInfo, null, 2));
+                await restoreBookInfo(bookInfo);
             } else {
                 await utils.touchFile(bookFileInfo);
                 const info = await fs.readFile(bookFileInfo, 'utf-8');
@@ -506,8 +512,7 @@ class WebWorker {
                     coverFile = `${this.config.publicFilesDir}${tmpInfo.cover}`;
 
                 if (coverFile && !await fs.pathExists(coverFile)) {
-                    Object.assign(bookInfo, await restoreBookInfo());
-                    await fs.writeFile(bookFileInfo, JSON.stringify(bookInfo, null, 2));
+                    await restoreBookInfo(bookInfo);
                 } else {
                     bookInfo = tmpInfo;
                 }

+ 6 - 10
server/core/fb2/Fb2Helper.js

@@ -63,12 +63,11 @@ class Fb2Helper {
             pickNode: route => route.indexOf('fictionbook/body') !== 0,
         });
 
-        const desc = parser.$$('description').toObject();
-        const coverImage = parser.inspector(desc).$('description/title-info/coverpage/image');
+        const coverImage = parser.$$('/description/title-info/coverpage/image');
 
         let cover = null;
         let coverExt = '';
-        if (coverImage) {
+        if (coverImage.count) {
             const coverAttrs = coverImage.attrs();
             const href = coverAttrs[`${parser.xlinkNS}:href`];
             let coverType = coverAttrs['content-type'];
@@ -79,24 +78,21 @@ class Fb2Helper {
                 const binaryId = (href[0] == '#' ? href.substring(1) : href);
 
                 //найдем нужный image
-                parser.$$('binary').eachSelf(node => {
+                for (const node of parser.$$array('/binary')) {
                     let attrs = node.attrs();
                     if (!attrs)
                         return;
-                    attrs = Object.fromEntries(attrs);
 
                     if (attrs.id === binaryId) {
-                        const textNode = new Fb2Parser(node.value);
-                        const base64 = textNode.$self('*TEXT').value;
-
+                        const base64 = node.text();
                         cover = (base64 ? Buffer.from(base64, 'base64') : null);
                     }
-                });
+                }
             }
         }
 
         parser.remove('binary');
-        return {fb2: parser.toObject(), cover, coverExt};
+        return {fb2: parser, cover, coverExt};
     }
 }
 

+ 19 - 22
server/core/fb2/Fb2Parser.js

@@ -3,7 +3,7 @@ const XmlParser = require('../xml/XmlParser');
 class Fb2Parser extends XmlParser {
     get xlinkNS() {
         if (!this._xlinkNS) {
-            const rootAttrs = this.$self().attrs();
+            const rootAttrs = this.selectFirstSelf().attrs();
             let ns = 'l';
             for (const [key, value] of rootAttrs) {
                 if (value == 'http://www.w3.org/1999/xlink') {
@@ -18,27 +18,24 @@ class Fb2Parser extends XmlParser {
         return this._xlinkNS;
     }
 
-    bookInfo(fb2Object) {
+    bookInfo() {
         const result = {};
 
-        if (!fb2Object)
-            fb2Object = this.toObject();
-
-        const desc = this.inspector(fb2Object).$('fictionbook/description');
+        const desc = this.$$('/description/');
 
         if (!desc)
             return result;
 
         const parseAuthors = (node, tagName) => {
             const authors = [];
-            for (const a of node.$$(tagName)) {
+            for (const a of node.$$array(tagName)) {
                 let names = [];
-                names.push(a.text('last-name'));
-                names.push(a.text('first-name'));
-                names.push(a.text('middle-name'));
+                names.push(a.text('/last-name'));
+                names.push(a.text('/first-name'));
+                names.push(a.text('/middle-name'));
                 names = names.filter(n => n);
                 if (!names.length)
-                    names.push(a.text('nickname'));
+                    names.push(a.text('/nickname'));
 
                 authors.push(names.join(' '));
             }
@@ -48,7 +45,7 @@ class Fb2Parser extends XmlParser {
 
         const parseSequence = (node, tagName) => {
             const sequence = [];
-            for (const s of node.$$(tagName)) {
+            for (const s of node.$$array(tagName)) {
                 const seqAttrs = s.attrs() || {};
                 const name = seqAttrs['name'] || null;
                 const num = seqAttrs['number'] || null;
@@ -64,7 +61,7 @@ class Fb2Parser extends XmlParser {
             const info = {};
 
             info.genre = [];
-            for (const g of titleInfo.$$('genre'))
+            for (const g of titleInfo.$$array('genre'))
                 info.genre.push(g.text());
 
             info.author = parseAuthors(titleInfo, 'author');
@@ -77,7 +74,7 @@ class Fb2Parser extends XmlParser {
             info.annotationHtml = null;
             if (info.annotation) {
                 //annotation как кусок xml
-                info.annotationXml = (new XmlParser()).fromObject(info.annotation).toString({noHeader: true});
+                info.annotationXml = titleInfo.$$('annotation/').toString({noHeader: true});
 
                 //annotation как html
                 info.annotationHtml = this.toHtml(info.annotationXml);
@@ -97,19 +94,19 @@ class Fb2Parser extends XmlParser {
         }
 
         //title-info
-        const titleInfo = desc.$('title-info');
+        const titleInfo = desc.$$('title-info/');
         if (titleInfo) {
             result.titleInfo = parseTitleInfo(titleInfo);
         }
 
         //src-title-info
-        const srcTitleInfo = desc.$('src-title-info');
+        const srcTitleInfo = desc.$$('src-title-info/');
         if (srcTitleInfo) {
             result.srcTitleInfo = parseTitleInfo(srcTitleInfo);
         }
 
         //document-info
-        const documentInfo = desc.$('document-info');
+        const documentInfo = desc.$$('document-info/');
         if (documentInfo) {
             const info = {};
 
@@ -118,7 +115,7 @@ class Fb2Parser extends XmlParser {
             info.date = documentInfo.text('date');
 
             info.srcUrl = [];
-            for (const url of documentInfo.$$('src-url'))
+            for (const url of documentInfo.$$array('src-url'))
                 info.srcUrl.push(url.text());
 
             info.srcOcr = documentInfo.text('src-ocr');
@@ -131,7 +128,7 @@ class Fb2Parser extends XmlParser {
             info.historyHtml = null;
             if (info.history) {
                 //history как кусок xml
-                info.historyXml = (new XmlParser()).fromObject(info.history).toString({noHeader: true});
+                info.historyXml = documentInfo.$$('history/').toString({noHeader: true});
 
                 //history как html
                 info.historyHtml = this.toHtml(info.historyXml);
@@ -143,7 +140,7 @@ class Fb2Parser extends XmlParser {
         }
 
         //publish-info
-        const publishInfo = desc.$('publish-info');
+        const publishInfo = desc.$$('publish-info/');
         if (publishInfo) {
             const info = {};
 
@@ -160,7 +157,7 @@ class Fb2Parser extends XmlParser {
         return result;
     }
 
-    bookInfoList(fb2Object, options = {}) {
+    bookInfoList(bookInfo, options = {}) {
         let {
             correctMapping = false,
             valueToString = false,
@@ -236,7 +233,7 @@ class Fb2Parser extends XmlParser {
         ];
 
         mapping = correctMapping(mapping);
-        const bookInfo = this.bookInfo(fb2Object);
+        bookInfo = (bookInfo ? bookInfo : this.bookInfo());
 
         //заполняем mapping
         let result = [];

+ 3 - 7
server/core/xml/ObjectInspector.js

@@ -3,7 +3,7 @@ class ObjectInspector {
         this.raw = raw;
     }
 
-    makeSelector(selector) {
+    narrowSelector(selector) {
         const result = [];
         selector = selector.trim();
         
@@ -31,7 +31,7 @@ class ObjectInspector {
     }
 
     select(selector = '') {
-        selector = this.makeSelector(selector);
+        selector = this.narrowSelector(selector);
 
         let raw = this.raw;
         for (const s of selector) {
@@ -50,14 +50,10 @@ class ObjectInspector {
             }
 
             if (raw === undefined || raw === null) {
-                raw = null;
-                break;
+                return [];
             }
         }
 
-        if (raw === null)
-            return [];
-
         raw = (Array.isArray(raw) ? raw : [raw]);
 
         const result = [];

+ 148 - 23
server/core/xml/XmlParser.js

@@ -1,5 +1,4 @@
 const sax = require('./sax');
-const ObjectInspector = require('./ObjectInspector');
 
 //node types
 const NODE = 1;
@@ -22,7 +21,7 @@ const type2name = {
 };
 
 class NodeBase {
-    makeSelectorObj(selectorString) {
+    wideSelector(selectorString) {
         const result = {all: false, before: false, type: 0, name: ''};
 
         if (selectorString === '') {
@@ -153,7 +152,7 @@ class NodeObject extends NodeBase {
         if (this.type !== NODE)
             return;
 
-        const selectorObj = this.makeSelectorObj(after);
+        const selectorObj = this.wideSelector(after);
 
         if (!Array.isArray(this.raw[3]))
             this.raw[3] = [];
@@ -172,7 +171,7 @@ class NodeObject extends NodeBase {
         if (this.type !== NODE || !this.raw[3])
             return;
 
-        const selectorObj = this.makeSelectorObj(selector);
+        const selectorObj = this.wideSelector(selector);
 
         this.rawRemove(this.raw[3], selectorObj);
         if (!this.raw[3].length)
@@ -233,6 +232,14 @@ class XmlParser extends NodeBase {
         return this.rawNodes.length;
     }
 
+    get nodes() {
+        const result = [];
+        for (const n of this.rawNodes)
+            result.push(new NodeObject(n));
+
+        return result;
+    }
+
     nodeObject(node) {
         return new NodeObject(node);
     }
@@ -279,7 +286,7 @@ class XmlParser extends NodeBase {
     }
 
     add(node, after = '*') {
-        const selectorObj = this.makeSelectorObj(after);
+        const selectorObj = this.wideSelector(after);
 
         for (const n of this.rawNodes) {
             if (n && n[0] === NODE) {
@@ -299,7 +306,7 @@ class XmlParser extends NodeBase {
     }
 
     addRoot(node, after = '*') {
-        const selectorObj = this.makeSelectorObj(after);
+        const selectorObj = this.wideSelector(after);
 
         if (Array.isArray(node)) {
             for (const node_ of node)
@@ -312,7 +319,7 @@ class XmlParser extends NodeBase {
     }
 
     remove(selector = '') {
-        const selectorObj = this.makeSelectorObj(selector);
+        const selectorObj = this.wideSelector(selector);
 
         for (const n of this.rawNodes) {
             if (n && n[0] === NODE && Array.isArray(n[3])) {
@@ -326,7 +333,7 @@ class XmlParser extends NodeBase {
     }
 
     removeRoot(selector = '') {
-        const selectorObj = this.makeSelectorObj(selector);
+        const selectorObj = this.wideSelector(selector);
 
         this.rawRemove(this.rawNodes, selectorObj);
 
@@ -409,7 +416,7 @@ class XmlParser extends NodeBase {
 
             newRawNodes = res.rawNodes;
         } else {
-            const selectorObj = this.makeSelectorObj(selector);
+            const selectorObj = this.wideSelector(selector);
 
             if (self) {
                 this.rawSelect(this.rawNodes, selectorObj, (node) => {
@@ -429,11 +436,7 @@ class XmlParser extends NodeBase {
         return new XmlParser(newRawNodes);
     }
 
-    $$(selector, self) {
-        return this.select(selector, self);
-    }
-
-    $$self(selector) {
+    selectSelf(selector) {
         return this.select(selector, true);
     }
 
@@ -443,11 +446,7 @@ class XmlParser extends NodeBase {
         return new NodeObject(node);
     }
 
-    $(selector, self) {
-        return this.selectFirst(selector, self);
-    }
-
-    $self(selector) {
+    selectFirstSelf(selector) {
         return this.selectFirst(selector, true);
     }
 
@@ -760,12 +759,138 @@ class XmlParser extends NodeBase {
         return this;
     }
 
-    inspector(obj) {
-        if (!obj)
-            obj = this.toObject();
+    // XML Inspector start
+    narrowSelector(selector) {
+        const result = [];
+        selector = selector.trim();
+        
+        //последний индекс не учитывется, только если не задан явно
+        if (selector && selector[selector.length - 1] == ']')
+            selector += '/';
+
+        const levels = selector.split('/');
+
+        for (const level of levels) {
+            const [name, indexPart] = level.split('[');
+            let index = 0;
+            if (indexPart) {
+                const i = indexPart.indexOf(']');
+                index = parseInt(indexPart.substring(0, i), 10) || 0;
+            }
+
+            let type = NODE;
+            if (name[0] === '*') {
+                const typeName = name.substring(1);
+                type = name2type[typeName];
+                if (!type)
+                    throw new Error(`Unknown selector type: ${typeName}`);
+            }
+
+            result.push({type, name, index});
+        }
+
+        if (result.length);
+            result[result.length - 1].last = true;
+
+        return result;
+    }
+
+    inspect(selector = '') {
+        selector = this.narrowSelector(selector);
+
+        let raw = this.rawNodes;
+        for (const s of selector) {
+            if (s.name) {
+                let found = [];
+                for (const n of raw) {
+                    if (n[0] === s.type && (n[0] !== NODE || s.name === '*NODE' || n[1] === s.name)) {
+                        found.push(n);
+
+                        if (found.length > s.index && !s.last)
+                            break;
+                    }
+                }
+
+                raw = found;
+            }
+
+            if (raw.length && !s.last) {
+                if (s.index < raw.length) {
+                    raw = raw[s.index];
+                    if (raw[0] === NODE && raw[3])
+                        raw = raw[3];
+                    else {
+                        raw = [];
+                        break;
+                    }
+                } else {
+                    raw = [];
+                    break;
+                }
+            }
+        }
+
+        return new XmlParser(raw);
+    }
+
+    $$(selector) {
+        return this.inspect(selector);
+    }
+
+    $$array(selector) {
+        const res = this.inspect(selector);
+        const result = [];
+        for (const n of res.rawNodes)
+            if (n[0] === NODE)
+                result.push(new XmlParser([n]));
+
+        return result;
+    }
+
+    $(selector) {
+        const res = this.inspect(selector);
+        const node = (res.count ? res.rawNodes[0] : null);
+        return new NodeObject(node);
+    }
+
+    v(selector = '') {
+        const res = this.$(selector);
+        return (res.type ? res.value : null);
+    }
+
+    text(selector = '') {
+        const res = this.$(`${selector}/*TEXT`);
+        return (res.type === TEXT ? res.value : null);
+    }
+
+    comment(selector = '') {
+        const res = this.$(`${selector}/*COMMENT`);
+        return (res.type === COMMENT ? res.value : null);
+    }
+
+    cdata(selector = '') {
+        const res = this.$(`${selector}/*CDATA`);
+        return (res.type === CDATA ? res.value : null);
+    }
+
+    concat(selector = '') {
+        const res = this.$$(selector);
+        const out = [];
+        for (const n of res.rawNodes) {
+            const node = new NodeObject(n);
+            if (node.type && node.type !== NODE)
+                out.push(node.value);
+        }
+
+        return (out.length ? out.join('') : null);
+    }
 
-        return new ObjectInspector(obj);
+    attrs(selector = '') {
+        const res = this.$(selector);
+        const attrs = res.attrs();
+        return (res.type === NODE && attrs ? Object.fromEntries(attrs) : null);
     }
+    // XML Inspector finish
 }
 
 module.exports = XmlParser;

+ 1 - 1
server/index.js

@@ -191,7 +191,7 @@ function initStatic(app, config) {
 
         if (path.extname(req.path) == '') {
             const bookFile = `${config.publicFilesDir}${req.path}`;
-            const bookFileDesc = `${bookFile}.json`;
+            const bookFileDesc = `${bookFile}.d.json`;
 
             let downFileName = '';
             //восстановим из json-файла описания