summaryrefslogtreecommitdiffstats
path: root/lib/cli
diff options
context:
space:
mode:
Diffstat (limited to 'lib/cli')
-rw-r--r--lib/cli/helper.js139
-rw-r--r--lib/cli/index.js187
-rw-r--r--lib/cli/server.js95
-rw-r--r--lib/cli/watch.js42
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;