diff options
-rw-r--r-- | lib/output/base.js | 18 | ||||
-rw-r--r-- | lib/output/ebook.js | 145 | ||||
-rw-r--r-- | lib/utils/command.js | 22 | ||||
-rw-r--r-- | package.json | 2 |
4 files changed, 181 insertions, 6 deletions
diff --git a/lib/output/base.js b/lib/output/base.js index 4190559..828a11e 100644 --- a/lib/output/base.js +++ b/lib/output/base.js @@ -7,7 +7,6 @@ var pathUtil = require('../utils/path'); var location = require('../utils/location'); var PluginsManager = require('../plugins'); var TemplateEngine = require('../template'); -var gitbook = require('../gitbook'); /* Output is like a stream interface for a parsed book @@ -16,9 +15,13 @@ to output "something". The process is mostly on the behavior of "onPage" and "onAsset" */ -function Output(book) { +function Output(book, opts) { _.bindAll(this); + this.opts = _.defaults(opts || {}, { + + }); + this.book = book; this.log = this.book.log; @@ -175,6 +178,17 @@ Output.prototype.onResolveTemplate = function(from, to) { // ---- Utilities ---- +// Return a default context for templates +Output.prototype.getContext = function() { + return _.extend( + {}, + this.book.getContext(), + this.book.summary.getContext(), + this.book.glossary.getContext(), + this.book.config.getContext() + ); +}; + // Resolve a file path in the context of a specific page // Result is an "absolute path relative to the output folder" Output.prototype.resolveForPage = function(page, href) { diff --git a/lib/output/ebook.js b/lib/output/ebook.js index 7e7a66d..a0e72a9 100644 --- a/lib/output/ebook.js +++ b/lib/output/ebook.js @@ -1,14 +1,155 @@ +var _ = require('lodash'); var util = require('util'); +var juice = require('juice'); +var command = require('../utils/command'); +var fs = require('../utils/fs'); var WebsiteOutput = require('./website'); var assetsInliner = require('./assets-inliner'); -function EbookOutput() { +function _EbookOutput() { WebsiteOutput.apply(this, arguments); } util.inherits(EbookOutput, WebsiteOutput); +var EbookOutput = assetsInliner(_EbookOutput); + EbookOutput.prototype.name = 'ebook'; +// Finish generation, create ebook using ebook-convert +EbookOutput.prototype.finish = function() { + var that = this; + + return Promise() + .then(function() { + return EbookOutput.super_.prototype.finish.apply(that); + }) + + // Generate SUMMARY.html + .then(function() { + return that.render('summary', that.getContext()) + .then(function(html) { + return that.writeFile( + that.resolve('SUMMARY.html'), + html + ); + }); + }) + + // Start ebook-convert + .then(function() { + return that.ebookConvertOption(); + }) + + .then(function(options) { + var cmd = [ + 'ebook-convert', + that.resolve('SUMMARY.html'), + that.resolve('index.'+that.opts.format), + command.optionsToShellArgs(options) + ].join(' '); + + return command.exec(cmd) + .fail(function(err) { + if (err.code == 127) { + err = new Error('Need to install ebook-convert from Calibre'); + } + + throw err; + }); + }); +}; + +// Generate header/footer for PDF +EbookOutput.prototype.getPDFTemplate = function(tpl) { + var that = this; + var context = _.extend( + { + // Nunjucks context mapping to ebook-convert templating + page: { + num: '_PAGENUM_', + title: '_TITLE_' + } + }, + this.getContext() + ); + + return this.render('pdf_'+tpl, context) + + // Inline css, include css relative to the output folder + .then(function(output) { + return Promise.nfcall(juice.juiceResources, tpl, { + webResources: { + relativeTo: that.root() + } + }); + }); +}; + +// Generate options for ebook-convert +EbookOutput.prototype.ebookConvertOption = function() { + var that = this; + var cover = this.book.config.get('cover'); + + if (!cover && fs.existsSync(this.resolve('cover.jpg'))) { + cover = this.resolve('cover.jpg'); + } + + var options = { + '--cover': cover, + '--title': this.book.config.get('title'), + '--comments': this.book.config.get('description'), + '--isbn': this.book.config.get('isbn'), + '--authors': this.book.config.get('author'), + '--language': this.book.config.get('language'), + '--book-producer': 'GitBook', + '--publisher': 'GitBook', + '--chapter': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter \')]', + '--level1-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-1 \')]', + '--level2-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-2 \')]', + '--level3-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-3 \')]', + '--no-chapters-in-toc': true, + '--max-levels': '1', + '--breadth-first': true + }; + + if (this.opts.format == 'epub') { + options = _.extend(options, { + '--dont-split-on-page-breaks': true + }); + } + + if (this.opts.format != 'pdf') return Promise(options); + + var pdfOptions = this.book.config.get('pdf'); + + options = _.extend(options, { + '--chapter-mark': String(pdfOptions.chapterMark), + '--page-breaks-before': String(pdfOptions.pageBreaksBefore), + '--margin-left': String(pdfOptions.margin.left), + '--margin-right': String(pdfOptions.margin.right), + '--margin-top': String(pdfOptions.margin.top), + '--margin-bottom': String(pdfOptions.margin.bottom), + '--pdf-default-font-size': String(pdfOptions.fontSize), + '--pdf-mono-font-size': String(pdfOptions.fontSize), + '--paper-size': String(pdfOptions.paperSize), + '--pdf-page-numbers': Boolean(pdfOptions.pageNumbers), + '--pdf-header-template': that.getPDFTemplate('header'), + '--pdf-footer-template': that.getPDFTemplate('footer'), + '--pdf-sans-family': String(pdfOptions.fontFamily) + }); + + return this.getPDFTemplate('header') + .then(function(tpl) { + options['--pdf-header-template'] = tpl; + + return that.getPDFTemplate('footer'); + }) + .then(function(tpl) { + options['--pdf-footer-template'] = tpl; + + return options; + }); +}; -module.exports = assetsInliner(EbookOutput); +module.exports = EbookOutput; diff --git a/lib/utils/command.js b/lib/utils/command.js index c3e33c7..49b0998 100644 --- a/lib/utils/command.js +++ b/lib/utils/command.js @@ -1,3 +1,4 @@ +var _ = require('lodash'); var childProcess = require('child_process'); var Promise = require('./promise'); @@ -37,8 +38,27 @@ function spawn(command, args, options) { return d.promise; } +// Transform an option object to a command line string +function escapeShellArg(s) { + s = s.replace(/"/g, '\\"'); + return '"' + s + '"'; +} + +function optionsToShellArgs(options) { + return _.chain(options) + .map(function(value, key) { + if (value === null || value === undefined || value === false) return null; + if (value === true) return key; + return key + '=' + escapeShellArg(value); + }) + .compact() + .value() + .join(' '); +} + module.exports = { isAvailable: isAvailable, exec: exec, - spawn: spawn + spawn: spawn, + optionsToShellArgs: optionsToShellArgs }; diff --git a/package.json b/package.json index 474effa..2710dde 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "dom-serializer": "0.1.0", "spawn-cmd": "0.0.2", "escape-string-regexp": "1.0.3", - "juice": "1.5.0", + "juice": "1.9.0", "jsonschema": "1.0.2", "json-schema-defaults": "0.1.1", "merge-defaults": "0.2.1", |