mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-11-07 16:44:16 +01:00
Start of new epub reader
This commit is contained in:
parent
70ba2f7850
commit
44363f05ac
88
client/components/readers/EpubReader2.vue
Normal file
88
client/components/readers/EpubReader2.vue
Normal file
@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div class="h-full w-full">
|
||||
<div id="viewer" class="border border-gray-100 bg-white text-black shadow-md h-screen overflow-y-auto p-4" v-html="pageHtml"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
url: String,
|
||||
libraryItem: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
bookInfo: {},
|
||||
page: 0,
|
||||
numPages: 0,
|
||||
pageHtml: '',
|
||||
progress: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
libraryItemId() {
|
||||
return this.libraryItem ? this.libraryItem.id : null
|
||||
},
|
||||
hasPrev() {
|
||||
return this.page > 0
|
||||
},
|
||||
hasNext() {
|
||||
return this.page < this.numPages - 1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
prev() {
|
||||
if (!this.hasPrev) return
|
||||
this.page--
|
||||
this.loadPage()
|
||||
},
|
||||
next() {
|
||||
if (!this.hasNext) return
|
||||
this.page++
|
||||
this.loadPage()
|
||||
},
|
||||
keyUp() {
|
||||
if ((e.keyCode || e.which) == 37) {
|
||||
this.prev()
|
||||
} else if ((e.keyCode || e.which) == 39) {
|
||||
this.next()
|
||||
}
|
||||
},
|
||||
loadPage() {
|
||||
this.$axios
|
||||
.$get(`/api/ebooks/${this.libraryItemId}/page/${this.page}?dev=${this.$isDev ? 1 : 0}`)
|
||||
.then((html) => {
|
||||
this.pageHtml = html
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to load page', error)
|
||||
this.$toast.error('Failed to load page')
|
||||
})
|
||||
},
|
||||
loadInfo() {
|
||||
this.$axios
|
||||
.$get(`/api/ebooks/${this.libraryItemId}/info?dev=${this.$isDev ? 1 : 0}`)
|
||||
.then((bookInfo) => {
|
||||
this.bookInfo = bookInfo
|
||||
this.numPages = bookInfo.pages
|
||||
this.page = 0
|
||||
this.loadPage()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to load page', error)
|
||||
this.$toast.error('Failed to load info')
|
||||
})
|
||||
},
|
||||
initEpub() {
|
||||
if (!this.libraryItemId) return
|
||||
this.loadInfo()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initEpub()
|
||||
}
|
||||
}
|
||||
</script>
|
@ -9,7 +9,7 @@
|
||||
<p v-if="abAuthor">by {{ abAuthor }}</p>
|
||||
</div>
|
||||
|
||||
<component v-if="componentName" ref="readerComponent" :is="componentName" :url="ebookUrl" />
|
||||
<component v-if="componentName" ref="readerComponent" :is="componentName" :url="ebookUrl" :library-item="selectedLibraryItem" />
|
||||
|
||||
<div class="absolute bottom-2 left-2">{{ ebookType }}</div>
|
||||
</div>
|
||||
@ -37,7 +37,8 @@ export default {
|
||||
}
|
||||
},
|
||||
componentName() {
|
||||
if (this.ebookType === 'epub') return 'readers-epub-reader'
|
||||
if (this.ebookType === 'epub' && this.$isDev) return 'readers-epub-reader2'
|
||||
else if (this.ebookType === 'epub') return 'readers-epub-reader'
|
||||
else if (this.ebookType === 'mobi') return 'readers-mobi-reader'
|
||||
else if (this.ebookType === 'pdf') return 'readers-pdf-reader'
|
||||
else if (this.ebookType === 'comic') return 'readers-comic-reader'
|
||||
|
@ -34,6 +34,7 @@ const AudioMetadataMangaer = require('./managers/AudioMetadataManager')
|
||||
const RssFeedManager = require('./managers/RssFeedManager')
|
||||
const CronManager = require('./managers/CronManager')
|
||||
const TaskManager = require('./managers/TaskManager')
|
||||
const EBookManager = require('./managers/EBookManager')
|
||||
|
||||
class Server {
|
||||
constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) {
|
||||
@ -77,6 +78,7 @@ class Server {
|
||||
this.podcastManager = new PodcastManager(this.db, this.watcher, this.notificationManager)
|
||||
this.audioMetadataManager = new AudioMetadataMangaer(this.db, this.taskManager)
|
||||
this.rssFeedManager = new RssFeedManager(this.db)
|
||||
this.eBookManager = new EBookManager(this.db)
|
||||
|
||||
this.scanner = new Scanner(this.db, this.coverManager)
|
||||
this.cronManager = new CronManager(this.db, this.scanner, this.podcastManager)
|
||||
|
52
server/controllers/EBookController.js
Normal file
52
server/controllers/EBookController.js
Normal file
@ -0,0 +1,52 @@
|
||||
const Logger = require('../Logger')
|
||||
const { isNullOrNaN } = require('../utils/index')
|
||||
|
||||
class EBookController {
|
||||
constructor() { }
|
||||
|
||||
async getEbookInfo(req, res) {
|
||||
const isDev = req.query.dev == 1
|
||||
const json = await this.eBookManager.getBookInfo(req.libraryItem, req.user, isDev)
|
||||
res.json(json)
|
||||
}
|
||||
|
||||
async getEbookPage(req, res) {
|
||||
if (isNullOrNaN(req.params.page)) {
|
||||
return res.status(400).send('Invalid page params')
|
||||
}
|
||||
const isDev = req.query.dev == 1
|
||||
const pageIndex = Number(req.params.page)
|
||||
const page = await this.eBookManager.getBookPage(req.libraryItem, req.user, pageIndex, isDev)
|
||||
if (!page) {
|
||||
return res.status(500).send('Failed to get page')
|
||||
}
|
||||
|
||||
res.send(page)
|
||||
}
|
||||
|
||||
async getEbookResource(req, res) {
|
||||
if (!req.query.path) {
|
||||
return res.status(400).send('Invalid query path')
|
||||
}
|
||||
const isDev = req.query.dev == 1
|
||||
this.eBookManager.getBookResource(req.libraryItem, req.user, req.query.path, isDev, res)
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
const item = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||
if (!item || !item.media) return res.sendStatus(404)
|
||||
|
||||
// Check user can access this library item
|
||||
if (!req.user.checkCanAccessLibraryItem(item)) {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
if (!item.isBook || !item.media.ebookFile) {
|
||||
return res.status(400).send('Invalid ebook library item')
|
||||
}
|
||||
|
||||
req.libraryItem = item
|
||||
next()
|
||||
}
|
||||
}
|
||||
module.exports = new EBookController()
|
9
server/libs/css/LICENSE
Normal file
9
server/libs/css/LICENSE
Normal file
@ -0,0 +1,9 @@
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2012 TJ Holowaychuk <tj@vision-media.ca>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
2
server/libs/css/index.js
Normal file
2
server/libs/css/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
exports.parse = require('./parse');
|
||||
exports.stringify = require('./stringify');
|
603
server/libs/css/parse/index.js
Normal file
603
server/libs/css/parse/index.js
Normal file
@ -0,0 +1,603 @@
|
||||
// http://www.w3.org/TR/CSS21/grammar.html
|
||||
// https://github.com/visionmedia/css-parse/pull/49#issuecomment-30088027
|
||||
var commentre = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//g
|
||||
|
||||
module.exports = function(css, options){
|
||||
options = options || {};
|
||||
|
||||
/**
|
||||
* Positional.
|
||||
*/
|
||||
|
||||
var lineno = 1;
|
||||
var column = 1;
|
||||
|
||||
/**
|
||||
* Update lineno and column based on `str`.
|
||||
*/
|
||||
|
||||
function updatePosition(str) {
|
||||
var lines = str.match(/\n/g);
|
||||
if (lines) lineno += lines.length;
|
||||
var i = str.lastIndexOf('\n');
|
||||
column = ~i ? str.length - i : column + str.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark position and patch `node.position`.
|
||||
*/
|
||||
|
||||
function position() {
|
||||
var start = { line: lineno, column: column };
|
||||
return function(node){
|
||||
node.position = new Position(start);
|
||||
whitespace();
|
||||
return node;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Store position information for a node
|
||||
*/
|
||||
|
||||
function Position(start) {
|
||||
this.start = start;
|
||||
this.end = { line: lineno, column: column };
|
||||
this.source = options.source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-enumerable source string
|
||||
*/
|
||||
|
||||
Position.prototype.content = css;
|
||||
|
||||
/**
|
||||
* Error `msg`.
|
||||
*/
|
||||
|
||||
var errorsList = [];
|
||||
|
||||
function error(msg) {
|
||||
var err = new Error(options.source + ':' + lineno + ':' + column + ': ' + msg);
|
||||
err.reason = msg;
|
||||
err.filename = options.source;
|
||||
err.line = lineno;
|
||||
err.column = column;
|
||||
err.source = css;
|
||||
|
||||
if (options.silent) {
|
||||
errorsList.push(err);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse stylesheet.
|
||||
*/
|
||||
|
||||
function stylesheet() {
|
||||
var rulesList = rules();
|
||||
|
||||
return {
|
||||
type: 'stylesheet',
|
||||
stylesheet: {
|
||||
source: options.source,
|
||||
rules: rulesList,
|
||||
parsingErrors: errorsList
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Opening brace.
|
||||
*/
|
||||
|
||||
function open() {
|
||||
return match(/^{\s*/);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closing brace.
|
||||
*/
|
||||
|
||||
function close() {
|
||||
return match(/^}/);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse ruleset.
|
||||
*/
|
||||
|
||||
function rules() {
|
||||
var node;
|
||||
var rules = [];
|
||||
whitespace();
|
||||
comments(rules);
|
||||
while (css.length && css.charAt(0) != '}' && (node = atrule() || rule())) {
|
||||
if (node !== false) {
|
||||
rules.push(node);
|
||||
comments(rules);
|
||||
}
|
||||
}
|
||||
return rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match `re` and return captures.
|
||||
*/
|
||||
|
||||
function match(re) {
|
||||
var m = re.exec(css);
|
||||
if (!m) return;
|
||||
var str = m[0];
|
||||
updatePosition(str);
|
||||
css = css.slice(str.length);
|
||||
return m;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse whitespace.
|
||||
*/
|
||||
|
||||
function whitespace() {
|
||||
match(/^\s*/);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse comments;
|
||||
*/
|
||||
|
||||
function comments(rules) {
|
||||
var c;
|
||||
rules = rules || [];
|
||||
while (c = comment()) {
|
||||
if (c !== false) {
|
||||
rules.push(c);
|
||||
}
|
||||
}
|
||||
return rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse comment.
|
||||
*/
|
||||
|
||||
function comment() {
|
||||
var pos = position();
|
||||
if ('/' != css.charAt(0) || '*' != css.charAt(1)) return;
|
||||
|
||||
var i = 2;
|
||||
while ("" != css.charAt(i) && ('*' != css.charAt(i) || '/' != css.charAt(i + 1))) ++i;
|
||||
i += 2;
|
||||
|
||||
if ("" === css.charAt(i-1)) {
|
||||
return error('End of comment missing');
|
||||
}
|
||||
|
||||
var str = css.slice(2, i - 2);
|
||||
column += 2;
|
||||
updatePosition(str);
|
||||
css = css.slice(i);
|
||||
column += 2;
|
||||
|
||||
return pos({
|
||||
type: 'comment',
|
||||
comment: str
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse selector.
|
||||
*/
|
||||
|
||||
function selector() {
|
||||
var m = match(/^([^{]+)/);
|
||||
if (!m) return;
|
||||
/* @fix Remove all comments from selectors
|
||||
* http://ostermiller.org/findcomment.html */
|
||||
return trim(m[0])
|
||||
.replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/+/g, '')
|
||||
.replace(/"(?:\\"|[^"])*"|'(?:\\'|[^'])*'/g, function(m) {
|
||||
return m.replace(/,/g, '\u200C');
|
||||
})
|
||||
.split(/\s*(?![^(]*\)),\s*/)
|
||||
.map(function(s) {
|
||||
return s.replace(/\u200C/g, ',');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse declaration.
|
||||
*/
|
||||
|
||||
function declaration() {
|
||||
var pos = position();
|
||||
|
||||
// prop
|
||||
var prop = match(/^(\*?[-#\/\*\\\w]+(\[[0-9a-z_-]+\])?)\s*/);
|
||||
if (!prop) return;
|
||||
prop = trim(prop[0]);
|
||||
|
||||
// :
|
||||
if (!match(/^:\s*/)) return error("property missing ':'");
|
||||
|
||||
// val
|
||||
var val = match(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+)/);
|
||||
|
||||
var ret = pos({
|
||||
type: 'declaration',
|
||||
property: prop.replace(commentre, ''),
|
||||
value: val ? trim(val[0]).replace(commentre, '') : ''
|
||||
});
|
||||
|
||||
// ;
|
||||
match(/^[;\s]*/);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse declarations.
|
||||
*/
|
||||
|
||||
function declarations() {
|
||||
var decls = [];
|
||||
|
||||
if (!open()) return error("missing '{'");
|
||||
comments(decls);
|
||||
|
||||
// declarations
|
||||
var decl;
|
||||
while (decl = declaration()) {
|
||||
if (decl !== false) {
|
||||
decls.push(decl);
|
||||
comments(decls);
|
||||
}
|
||||
}
|
||||
|
||||
if (!close()) return error("missing '}'");
|
||||
return decls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse keyframe.
|
||||
*/
|
||||
|
||||
function keyframe() {
|
||||
var m;
|
||||
var vals = [];
|
||||
var pos = position();
|
||||
|
||||
while (m = match(/^((\d+\.\d+|\.\d+|\d+)%?|[a-z]+)\s*/)) {
|
||||
vals.push(m[1]);
|
||||
match(/^,\s*/);
|
||||
}
|
||||
|
||||
if (!vals.length) return;
|
||||
|
||||
return pos({
|
||||
type: 'keyframe',
|
||||
values: vals,
|
||||
declarations: declarations()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse keyframes.
|
||||
*/
|
||||
|
||||
function atkeyframes() {
|
||||
var pos = position();
|
||||
var m = match(/^@([-\w]+)?keyframes\s*/);
|
||||
|
||||
if (!m) return;
|
||||
var vendor = m[1];
|
||||
|
||||
// identifier
|
||||
var m = match(/^([-\w]+)\s*/);
|
||||
if (!m) return error("@keyframes missing name");
|
||||
var name = m[1];
|
||||
|
||||
if (!open()) return error("@keyframes missing '{'");
|
||||
|
||||
var frame;
|
||||
var frames = comments();
|
||||
while (frame = keyframe()) {
|
||||
frames.push(frame);
|
||||
frames = frames.concat(comments());
|
||||
}
|
||||
|
||||
if (!close()) return error("@keyframes missing '}'");
|
||||
|
||||
return pos({
|
||||
type: 'keyframes',
|
||||
name: name,
|
||||
vendor: vendor,
|
||||
keyframes: frames
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse supports.
|
||||
*/
|
||||
|
||||
function atsupports() {
|
||||
var pos = position();
|
||||
var m = match(/^@supports *([^{]+)/);
|
||||
|
||||
if (!m) return;
|
||||
var supports = trim(m[1]);
|
||||
|
||||
if (!open()) return error("@supports missing '{'");
|
||||
|
||||
var style = comments().concat(rules());
|
||||
|
||||
if (!close()) return error("@supports missing '}'");
|
||||
|
||||
return pos({
|
||||
type: 'supports',
|
||||
supports: supports,
|
||||
rules: style
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse host.
|
||||
*/
|
||||
|
||||
function athost() {
|
||||
var pos = position();
|
||||
var m = match(/^@host\s*/);
|
||||
|
||||
if (!m) return;
|
||||
|
||||
if (!open()) return error("@host missing '{'");
|
||||
|
||||
var style = comments().concat(rules());
|
||||
|
||||
if (!close()) return error("@host missing '}'");
|
||||
|
||||
return pos({
|
||||
type: 'host',
|
||||
rules: style
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse media.
|
||||
*/
|
||||
|
||||
function atmedia() {
|
||||
var pos = position();
|
||||
var m = match(/^@media *([^{]+)/);
|
||||
|
||||
if (!m) return;
|
||||
var media = trim(m[1]);
|
||||
|
||||
if (!open()) return error("@media missing '{'");
|
||||
|
||||
var style = comments().concat(rules());
|
||||
|
||||
if (!close()) return error("@media missing '}'");
|
||||
|
||||
return pos({
|
||||
type: 'media',
|
||||
media: media,
|
||||
rules: style
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse custom-media.
|
||||
*/
|
||||
|
||||
function atcustommedia() {
|
||||
var pos = position();
|
||||
var m = match(/^@custom-media\s+(--[^\s]+)\s*([^{;]+);/);
|
||||
if (!m) return;
|
||||
|
||||
return pos({
|
||||
type: 'custom-media',
|
||||
name: trim(m[1]),
|
||||
media: trim(m[2])
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse paged media.
|
||||
*/
|
||||
|
||||
function atpage() {
|
||||
var pos = position();
|
||||
var m = match(/^@page */);
|
||||
if (!m) return;
|
||||
|
||||
var sel = selector() || [];
|
||||
|
||||
if (!open()) return error("@page missing '{'");
|
||||
var decls = comments();
|
||||
|
||||
// declarations
|
||||
var decl;
|
||||
while (decl = declaration()) {
|
||||
decls.push(decl);
|
||||
decls = decls.concat(comments());
|
||||
}
|
||||
|
||||
if (!close()) return error("@page missing '}'");
|
||||
|
||||
return pos({
|
||||
type: 'page',
|
||||
selectors: sel,
|
||||
declarations: decls
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse document.
|
||||
*/
|
||||
|
||||
function atdocument() {
|
||||
var pos = position();
|
||||
var m = match(/^@([-\w]+)?document *([^{]+)/);
|
||||
if (!m) return;
|
||||
|
||||
var vendor = trim(m[1]);
|
||||
var doc = trim(m[2]);
|
||||
|
||||
if (!open()) return error("@document missing '{'");
|
||||
|
||||
var style = comments().concat(rules());
|
||||
|
||||
if (!close()) return error("@document missing '}'");
|
||||
|
||||
return pos({
|
||||
type: 'document',
|
||||
document: doc,
|
||||
vendor: vendor,
|
||||
rules: style
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse font-face.
|
||||
*/
|
||||
|
||||
function atfontface() {
|
||||
var pos = position();
|
||||
var m = match(/^@font-face\s*/);
|
||||
if (!m) return;
|
||||
|
||||
if (!open()) return error("@font-face missing '{'");
|
||||
var decls = comments();
|
||||
|
||||
// declarations
|
||||
var decl;
|
||||
while (decl = declaration()) {
|
||||
decls.push(decl);
|
||||
decls = decls.concat(comments());
|
||||
}
|
||||
|
||||
if (!close()) return error("@font-face missing '}'");
|
||||
|
||||
return pos({
|
||||
type: 'font-face',
|
||||
declarations: decls
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse import
|
||||
*/
|
||||
|
||||
var atimport = _compileAtrule('import');
|
||||
|
||||
/**
|
||||
* Parse charset
|
||||
*/
|
||||
|
||||
var atcharset = _compileAtrule('charset');
|
||||
|
||||
/**
|
||||
* Parse namespace
|
||||
*/
|
||||
|
||||
var atnamespace = _compileAtrule('namespace');
|
||||
|
||||
/**
|
||||
* Parse non-block at-rules
|
||||
*/
|
||||
|
||||
|
||||
function _compileAtrule(name) {
|
||||
var re = new RegExp('^@' + name + '\\s*([^;]+);');
|
||||
return function() {
|
||||
var pos = position();
|
||||
var m = match(re);
|
||||
if (!m) return;
|
||||
var ret = { type: name };
|
||||
ret[name] = m[1].trim();
|
||||
return pos(ret);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse at rule.
|
||||
*/
|
||||
|
||||
function atrule() {
|
||||
if (css[0] != '@') return;
|
||||
|
||||
return atkeyframes()
|
||||
|| atmedia()
|
||||
|| atcustommedia()
|
||||
|| atsupports()
|
||||
|| atimport()
|
||||
|| atcharset()
|
||||
|| atnamespace()
|
||||
|| atdocument()
|
||||
|| atpage()
|
||||
|| athost()
|
||||
|| atfontface();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse rule.
|
||||
*/
|
||||
|
||||
function rule() {
|
||||
var pos = position();
|
||||
var sel = selector();
|
||||
|
||||
if (!sel) return error('selector missing');
|
||||
comments();
|
||||
|
||||
return pos({
|
||||
type: 'rule',
|
||||
selectors: sel,
|
||||
declarations: declarations()
|
||||
});
|
||||
}
|
||||
|
||||
return addParent(stylesheet());
|
||||
};
|
||||
|
||||
/**
|
||||
* Trim `str`.
|
||||
*/
|
||||
|
||||
function trim(str) {
|
||||
return str ? str.replace(/^\s+|\s+$/g, '') : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds non-enumerable parent node reference to each node.
|
||||
*/
|
||||
|
||||
function addParent(obj, parent) {
|
||||
var isNode = obj && typeof obj.type === 'string';
|
||||
var childParent = isNode ? obj : parent;
|
||||
|
||||
for (var k in obj) {
|
||||
var value = obj[k];
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(function(v) { addParent(v, childParent); });
|
||||
} else if (value && typeof value === 'object') {
|
||||
addParent(value, childParent);
|
||||
}
|
||||
}
|
||||
|
||||
if (isNode) {
|
||||
Object.defineProperty(obj, 'parent', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
value: parent || null
|
||||
});
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
50
server/libs/css/stringify/compiler.js
Normal file
50
server/libs/css/stringify/compiler.js
Normal file
@ -0,0 +1,50 @@
|
||||
|
||||
/**
|
||||
* Expose `Compiler`.
|
||||
*/
|
||||
|
||||
module.exports = Compiler;
|
||||
|
||||
/**
|
||||
* Initialize a compiler.
|
||||
*
|
||||
* @param {Type} name
|
||||
* @return {Type}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function Compiler(opts) {
|
||||
this.options = opts || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit `str`
|
||||
*/
|
||||
|
||||
Compiler.prototype.emit = function(str) {
|
||||
return str;
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit `node`.
|
||||
*/
|
||||
|
||||
Compiler.prototype.visit = function(node){
|
||||
return this[node.type](node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Map visit over array of `nodes`, optionally using a `delim`
|
||||
*/
|
||||
|
||||
Compiler.prototype.mapVisit = function(nodes, delim){
|
||||
var buf = '';
|
||||
delim = delim || '';
|
||||
|
||||
for (var i = 0, length = nodes.length; i < length; i++) {
|
||||
buf += this.visit(nodes[i]);
|
||||
if (delim && i < length - 1) buf += this.emit(delim);
|
||||
}
|
||||
|
||||
return buf;
|
||||
};
|
199
server/libs/css/stringify/compress.js
Normal file
199
server/libs/css/stringify/compress.js
Normal file
@ -0,0 +1,199 @@
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var Base = require('./compiler');
|
||||
var inherits = require('inherits');
|
||||
|
||||
/**
|
||||
* Expose compiler.
|
||||
*/
|
||||
|
||||
module.exports = Compiler;
|
||||
|
||||
/**
|
||||
* Initialize a new `Compiler`.
|
||||
*/
|
||||
|
||||
function Compiler(options) {
|
||||
Base.call(this, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inherit from `Base.prototype`.
|
||||
*/
|
||||
|
||||
inherits(Compiler, Base);
|
||||
|
||||
/**
|
||||
* Compile `node`.
|
||||
*/
|
||||
|
||||
Compiler.prototype.compile = function(node){
|
||||
return node.stylesheet
|
||||
.rules.map(this.visit, this)
|
||||
.join('');
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit comment node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.comment = function(node){
|
||||
return this.emit('', node.position);
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit import node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.import = function(node){
|
||||
return this.emit('@import ' + node.import + ';', node.position);
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit media node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.media = function(node){
|
||||
return this.emit('@media ' + node.media, node.position)
|
||||
+ this.emit('{')
|
||||
+ this.mapVisit(node.rules)
|
||||
+ this.emit('}');
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit document node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.document = function(node){
|
||||
var doc = '@' + (node.vendor || '') + 'document ' + node.document;
|
||||
|
||||
return this.emit(doc, node.position)
|
||||
+ this.emit('{')
|
||||
+ this.mapVisit(node.rules)
|
||||
+ this.emit('}');
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit charset node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.charset = function(node){
|
||||
return this.emit('@charset ' + node.charset + ';', node.position);
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit namespace node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.namespace = function(node){
|
||||
return this.emit('@namespace ' + node.namespace + ';', node.position);
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit supports node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.supports = function(node){
|
||||
return this.emit('@supports ' + node.supports, node.position)
|
||||
+ this.emit('{')
|
||||
+ this.mapVisit(node.rules)
|
||||
+ this.emit('}');
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit keyframes node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.keyframes = function(node){
|
||||
return this.emit('@'
|
||||
+ (node.vendor || '')
|
||||
+ 'keyframes '
|
||||
+ node.name, node.position)
|
||||
+ this.emit('{')
|
||||
+ this.mapVisit(node.keyframes)
|
||||
+ this.emit('}');
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit keyframe node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.keyframe = function(node){
|
||||
var decls = node.declarations;
|
||||
|
||||
return this.emit(node.values.join(','), node.position)
|
||||
+ this.emit('{')
|
||||
+ this.mapVisit(decls)
|
||||
+ this.emit('}');
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit page node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.page = function(node){
|
||||
var sel = node.selectors.length
|
||||
? node.selectors.join(', ')
|
||||
: '';
|
||||
|
||||
return this.emit('@page ' + sel, node.position)
|
||||
+ this.emit('{')
|
||||
+ this.mapVisit(node.declarations)
|
||||
+ this.emit('}');
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit font-face node.
|
||||
*/
|
||||
|
||||
Compiler.prototype['font-face'] = function(node){
|
||||
return this.emit('@font-face', node.position)
|
||||
+ this.emit('{')
|
||||
+ this.mapVisit(node.declarations)
|
||||
+ this.emit('}');
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit host node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.host = function(node){
|
||||
return this.emit('@host', node.position)
|
||||
+ this.emit('{')
|
||||
+ this.mapVisit(node.rules)
|
||||
+ this.emit('}');
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit custom-media node.
|
||||
*/
|
||||
|
||||
Compiler.prototype['custom-media'] = function(node){
|
||||
return this.emit('@custom-media ' + node.name + ' ' + node.media + ';', node.position);
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit rule node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.rule = function(node){
|
||||
var decls = node.declarations;
|
||||
if (!decls.length) return '';
|
||||
|
||||
return this.emit(node.selectors.join(','), node.position)
|
||||
+ this.emit('{')
|
||||
+ this.mapVisit(decls)
|
||||
+ this.emit('}');
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit declaration node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.declaration = function(node){
|
||||
return this.emit(node.property + ':' + node.value, node.position) + this.emit(';');
|
||||
};
|
||||
|
254
server/libs/css/stringify/identity.js
Normal file
254
server/libs/css/stringify/identity.js
Normal file
@ -0,0 +1,254 @@
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var Base = require('./compiler');
|
||||
var inherits = require('inherits');
|
||||
|
||||
/**
|
||||
* Expose compiler.
|
||||
*/
|
||||
|
||||
module.exports = Compiler;
|
||||
|
||||
/**
|
||||
* Initialize a new `Compiler`.
|
||||
*/
|
||||
|
||||
function Compiler(options) {
|
||||
options = options || {};
|
||||
Base.call(this, options);
|
||||
this.indentation = typeof options.indent === 'string' ? options.indent : ' ';
|
||||
}
|
||||
|
||||
/**
|
||||
* Inherit from `Base.prototype`.
|
||||
*/
|
||||
|
||||
inherits(Compiler, Base);
|
||||
|
||||
/**
|
||||
* Compile `node`.
|
||||
*/
|
||||
|
||||
Compiler.prototype.compile = function(node){
|
||||
return this.stylesheet(node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit stylesheet node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.stylesheet = function(node){
|
||||
return this.mapVisit(node.stylesheet.rules, '\n\n');
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit comment node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.comment = function(node){
|
||||
return this.emit(this.indent() + '/*' + node.comment + '*/', node.position);
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit import node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.import = function(node){
|
||||
return this.emit('@import ' + node.import + ';', node.position);
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit media node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.media = function(node){
|
||||
return this.emit('@media ' + node.media, node.position)
|
||||
+ this.emit(
|
||||
' {\n'
|
||||
+ this.indent(1))
|
||||
+ this.mapVisit(node.rules, '\n\n')
|
||||
+ this.emit(
|
||||
this.indent(-1)
|
||||
+ '\n}');
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit document node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.document = function(node){
|
||||
var doc = '@' + (node.vendor || '') + 'document ' + node.document;
|
||||
|
||||
return this.emit(doc, node.position)
|
||||
+ this.emit(
|
||||
' '
|
||||
+ ' {\n'
|
||||
+ this.indent(1))
|
||||
+ this.mapVisit(node.rules, '\n\n')
|
||||
+ this.emit(
|
||||
this.indent(-1)
|
||||
+ '\n}');
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit charset node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.charset = function(node){
|
||||
return this.emit('@charset ' + node.charset + ';', node.position);
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit namespace node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.namespace = function(node){
|
||||
return this.emit('@namespace ' + node.namespace + ';', node.position);
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit supports node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.supports = function(node){
|
||||
return this.emit('@supports ' + node.supports, node.position)
|
||||
+ this.emit(
|
||||
' {\n'
|
||||
+ this.indent(1))
|
||||
+ this.mapVisit(node.rules, '\n\n')
|
||||
+ this.emit(
|
||||
this.indent(-1)
|
||||
+ '\n}');
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit keyframes node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.keyframes = function(node){
|
||||
return this.emit('@' + (node.vendor || '') + 'keyframes ' + node.name, node.position)
|
||||
+ this.emit(
|
||||
' {\n'
|
||||
+ this.indent(1))
|
||||
+ this.mapVisit(node.keyframes, '\n')
|
||||
+ this.emit(
|
||||
this.indent(-1)
|
||||
+ '}');
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit keyframe node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.keyframe = function(node){
|
||||
var decls = node.declarations;
|
||||
|
||||
return this.emit(this.indent())
|
||||
+ this.emit(node.values.join(', '), node.position)
|
||||
+ this.emit(
|
||||
' {\n'
|
||||
+ this.indent(1))
|
||||
+ this.mapVisit(decls, '\n')
|
||||
+ this.emit(
|
||||
this.indent(-1)
|
||||
+ '\n'
|
||||
+ this.indent() + '}\n');
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit page node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.page = function(node){
|
||||
var sel = node.selectors.length
|
||||
? node.selectors.join(', ') + ' '
|
||||
: '';
|
||||
|
||||
return this.emit('@page ' + sel, node.position)
|
||||
+ this.emit('{\n')
|
||||
+ this.emit(this.indent(1))
|
||||
+ this.mapVisit(node.declarations, '\n')
|
||||
+ this.emit(this.indent(-1))
|
||||
+ this.emit('\n}');
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit font-face node.
|
||||
*/
|
||||
|
||||
Compiler.prototype['font-face'] = function(node){
|
||||
return this.emit('@font-face ', node.position)
|
||||
+ this.emit('{\n')
|
||||
+ this.emit(this.indent(1))
|
||||
+ this.mapVisit(node.declarations, '\n')
|
||||
+ this.emit(this.indent(-1))
|
||||
+ this.emit('\n}');
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit host node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.host = function(node){
|
||||
return this.emit('@host', node.position)
|
||||
+ this.emit(
|
||||
' {\n'
|
||||
+ this.indent(1))
|
||||
+ this.mapVisit(node.rules, '\n\n')
|
||||
+ this.emit(
|
||||
this.indent(-1)
|
||||
+ '\n}');
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit custom-media node.
|
||||
*/
|
||||
|
||||
Compiler.prototype['custom-media'] = function(node){
|
||||
return this.emit('@custom-media ' + node.name + ' ' + node.media + ';', node.position);
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit rule node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.rule = function(node){
|
||||
var indent = this.indent();
|
||||
var decls = node.declarations;
|
||||
if (!decls.length) return '';
|
||||
|
||||
return this.emit(node.selectors.map(function(s){ return indent + s }).join(',\n'), node.position)
|
||||
+ this.emit(' {\n')
|
||||
+ this.emit(this.indent(1))
|
||||
+ this.mapVisit(decls, '\n')
|
||||
+ this.emit(this.indent(-1))
|
||||
+ this.emit('\n' + this.indent() + '}');
|
||||
};
|
||||
|
||||
/**
|
||||
* Visit declaration node.
|
||||
*/
|
||||
|
||||
Compiler.prototype.declaration = function(node){
|
||||
return this.emit(this.indent())
|
||||
+ this.emit(node.property + ': ' + node.value, node.position)
|
||||
+ this.emit(';');
|
||||
};
|
||||
|
||||
/**
|
||||
* Increase, decrease or return current indentation.
|
||||
*/
|
||||
|
||||
Compiler.prototype.indent = function(level) {
|
||||
this.level = this.level || 1;
|
||||
|
||||
if (null != level) {
|
||||
this.level += level;
|
||||
return '';
|
||||
}
|
||||
|
||||
return Array(this.level).join(this.indentation);
|
||||
};
|
47
server/libs/css/stringify/index.js
Normal file
47
server/libs/css/stringify/index.js
Normal file
@ -0,0 +1,47 @@
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var Compressed = require('./compress');
|
||||
var Identity = require('./identity');
|
||||
|
||||
/**
|
||||
* Stringfy the given AST `node`.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* - `compress` space-optimized output
|
||||
* - `sourcemap` return an object with `.code` and `.map`
|
||||
*
|
||||
* @param {Object} node
|
||||
* @param {Object} [options]
|
||||
* @return {String}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
module.exports = function(node, options){
|
||||
options = options || {};
|
||||
|
||||
var compiler = options.compress
|
||||
? new Compressed(options)
|
||||
: new Identity(options);
|
||||
|
||||
// source maps
|
||||
if (options.sourcemap) {
|
||||
var sourcemaps = require('./source-map-support');
|
||||
sourcemaps(compiler);
|
||||
|
||||
var code = compiler.compile(node);
|
||||
compiler.applySourceMaps();
|
||||
|
||||
var map = options.sourcemap === 'generator'
|
||||
? compiler.map
|
||||
: compiler.map.toJSON();
|
||||
|
||||
return { code: code, map: map };
|
||||
}
|
||||
|
||||
var code = compiler.compile(node);
|
||||
return code;
|
||||
};
|
133
server/libs/css/stringify/source-map-support.js
Normal file
133
server/libs/css/stringify/source-map-support.js
Normal file
@ -0,0 +1,133 @@
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var SourceMap = require('source-map').SourceMapGenerator;
|
||||
var SourceMapConsumer = require('source-map').SourceMapConsumer;
|
||||
var sourceMapResolve = require('source-map-resolve');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
/**
|
||||
* Expose `mixin()`.
|
||||
*/
|
||||
|
||||
module.exports = mixin;
|
||||
|
||||
/**
|
||||
* Ensure Windows-style paths are formatted properly
|
||||
*/
|
||||
|
||||
const makeFriendlyPath = function(aPath) {
|
||||
return path.sep === "\\" ? aPath.replace(/\\/g, "/").replace(/^[a-z]:\/?/i, "/") : aPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mixin source map support into `compiler`.
|
||||
*
|
||||
* @param {Compiler} compiler
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function mixin(compiler) {
|
||||
compiler._comment = compiler.comment;
|
||||
compiler.map = new SourceMap();
|
||||
compiler.position = { line: 1, column: 1 };
|
||||
compiler.files = {};
|
||||
for (var k in exports) compiler[k] = exports[k];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update position.
|
||||
*
|
||||
* @param {String} str
|
||||
* @api private
|
||||
*/
|
||||
|
||||
exports.updatePosition = function(str) {
|
||||
var lines = str.match(/\n/g);
|
||||
if (lines) this.position.line += lines.length;
|
||||
var i = str.lastIndexOf('\n');
|
||||
this.position.column = ~i ? str.length - i : this.position.column + str.length;
|
||||
};
|
||||
|
||||
/**
|
||||
* Emit `str`.
|
||||
*
|
||||
* @param {String} str
|
||||
* @param {Object} [pos]
|
||||
* @return {String}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
exports.emit = function(str, pos) {
|
||||
if (pos) {
|
||||
var sourceFile = makeFriendlyPath(pos.source || 'source.css');
|
||||
|
||||
this.map.addMapping({
|
||||
source: sourceFile,
|
||||
generated: {
|
||||
line: this.position.line,
|
||||
column: Math.max(this.position.column - 1, 0)
|
||||
},
|
||||
original: {
|
||||
line: pos.start.line,
|
||||
column: pos.start.column - 1
|
||||
}
|
||||
});
|
||||
|
||||
this.addFile(sourceFile, pos);
|
||||
}
|
||||
|
||||
this.updatePosition(str);
|
||||
|
||||
return str;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a file to the source map output if it has not already been added
|
||||
* @param {String} file
|
||||
* @param {Object} pos
|
||||
*/
|
||||
|
||||
exports.addFile = function(file, pos) {
|
||||
if (typeof pos.content !== 'string') return;
|
||||
if (Object.prototype.hasOwnProperty.call(this.files, file)) return;
|
||||
|
||||
this.files[file] = pos.content;
|
||||
};
|
||||
|
||||
/**
|
||||
* Applies any original source maps to the output and embeds the source file
|
||||
* contents in the source map.
|
||||
*/
|
||||
|
||||
exports.applySourceMaps = function() {
|
||||
Object.keys(this.files).forEach(function(file) {
|
||||
var content = this.files[file];
|
||||
this.map.setSourceContent(file, content);
|
||||
|
||||
if (this.options.inputSourcemaps !== false) {
|
||||
var originalMap = sourceMapResolve.resolveSync(
|
||||
content, file, fs.readFileSync);
|
||||
if (originalMap) {
|
||||
var map = new SourceMapConsumer(originalMap.map);
|
||||
var relativeTo = originalMap.sourcesRelativeTo;
|
||||
this.map.applySourceMap(map, file, makeFriendlyPath(path.dirname(relativeTo)));
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Process comments, drops sourceMap comments.
|
||||
* @param {Object} node
|
||||
*/
|
||||
|
||||
exports.comment = function(node) {
|
||||
if (/^# sourceMappingURL=/.test(node.comment))
|
||||
return this.emit('', node.position);
|
||||
else
|
||||
return this._comment(node);
|
||||
};
|
80
server/managers/EBookManager.js
Normal file
80
server/managers/EBookManager.js
Normal file
@ -0,0 +1,80 @@
|
||||
const Logger = require('../Logger')
|
||||
const StreamZip = require('../libs/nodeStreamZip')
|
||||
|
||||
const parseEpub = require('../utils/parsers/parseEpub')
|
||||
|
||||
class EBookManager {
|
||||
constructor() {
|
||||
this.extractedEpubs = {}
|
||||
}
|
||||
|
||||
async extractBookData(libraryItem, user, isDev = false) {
|
||||
if (!libraryItem || !libraryItem.isBook || !libraryItem.media.ebookFile) return null
|
||||
|
||||
if (this.extractedEpubs[libraryItem.id]) return this.extractedEpubs[libraryItem.id]
|
||||
|
||||
const ebookFile = libraryItem.media.ebookFile
|
||||
if (!ebookFile.isEpub) {
|
||||
Logger.error(`[EBookManager] get book data is not supported for format ${ebookFile.ebookFormat}`)
|
||||
return null
|
||||
}
|
||||
|
||||
this.extractedEpubs[libraryItem.id] = await parseEpub.parse(ebookFile, libraryItem.id, user.token, isDev)
|
||||
|
||||
return this.extractedEpubs[libraryItem.id]
|
||||
}
|
||||
|
||||
async getBookInfo(libraryItem, user, isDev = false) {
|
||||
if (!libraryItem || !libraryItem.isBook || !libraryItem.media.ebookFile) return null
|
||||
|
||||
const bookData = await this.extractBookData(libraryItem, user, isDev)
|
||||
|
||||
return {
|
||||
title: libraryItem.media.metadata.title,
|
||||
pages: bookData.pages.length
|
||||
}
|
||||
}
|
||||
|
||||
async getBookPage(libraryItem, user, pageIndex, isDev = false) {
|
||||
if (!libraryItem || !libraryItem.isBook || !libraryItem.media.ebookFile) return null
|
||||
|
||||
const bookData = await this.extractBookData(libraryItem, user, isDev)
|
||||
|
||||
const pageObj = bookData.pages[pageIndex]
|
||||
|
||||
if (!pageObj) {
|
||||
return null
|
||||
}
|
||||
|
||||
const parsed = await parseEpub.parsePage(pageObj.path, bookData, libraryItem.id, user.token, isDev)
|
||||
|
||||
if (parsed.error) {
|
||||
Logger.error(`[EBookManager] Failed to parse epub page at "${pageObj.path}"`, parsed.error)
|
||||
return null
|
||||
}
|
||||
|
||||
return parsed.html
|
||||
}
|
||||
|
||||
async getBookResource(libraryItem, user, resourcePath, isDev = false, res) {
|
||||
if (!libraryItem || !libraryItem.isBook || !libraryItem.media.ebookFile) return res.sendStatus(500)
|
||||
const bookData = await this.extractBookData(libraryItem, user, isDev)
|
||||
const resourceItem = bookData.resources.find(r => r.path === resourcePath)
|
||||
|
||||
if (!resourceItem) {
|
||||
return res.status(404).send('Resource not found')
|
||||
}
|
||||
|
||||
const zip = new StreamZip.async({ file: bookData.filepath })
|
||||
const stm = await zip.stream(resourceItem.path)
|
||||
|
||||
res.set('content-type', resourceItem['media-type'])
|
||||
|
||||
stm.pipe(res)
|
||||
stm.on('end', () => {
|
||||
zip.close()
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
module.exports = EBookManager
|
@ -31,6 +31,10 @@ class EBookFile {
|
||||
}
|
||||
}
|
||||
|
||||
get isEpub() {
|
||||
return this.ebookFormat === 'epub'
|
||||
}
|
||||
|
||||
setData(libraryFile) {
|
||||
this.ino = libraryFile.ino
|
||||
this.metadata = libraryFile.metadata.clone()
|
||||
|
@ -24,6 +24,7 @@ const SearchController = require('../controllers/SearchController')
|
||||
const CacheController = require('../controllers/CacheController')
|
||||
const ToolsController = require('../controllers/ToolsController')
|
||||
const RSSFeedController = require('../controllers/RSSFeedController')
|
||||
const EBookController = require('../controllers/EBookController')
|
||||
const MiscController = require('../controllers/MiscController')
|
||||
|
||||
const BookFinder = require('../finders/BookFinder')
|
||||
@ -50,6 +51,7 @@ class ApiRouter {
|
||||
this.cronManager = Server.cronManager
|
||||
this.notificationManager = Server.notificationManager
|
||||
this.taskManager = Server.taskManager
|
||||
this.eBookManager = Server.eBookManager
|
||||
|
||||
this.bookFinder = new BookFinder()
|
||||
this.authorFinder = new AuthorFinder()
|
||||
@ -271,6 +273,13 @@ class ApiRouter {
|
||||
this.router.post('/feeds/series/:seriesId/open', RSSFeedController.middleware.bind(this), RSSFeedController.openRSSFeedForSeries.bind(this))
|
||||
this.router.post('/feeds/:id/close', RSSFeedController.middleware.bind(this), RSSFeedController.closeRSSFeed.bind(this))
|
||||
|
||||
//
|
||||
// EBook Routes
|
||||
//
|
||||
this.router.get('/ebooks/:id/info', EBookController.middleware.bind(this), EBookController.getEbookInfo.bind(this))
|
||||
this.router.get('/ebooks/:id/page/:page', EBookController.middleware.bind(this), EBookController.getEbookPage.bind(this))
|
||||
this.router.get('/ebooks/:id/resource', EBookController.middleware.bind(this), EBookController.getEbookResource.bind(this))
|
||||
|
||||
//
|
||||
// Misc Routes
|
||||
//
|
||||
|
226
server/utils/parsers/parseEpub.js
Normal file
226
server/utils/parsers/parseEpub.js
Normal file
@ -0,0 +1,226 @@
|
||||
|
||||
const Path = require('path')
|
||||
const h = require('htmlparser2')
|
||||
const ds = require('dom-serializer')
|
||||
|
||||
const Logger = require('../../Logger')
|
||||
const StreamZip = require('../../libs/nodeStreamZip')
|
||||
const css = require('../../libs/css')
|
||||
|
||||
const { xmlToJSON } = require('../index.js')
|
||||
|
||||
module.exports.parse = async (ebookFile, libraryItemId, token, isDev) => {
|
||||
const zip = new StreamZip.async({ file: ebookFile.metadata.path })
|
||||
const containerXml = await zip.entryData('META-INF/container.xml')
|
||||
const containerJson = await xmlToJSON(containerXml.toString('utf8'))
|
||||
|
||||
const packageOpfPath = containerJson.container.rootfiles[0].rootfile[0].$['full-path']
|
||||
const packageOpfDir = Path.dirname(packageOpfPath)
|
||||
|
||||
const packageDoc = await zip.entryData(packageOpfPath)
|
||||
const packageJson = await xmlToJSON(packageDoc.toString('utf8'))
|
||||
|
||||
const pages = []
|
||||
|
||||
let manifestItems = packageJson.package.manifest[0].item.map(item => item.$)
|
||||
const spineItems = packageJson.package.spine[0].itemref.map(ref => ref.$.idref)
|
||||
for (const spineItem of spineItems) {
|
||||
const mi = manifestItems.find(i => i.id === spineItem)
|
||||
if (mi) {
|
||||
manifestItems = manifestItems.filter(_mi => _mi.id !== mi.id) // Remove from manifest items
|
||||
|
||||
mi.path = Path.posix.join(packageOpfDir, mi.href)
|
||||
pages.push(mi)
|
||||
} else {
|
||||
Logger.error('[parseEpub] Invalid spine item', spineItem)
|
||||
}
|
||||
}
|
||||
|
||||
const stylesheets = []
|
||||
const resources = []
|
||||
|
||||
for (const manifestItem of manifestItems) {
|
||||
manifestItem.path = Path.posix.join(packageOpfDir, manifestItem.href)
|
||||
|
||||
if (manifestItem['media-type'] === 'text/css') {
|
||||
const stylesheetData = await zip.entryData(manifestItem.path)
|
||||
const modifiedCss = this.parseStylesheet(stylesheetData.toString('utf8'), manifestItem.path, libraryItemId, token, isDev)
|
||||
if (modifiedCss) {
|
||||
manifestItem.style = modifiedCss
|
||||
stylesheets.push(manifestItem)
|
||||
} else {
|
||||
Logger.error(`[parseEpub] Invalid stylesheet "${manifestItem.path}"`)
|
||||
}
|
||||
} else {
|
||||
resources.push(manifestItem)
|
||||
}
|
||||
}
|
||||
|
||||
await zip.close()
|
||||
|
||||
return {
|
||||
filepath: ebookFile.metadata.path,
|
||||
epubVersion: packageJson.package.$.version,
|
||||
packageDir: packageOpfDir,
|
||||
resources,
|
||||
stylesheets,
|
||||
pages
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.parsePage = async (pagePath, bookData, libraryItemId, token, isDev) => {
|
||||
const pageDir = Path.dirname(pagePath)
|
||||
|
||||
const zip = new StreamZip.async({ file: bookData.filepath })
|
||||
const pageData = await zip.entryData(pagePath)
|
||||
await zip.close()
|
||||
const rawHtml = pageData.toString('utf8')
|
||||
|
||||
const results = {}
|
||||
|
||||
const dh = new h.DomHandler((err, dom) => {
|
||||
if (err) return results.error = err
|
||||
|
||||
// Get stylesheets
|
||||
const isStylesheetLink = (elem) => elem.type == 'tag' && elem.name.toLowerCase() === 'link' && elem.attribs.rel === 'stylesheet' && elem.attribs.type === 'text/css'
|
||||
const stylesheets = h.DomUtils.findAll(isStylesheetLink, dom)
|
||||
|
||||
// Get body tag
|
||||
const isBodyTag = (elem) => elem.type == 'tag' && elem.name.toLowerCase() == 'body'
|
||||
const body = h.DomUtils.findOne(isBodyTag, dom)
|
||||
|
||||
// Get all svg elements
|
||||
const isSvgTag = (name) => ['svg'].includes((name || '').toLowerCase())
|
||||
const svgElements = h.DomUtils.getElementsByTagName(isSvgTag, body.children)
|
||||
svgElements.forEach((el) => {
|
||||
if (el.attribs.class) el.attribs.class += ' abs-svg-scale'
|
||||
else el.attribs.class = 'abs-svg-scale'
|
||||
})
|
||||
|
||||
// Get all img elements
|
||||
const isImageTag = (name) => ['img', 'image'].includes((name || '').toLowerCase())
|
||||
const imgElements = h.DomUtils.getElementsByTagName(isImageTag, body.children)
|
||||
|
||||
imgElements.forEach(el => {
|
||||
if (!el.attribs.src && !el.attribs['xlink:href']) {
|
||||
Logger.warn('[parseEpub] parsePage: Invalid img element attribs', el.attribs)
|
||||
return
|
||||
}
|
||||
|
||||
if (el.attribs.class) el.attribs.class += ' abs-image-scale'
|
||||
else el.attribs.class = 'abs-image-scale'
|
||||
|
||||
const srcKey = el.attribs.src ? 'src' : 'xlink:href'
|
||||
const src = encodeURIComponent(Path.posix.join(pageDir, el.attribs[srcKey]))
|
||||
|
||||
const basePath = isDev ? 'http://localhost:3333' : ''
|
||||
el.attribs[srcKey] = `${basePath}/api/ebooks/${libraryItemId}/resource?path=${src}&token=${token}`
|
||||
})
|
||||
|
||||
let finalHtml = '<div class="abs-page-content" style="max-height: unset; margin-left: 15% !important; margin-right: 15% !important;">'
|
||||
|
||||
stylesheets.forEach((el) => {
|
||||
const href = Path.posix.join(pageDir, el.attribs.href)
|
||||
const ssObj = bookData.stylesheets.find(sso => sso.path === href)
|
||||
|
||||
// find @import css and add it
|
||||
const importSheets = getStylesheetImports(ssObj.style, bookData.stylesheets)
|
||||
if (importSheets) {
|
||||
importSheets.forEach((sheet) => {
|
||||
finalHtml += `<style>${sheet.style}</style>\n`
|
||||
})
|
||||
}
|
||||
|
||||
if (!ssObj) {
|
||||
Logger.warn('[parseEpub] parsePage: Stylesheet object not found for href', href)
|
||||
} else {
|
||||
finalHtml += `<style>${ssObj.style}</style>\n`
|
||||
}
|
||||
})
|
||||
|
||||
finalHtml += `<style>
|
||||
.abs-image-scale { max-width: 100%; object-fit: contain; object-position: top center; max-height: 100vh; }
|
||||
.abs-svg-scale { width: auto; max-height: 80vh; }
|
||||
</style>\n`
|
||||
|
||||
finalHtml += ds.render(body.children)
|
||||
|
||||
finalHtml += '\n</div>'
|
||||
|
||||
results.html = finalHtml
|
||||
})
|
||||
|
||||
const parser = new h.Parser(dh)
|
||||
parser.write(rawHtml)
|
||||
parser.end()
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
module.exports.parseStylesheet = (rawCss, stylesheetPath, libraryItemId, token, isDev) => {
|
||||
try {
|
||||
const stylesheetDir = Path.dirname(stylesheetPath)
|
||||
|
||||
const res = css.parse(rawCss)
|
||||
|
||||
res.stylesheet.rules.forEach((rule) => {
|
||||
if (rule.type === 'rule') {
|
||||
rule.selectors = rule.selectors.map(s => s === 'body' ? '.abs-page-content' : `.abs-page-content ${s}`)
|
||||
} else if (rule.type === 'font-face' && rule.declarations) {
|
||||
rule.declarations = rule.declarations.map(dec => {
|
||||
if (dec.property === 'src') {
|
||||
const match = dec.value.trim().split(' ').shift().match(/url\((.+)\)/)
|
||||
if (match && match[1]) {
|
||||
const fontPath = Path.posix.join(stylesheetDir, match[1])
|
||||
const newSrc = encodeURIComponent(fontPath)
|
||||
|
||||
const basePath = isDev ? 'http://localhost:3333' : ''
|
||||
dec.value = dec.value.replace(match[1], `"${basePath}/api/ebooks/${libraryItemId}/resource?path=${newSrc}&token=${token}"`)
|
||||
}
|
||||
}
|
||||
return dec
|
||||
})
|
||||
} else if (rule.type === 'import') {
|
||||
const importUrl = rule.import
|
||||
const match = importUrl.match(/\"(.*)\"/)
|
||||
const path = match ? match[1] || '' : ''
|
||||
if (path) {
|
||||
// const newSrc = encodeURIComponent(Path.posix.join(stylesheetDir, path))
|
||||
// const basePath = isDev ? 'http://localhost:3333' : ''
|
||||
// const newPath = `"${basePath}/api/ebooks/${libraryItemId}/resource?path=${newSrc}&token=${token}"`
|
||||
// rule.import = rule.import.replace(path, newPath)
|
||||
|
||||
rule.import = Path.posix.join(stylesheetDir, path)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return css.stringify(res)
|
||||
} catch (error) {
|
||||
Logger.error('[parseEpub] parseStylesheet: Failed', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function getStylesheetImports(rawCss, stylesheets) {
|
||||
try {
|
||||
const res = css.parse(rawCss)
|
||||
|
||||
const imports = []
|
||||
res.stylesheet.rules.forEach((rule) => {
|
||||
if (rule.type === 'import') {
|
||||
const importUrl = rule.import.replaceAll('"', '')
|
||||
const sheet = stylesheets.find(s => s.path === importUrl)
|
||||
if (sheet) imports.push(sheet)
|
||||
else {
|
||||
Logger.error('[parseEpub] getStylesheetImports: Sheet not found', stylesheets)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return imports
|
||||
} catch (error) {
|
||||
Logger.error('[parseEpub] getStylesheetImports: Failed', error)
|
||||
return null
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user