diff options
Diffstat (limited to 'lib/cli')
-rw-r--r-- | lib/cli/helper.js | 139 | ||||
-rw-r--r-- | lib/cli/index.js | 187 | ||||
-rw-r--r-- | lib/cli/server.js | 95 | ||||
-rw-r--r-- | lib/cli/watch.js | 42 |
4 files changed, 463 insertions, 0 deletions
diff --git a/lib/cli/helper.js b/lib/cli/helper.js new file mode 100644 index 0000000..e4dc8da --- /dev/null +++ b/lib/cli/helper.js @@ -0,0 +1,139 @@ +var _ = require('lodash'); +var path = require('path'); + +var Book = require('../book'); +var NodeFS = require('../fs/node'); +var Logger = require('../utils/logger'); +var Promise = require('../utils/promise'); +var fs = require('../utils/fs'); +var JSONOutput = require('../output/json'); +var WebsiteOutput = require('../output/website'); +var EBookOutput = require('../output/ebook'); + +var nodeFS = new NodeFS(); + +var LOG_OPTION = { + name: 'log', + description: 'Minimum log level to display', + values: _.chain(Logger.LEVELS) + .keys() + .map(function(s) { + return s.toLowerCase(); + }) + .value(), + defaults: 'info' +}; + +var FORMAT_OPTION = { + name: 'format', + description: 'Format to build to', + values: ['website', 'json', 'ebook'], + defaults: 'website' +}; + +var FORMATS = { + json: JSONOutput, + website: WebsiteOutput, + ebook: EBookOutput +}; + +// Commands which is processing a book +// the root of the book is the first argument (or current directory) +function bookCmd(fn) { + return function(args, kwargs) { + var input = path.resolve(args[0] || process.cwd()); + return Book.setup(nodeFS, input, { + logLevel: kwargs.log + }) + .then(function(book) { + return fn(book, args.slice(1), kwargs); + }); + }; +} + +// Commands which is working on a Output instance +function outputCmd(fn) { + return bookCmd(function(book, args, kwargs) { + var Out = FORMATS[kwargs.format]; + var outputFolder = undefined; + + // Set output folder + if (args[0]) { + outputFolder = path.resolve(process.cwd(), args[0]); + } + + return fn(new Out(book, { + root: outputFolder + }), args); + }); +} + +// Command to generate an ebook +function ebookCmd(format) { + return { + name: format + ' [book] [output] [file]', + description: 'generates ebook '+format, + options: [ + LOG_OPTION + ], + exec: bookCmd(function(book, args, kwargs) { + return fs.tmpDir() + .then(function(dir) { + var ext = '.'+format; + var outputFile = path.resolve(process.cwd(), args[1] || ('book' + ext)); + var output = new EBookOutput(book, { + root: dir, + format: format + }); + + return output.book.parse() + .then(function() { + return output.generate(); + }) + + // Copy the ebook files + .then(function() { + if (output.book.isMultilingual()) { + return Promise.serie(output.book.langs.list(), function(lang) { + var _outputFile = path.join( + path.dirname(outputFile), + path.basename(outputFile, ext) + '_' + lang.id + ext + ); + + return fs.copy( + path.resolve(dir, lang.id, 'index' + ext), + _outputFile + ); + }) + .thenResolve(output.book.langs.count()); + } else { + return fs.copy( + path.resolve(dir, 'index' + ext), + outputFile + ).thenResolve(1); + } + }) + .then(function(n) { + output.book.log.info.ok(n+' file(s) generated'); + + output.book.log.info('cleaning up... '); + return output.book.log.info.promise(fs.rmDir(dir)); + }); + }); + }) + }; +} + +module.exports = { + nodeFS: nodeFS, + bookCmd: bookCmd, + outputCmd: outputCmd, + ebookCmd: ebookCmd, + + options: { + log: LOG_OPTION, + format: FORMAT_OPTION + }, + + FORMATS: FORMATS +}; diff --git a/lib/cli/index.js b/lib/cli/index.js new file mode 100644 index 0000000..4d3d364 --- /dev/null +++ b/lib/cli/index.js @@ -0,0 +1,187 @@ +/* eslint-disable no-console */ + +var _ = require('lodash'); +var path = require('path'); +var tinylr = require('tiny-lr'); + +var Promise = require('../utils/promise'); +var PluginsManager = require('../plugins'); +var Book = require('../book'); + +var helper = require('./helper'); +var Server = require('./server'); +var watch = require('./watch'); + +module.exports = { + commands: [ + + { + name: 'parse [book]', + description: 'parse and returns debug information for a book', + options: [ + helper.options.log + ], + exec: helper.bookCmd(function(book) { + return book.parse() + .then(function() { + book.log.info.ln('Book located in:', book.root); + book.log.info.ln(''); + + if (book.config.exists()) book.log.info.ln('Configuration:', book.config.path); + + if (book.isMultilingual()) { + book.log.info.ln('Multilingual book detected:', book.langs.path); + } else { + book.log.info.ln('Readme:', book.readme.path); + book.log.info.ln('Summary:', book.summary.path); + if (book.glossary.exists()) book.log.info.ln('Glossary:', book.glossary.path); + + book.log.info.ln('Pages:'); + _.each(book.pages, function(page) { + book.log.info.ln('\t-', page.path); + }); + } + }); + }) + }, + + { + name: 'install [book]', + description: 'install all plugins dependencies', + options: [ + helper.options.log + ], + exec: helper.bookCmd(function(book, args) { + var plugins = new PluginsManager(book); + + return book.config.load() + .then(function() { + return plugins.install(); + }); + }) + }, + + { + name: 'build [book] [output]', + description: 'build a book', + options: [ + helper.options.log, + helper.options.format + ], + exec: helper.outputCmd(function(output, args, kwargs) { + return output.book.parse() + .then(function() { + return output.generate(); + }); + }) + }, + + helper.ebookCmd('pdf'), + helper.ebookCmd('epub'), + helper.ebookCmd('mobi'), + + { + name: 'serve [book]', + description: 'Build then serve a book from a directory', + options: [ + { + name: 'port', + description: 'Port for server to listen on', + defaults: 4000 + }, + { + name: 'lrport', + description: 'Port for livereload server to listen on', + defaults: 35729 + }, + { + name: 'watch', + description: 'Enable/disable file watcher', + defaults: true + }, + helper.options.format, + helper.options.log + ], + exec: function(args, kwargs) { + var input = path.resolve(args[0] || process.cwd()); + var server = new Server(); + + // Init livereload server + var lrServer = tinylr({}); + var port = kwargs.port; + var lrPath; + + var generate = function() { + + // Stop server if running + if (server.isRunning()) console.log('Stopping server'); + return server.stop() + + // Generate the book + .then(function() { + return Book.setup(helper.nodeFS, input, { + 'logLevel': kwargs.log + }) + .then(function(book) { + return book.parse() + .then(function() { + // Add livereload plugin + book.config.set('plugins', + book.config.get('plugins') + .concat([ + { name: 'livereload' } + ]) + ); + + var Out = helper.FORMATS[kwargs.format]; + var output = new Out(book); + + return output.generate() + .thenResolve(output); + }); + }); + }) + + // Start server and watch changes + .then(function(output) { + console.log(); + console.log('Starting server ...'); + return server.start(output.root(), port) + .then(function() { + console.log('Serving book on http://localhost:'+port); + + if (lrPath) { + // trigger livereload + lrServer.changed({ + body: { + files: [lrPath] + } + }); + } + + if (!kwargs.watch) return; + + return watch(output.book.root) + .then(function(filepath) { + // set livereload path + lrPath = filepath; + console.log('Restart after change in file', filepath); + console.log(''); + return generate(); + }); + }); + }); + }; + + return Promise.nfcall(lrServer.listen.bind(lrServer), kwargs.lrport) + .then(function() { + console.log('Live reload server started on port:', kwargs.lrport); + console.log('Press CTRL+C to quit ...'); + console.log(''); + return generate(); + }); + } + } + + ] +}; diff --git a/lib/cli/server.js b/lib/cli/server.js new file mode 100644 index 0000000..3bd5d18 --- /dev/null +++ b/lib/cli/server.js @@ -0,0 +1,95 @@ +var events = require('events'); +var http = require('http'); +var send = require('send'); +var util = require('util'); +var url = require('url'); + +var Promise = require('../utils/promise'); + +var Server = function() { + this.running = null; + this.dir = null; + this.port = 0; + this.sockets = []; +}; +util.inherits(Server, events.EventEmitter); + +// Return true if the server is running +Server.prototype.isRunning = function() { + return !!this.running; +}; + +// Stop the server +Server.prototype.stop = function() { + var that = this; + if (!this.isRunning()) return Promise(); + + var d = Promise.defer(); + this.running.close(function(err) { + that.running = null; + that.emit('state', false); + + if (err) d.reject(err); + else d.resolve(); + }); + + for (var i = 0; i < this.sockets.length; i++) { + this.sockets[i].destroy(); + } + + return d.promise; +}; + +Server.prototype.start = function(dir, port) { + var that = this, pre = Promise(); + port = port || 8004; + + if (that.isRunning()) pre = this.stop(); + return pre + .then(function() { + var d = Promise.defer(); + + that.running = http.createServer(function(req, res){ + // Render error + function error(err) { + res.statusCode = err.status || 500; + res.end(err.message); + } + + // Redirect to directory's index.html + function redirect() { + res.statusCode = 301; + res.setHeader('Location', req.url + '/'); + res.end('Redirecting to ' + req.url + '/'); + } + + // Send file + send(req, url.parse(req.url).pathname) + .root(dir) + .on('error', error) + .on('directory', redirect) + .pipe(res); + }); + + that.running.on('connection', function (socket) { + that.sockets.push(socket); + socket.setTimeout(4000); + socket.on('close', function () { + that.sockets.splice(that.sockets.indexOf(socket), 1); + }); + }); + + that.running.listen(port, function(err) { + if (err) return d.reject(err); + + that.port = port; + that.dir = dir; + that.emit('state', true); + d.resolve(); + }); + + return d.promise; + }); +}; + +module.exports = Server; diff --git a/lib/cli/watch.js b/lib/cli/watch.js new file mode 100644 index 0000000..b98faeb --- /dev/null +++ b/lib/cli/watch.js @@ -0,0 +1,42 @@ +var _ = require('lodash'); +var path = require('path'); +var chokidar = require('chokidar'); + +var Promise = require('../utils/promise'); +var parsers = require('../parsers'); + +// Watch a folder and resolve promise once a file is modified +function watch(dir) { + var d = Promise.defer(); + dir = path.resolve(dir); + + var toWatch = [ + 'book.json', 'book.js' + ]; + + // Watch all parsable files + _.each(parsers.extensions, function(ext) { + toWatch.push('**/*'+ext); + }); + + var watcher = chokidar.watch(toWatch, { + cwd: dir, + ignored: '_book/**', + ignoreInitial: true + }); + + watcher.once('all', function(e, filepath) { + watcher.close(); + + d.resolve(filepath); + }); + watcher.once('error', function(err) { + watcher.close(); + + d.reject(err); + }); + + return d.promise; +} + +module.exports = watch; |