diff options
-rw-r--r-- | CHANGES.md | 12 | ||||
-rw-r--r-- | README.md | 15 | ||||
-rw-r--r-- | bin/build.js | 7 | ||||
-rwxr-xr-x | bin/gitbook.js | 66 | ||||
-rw-r--r-- | bin/utils.js | 30 | ||||
-rw-r--r-- | lib/generate/config.js | 32 | ||||
-rw-r--r-- | lib/generate/ebook/index.js | 1 | ||||
-rw-r--r-- | lib/generate/fs.js | 15 | ||||
-rw-r--r-- | lib/generate/index.js | 36 | ||||
-rw-r--r-- | lib/generate/page/index.js | 10 | ||||
-rw-r--r-- | lib/generate/plugin.js | 73 | ||||
-rw-r--r-- | package.json | 3 | ||||
-rw-r--r-- | test/plugin.js | 11 | ||||
-rw-r--r-- | theme/templates/ebook/glossary.html | 15 | ||||
-rw-r--r-- | theme/templates/ebook/summary.html | 4 | ||||
-rwxr-xr-x | theme/templates/layout.html | 4 |
16 files changed, 259 insertions, 75 deletions
@@ -1,5 +1,17 @@ # Release notes +## 1.4.1 +- Fix command 'install' without arguments + +## 1.4.0 +- Add command `gitbook install` to install plugins from book.json +- `package.json` is no longer necessary + +## 1.3.4 +- Add glossary to ebooks +- Fix autocover with new hook "finish:before" +- Add X-UA-Compatible meta tag for IE + ## 1.3.3 - Fix parsing of lexed content using the client library @@ -16,6 +16,11 @@ GitBook can be installed from **NPM** using: $ npm install gitbook -g ``` +Create the directories and files for a book from its [SUMMARY.md](https://github.com/GitbookIO/gitbook#book-format) file using +``` +$ gitbook init +``` + You can serve a repository as a book using: ``` @@ -52,9 +57,10 @@ Here are the options that can be stored in this file: // It's not advised this option in the book.json "generator": "site", - // Book title and description (defaults are extracted from the README) + // Book metadats (somes are extracted from the README by default) "title": null, "description": null, + "isbn": null, // For ebook format, the extension to use for generation (default is detected from output extension) // "epub", "pdf", "mobi" @@ -132,7 +138,10 @@ You can publish your books to our index by visiting [GitBook.io](http://www.gitb GitBook can generate your book in the following formats: * **Static Website**: This is the default format. It generates a complete interactive static website that can be, for example, hosted on GitHub Pages. -* **eBook**: A complete eBook with exercise solutions at the end of the book. Generate this format using: ```gitbook ebook ./myrepo```. You need to have [ebook-convert](http://manual.calibre-ebook.com/cli/ebook-convert.html) installed. The output format could be **PDF**, **ePub** or **MOBI**. +* **eBook**: A complete eBook with exercise solutions at the end of the book. You need to have [ebook-convert](http://manual.calibre-ebook.com/cli/ebook-convert.html) installed. You can specify the eBook filename with the `-o` option, otherwise `book` will be used. + * Generate a **PDF** using: `gitbook pdf ./myrepo` + * Generate a **ePub** using: `gitbook epub ./myrepo` + * Generate a **MOBI** using: `gitbook mobi ./myrepo` * **JSON**: This format is used for debugging or extracting metadata from a book. Generate this format using: ```gitbook build ./myrepo -f json```. ## Book Format @@ -211,6 +220,8 @@ The platform [GitBook.io](https://www.gitbook.io/) is like an "Heroku for books" Plugins can be used to extend your book's functionality. Read [GitbookIO/plugin](https://github.com/GitbookIO/plugin) for more information about how to build a plugin for GitBook. +Plugins needed to build a book can be installed using: `gitbook install ./`. + ##### Official plugins: | Name | Description | diff --git a/bin/build.js b/bin/build.js index 965a211..28d7641 100644 --- a/bin/build.js +++ b/bin/build.js @@ -44,13 +44,6 @@ var makeBuildFunc = function(converter) { .then(function(output) { console.log("Successfully built!"); return output; - }) - .fail(function(err) { - // Log error - utils.logError(err); - - // Exit process with failure code - process.exit(-1); }); }; }; diff --git a/bin/gitbook.js b/bin/gitbook.js index 7a49cde..80d139f 100755 --- a/bin/gitbook.js +++ b/bin/gitbook.js @@ -7,11 +7,12 @@ var prog = require('commander'); var tinylr = require('tiny-lr-fork'); var pkg = require('../package.json'); -var generators = require("../lib/generate").generators; +var genbook = require("../lib/generate"); var initDir = require("../lib/generate/init"); var fs = require('../lib/generate/fs'); var utils = require('./utils'); +var action = utils.action; var build = require('./build'); var Server = require('./server'); var platform = require("./platform"); @@ -22,13 +23,13 @@ prog build.command(prog.command('build [source_dir]')) .description('Build a gitbook from a directory') -.action(build.folder); +.action(action(build.folder)); build.command(prog.command('serve [source_dir]')) .description('Build then serve a gitbook from a directory') .option('-p, --port <port>', 'Port for server to listen on', 4000) .option('--no-watch', 'Disable restart with file watching') -.action(function(dir, options) { +.action(action(function(dir, options) { var server = new Server(); // init livereload server @@ -41,9 +42,11 @@ build.command(prog.command('serve [source_dir]')) }); var generate = function() { - if (server.isRunning()) console.log("Stopping server"); + if (server.isRunning()) { + console.log("Stopping server"); + } - server.stop() + return server.stop() .then(function() { return build.folder(dir, _.extend(options || {}, { defaultsPlugins: ["livereload"] @@ -77,59 +80,78 @@ build.command(prog.command('serve [source_dir]')) console.log('Press CTRL+C to quit ...'); console.log('') - generate(); -}); + + return generate(); +})); + +build.commandEbook(prog.command('install [source_dir]')) +.description('Install plugins for a book') +.action(action(function(dir, options) { + dir = dir || process.cwd(); + + console.log("Install plugins in", dir); + + return genbook.config.read({ + input: dir + }) + .then(function(options) { + return genbook.Plugin.install(options); + }) + .then(function() { + console.log("Successfully installed plugins!"); + }); +})); build.commandEbook(prog.command('pdf [source_dir]')) .description('Build a gitbook as a PDF') -.action(function(dir, options) { - build.file(dir, _.extend(options, { +.action(action(function(dir, options) { + return build.file(dir, _.extend(options, { extension: "pdf", format: "ebook" })); -}); +})); build.commandEbook(prog.command('epub [source_dir]')) .description('Build a gitbook as a ePub book') -.action(function(dir, options) { - build.file(dir, _.extend(options, { +.action(action(function(dir, options) { + return build.file(dir, _.extend(options, { extension: "epub", format: "ebook" })); -}); +})); build.commandEbook(prog.command('mobi [source_dir]')) .description('Build a gitbook as a Mobi book') -.action(function(dir, options) { - build.file(dir, _.extend(options, { +.action(action(function(dir, options) { + return build.file(dir, _.extend(options, { extension: "mobi", format: "ebook" })); -}); +})); prog .command('init [source_dir]') .description('Create files and folders based on contents of SUMMARY.md') -.action(function(dir) { +.action(action(function(dir) { dir = dir || process.cwd(); return initDir(dir); -}); +})); prog .command('publish [source_dir]') .description('Publish content to the associated gitbook.io book') -.action(function(dir) { +.action(action(function(dir) { dir = dir || process.cwd(); return platform.publish(dir); -}); +})); prog .command('git:remote [source_dir] [book_id]') .description('Adds a git remote to a book repository') -.action(function(dir, bookId) { +.action(action(function(dir, bookId) { dir = dir || process.cwd(); return platform.remote(dir, bookId); -}); +})); // Parse and fallback to help if no args if(_.isEmpty(prog.parse(process.argv).args) && process.argv.length === 2) { diff --git a/bin/utils.js b/bin/utils.js index 821112e..84d2e2f 100644 --- a/bin/utils.js +++ b/bin/utils.js @@ -30,6 +30,34 @@ function watch(dir) { return d.promise; } +// exit wraps a promise +// and forcefully quits the program when the promise is resolved +function exit(promise) { + promise + .then(function() { + // Prevent badly behaving plugins + // from making the process hang + process.exit(0); + }, function(err) { + // Log error + logError(err); + + // Exit process with failure code + process.exit(-1); + }); +} + +// CLI action wrapper, calling exit when finished +function action(f) { + return function() { + // Call func and get optional promise + var p = f.apply(null, arguments); + + // Exit process + return exit(Q(p)); + } +} + function logError(err) { var message = err.message || err; if (process.env.DEBUG != null) message = err.stack || message; @@ -60,6 +88,8 @@ function runGitCommand(command, args) { // Exports module.exports = { + exit: exit, + action: action, watch: watch, logError: logError, gitCmd: runGitCommand diff --git a/lib/generate/config.js b/lib/generate/config.js index 7ec0741..fb232a6 100644 --- a/lib/generate/config.js +++ b/lib/generate/config.js @@ -1,3 +1,4 @@ +var Q = require('q'); var _ = require('lodash'); var path = require('path'); @@ -16,9 +17,10 @@ var CONFIG = { // Configuration file to use "configFile": "book", - // Book title and description (defaults are extracted from the README) + // Book metadats (somes are extracted from the README by default) "title": null, "description": null, + "isbn": null, // For ebook format, the extension to use for generation (default is detected from output extension) // "epub", "pdf", "mobi" @@ -96,10 +98,32 @@ var CONFIG = { } }; +// Return complete configuration +var defaultsConfig = function(options) { + return _.merge(options || {}, CONFIG, _.defaults); +}; + +// Read configuration from book.json +var readConfig = function(options) { + options = defaultsConfig(options); + + return Q() + .then(function() { + try { + var _config = require(path.resolve(options.input, options.configFile)); + options = _.merge(options, _.omit(_config, 'input', 'configFile', 'defaultsPlugins', 'generator')); + } + catch(err) { + // No config file: not a big deal + return Q(); + } + }) + .thenResolve(options); +}; + module.exports = { CONFIG: CONFIG, - defaults: function(options) { - return _.merge(options || {}, CONFIG, _.defaults); - } + defaults: defaultsConfig, + read: readConfig } diff --git a/lib/generate/ebook/index.js b/lib/generate/ebook/index.js index bc39e15..4ecccec 100644 --- a/lib/generate/ebook/index.js +++ b/lib/generate/ebook/index.js @@ -30,6 +30,7 @@ Generator.prototype.finish = function() { "--cover": that.options.cover, "--title": that.options.title, "--comments": that.options.description, + "--isbn": that.options.isbn, "--authors": that.options.author, "--publisher": "GitBook", "--chapter": "descendant-or-self::*[contains(concat(' ', normalize-space(@class), ' '), ' book-chapter ')]", diff --git a/lib/generate/fs.js b/lib/generate/fs.js index 4c232e7..50219eb 100644 --- a/lib/generate/fs.js +++ b/lib/generate/fs.js @@ -64,7 +64,20 @@ var getFiles = function(path) { module.exports = { list: getFiles, readFile: Q.denodeify(fs.readFile), - writeFile: Q.denodeify(fs.writeFile), + //writeFile: Q.denodeify(fs.writeFile), + writeFile: function(filename, data, options) { + var d = Q.defer(); + + try { + fs.writeFileSync(filename, data, options) + } catch(err) { + d.reject(err); + } + d.resolve(); + + + return d.promise; + }, mkdirp: Q.denodeify(fsExtra.mkdirp), copy: Q.denodeify(fsExtra.copy), remove: Q.denodeify(fsExtra.remove), diff --git a/lib/generate/index.js b/lib/generate/index.js index 2118c46..de1fc0e 100644 --- a/lib/generate/index.js +++ b/lib/generate/index.js @@ -48,28 +48,18 @@ var loadGenerator = function(options) { var generate = function(options) { - // Set defaults to options - options = defaultConfig.defaults(options); - - // Validate options - if (!options.input) { - return Q.reject(new Error("Need option input (book input directory)")); - } - - // Check files to get folder type (book, multilanguage book or neither) - return checkGenerator(options) - // Read config file - .then(function() { - try { - var _config = require(path.resolve(options.input, options.configFile)); + return defaultConfig.read(options) + .then(function(_options) { + options = _options; - options = _.merge(options, _.omit(_config, 'input', 'configFile', 'defaultsPlugins')); - } - catch(err) { - // No config file: not a big deal - return Q(); + // Validate options + if (!options.input) { + return Q.reject(new Error("Need option input (book input directory)")); } + + // Check files to get folder type (book, multilanguage book or neither) + return checkGenerator(options); }) // Read readme @@ -99,7 +89,9 @@ var generate = function(options) { }); }; - +/* + * Generate a multilanguage book by generating a book for each folder. + */ var generateMultiLang = function(options) { var langsSummary; options.output = options.output || path.join(options.input, "_book"); @@ -298,6 +290,9 @@ var generateBook = function(options) { // Finish generation .then(function() { + return generator.callHook("finish:before"); + }) + .then(function() { return generator.finish(); }) .then(function() { @@ -375,4 +370,5 @@ module.exports = { file: generateFile, book: generateBook, Plugin: Plugin, + config: defaultConfig }; diff --git a/lib/generate/page/index.js b/lib/generate/page/index.js index 8e44187..a926d13 100644 --- a/lib/generate/page/index.js +++ b/lib/generate/page/index.js @@ -26,6 +26,9 @@ Generator.prototype.loadTemplates = function() { this.summaryTemplate = swig.compileFile( this.plugins.template("ebook:sumary") || path.resolve(this.options.theme, 'templates/ebook/summary.html') ); + this.glossaryTemplate = swig.compileFile( + this.plugins.template("ebook:glossary") || path.resolve(this.options.theme, 'templates/ebook/glossary.html') + ); }; // Generate table of contents @@ -46,7 +49,7 @@ Generator.prototype.finish = function() { var output = path.join(this.options.output, "index.html"); var progress = parse.progress(this.options.navigation, "README.md"); - + return Q() // Write table of contents @@ -54,6 +57,11 @@ Generator.prototype.finish = function() { return that.writeToc(); }) + // Write glossary + .then(function() { + return that.writeGlossary(); + }) + // Copy cover .then(function() { return that.copyCover(); diff --git a/lib/generate/plugin.js b/lib/generate/plugin.js index 9d740a5..5ca5e92 100644 --- a/lib/generate/plugin.js +++ b/lib/generate/plugin.js @@ -4,6 +4,7 @@ var semver = require("semver"); var path = require("path"); var url = require("url"); var fs = require("./fs"); +var npmi = require('npmi'); var resolve = require('resolve'); var pkg = require("../../package.json"); @@ -152,33 +153,73 @@ Plugin.prototype.copyAssets = function(out, options) { }; -// Normalize a list of plugin name to use -Plugin.normalizeNames = function(names) { +// Install a list of plugin +Plugin.install = function(options) { + // Normalize list of plugins + var plugins = Plugin.normalizeList(options.plugins); + + // Install plugins one by one + return _.reduce(plugins, function(prev, plugin) { + return prev.then(function() { + var fullname = "gitbook-plugin-"+plugin.name; + console.log("Install plugin", plugin.name, "from npm ("+fullname+") with version", (plugin.version || "*")); + return Q.nfcall(npmi, { + 'name': fullname, + 'version': plugin.version, + 'path': options.input, + 'npmLoad': { + 'loglevel': 'silent', + 'loaded': false, + 'prefix': options.input + } + }); + }); + }, Q()); +}; + +// Normalize a list of plugins to use +Plugin.normalizeList = function(plugins) { // Normalize list to an array - names = _.isString(names) ? names.split(",") : (names || []); + plugins = _.isString(plugins) ? plugins.split(",") : (plugins || []); + + // Divide as {name, version} to handle format like "myplugin@1.0.0" + plugins = _.map(plugins, function(plugin) { + var parts = plugin.split("@"); + return { + 'name': parts[0], + 'version': parts[1] // optional + } + }); // List plugins to remove - var toremove = _.chain(names) - .filter(function(name) { - return name.length > 0 && name[0] == "-"; + var toremove = _.chain(plugins) + .filter(function(plugin) { + return plugin.name.length > 0 && plugin.name[0] == "-"; }) - .map(function(name) { - return name.slice(1); + .map(function(plugin) { + return plugin.name.slice(1); }) .value(); // Merge with defaults - names = _.chain(names) - .concat(Plugin.defaults) + plugins = _.chain(plugins) + .concat(_.map(Plugin.defaults, function(plugin) { + return { 'name': plugin } + })) .uniq() .value(); - // Remove plugins starting with - names = _.filter(names, function(name) { - return !_.contains(toremove, name) && !(name.length > 0 && name[0] == "-"); + // Build final list + plugins = _.filter(plugins, function(plugin) { + return !_.contains(toremove, plugin.name) && !(plugin.name.length > 0 && plugin.name[0] == "-"); }); - return names; + return plugins; +}; + +// Normalize a list of plugin name to use +Plugin.normalizeNames = function(plugins) { + return _.pluck(Plugin.normalizeList(plugins), "name"); }; // Extract data from a list of plugin @@ -196,7 +237,7 @@ Plugin.fromList = function(names, root, generator, options) { return plugin; }); - if (_.size(failed) > 0) return Q.reject(new Error("Error loading plugins: "+failed.join(","))); + if (_.size(failed) > 0) return Q.reject(new Error("Error loading plugins: "+failed.join(",")+". Run 'gitbook install' to install plugins from NPM.")); // The raw resources extracted from each plugin var pluginResources; @@ -276,7 +317,7 @@ Plugin.fromList = function(names, root, generator, options) { }); }; -// Default plugins +// Default plugins added to each books Plugin.defaults = [ "mathjax" ]; diff --git a/package.json b/package.json index 6861918..71cd55e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gitbook", - "version": "1.3.3", + "version": "1.4.2", "homepage": "http://www.gitbook.io/", "description": "Library and cmd utility to generate GitBooks", "main": "lib/index.js", @@ -19,6 +19,7 @@ "highlight.js": "8.3.0", "tmp": "0.0.23", "semver": "2.2.1", + "npmi": "0.1.1", "gaze": "~0.5.1", "resolve": "0.6.3", "tiny-lr-fork": "0.0.5", diff --git a/test/plugin.js b/test/plugin.js index 6f05bd5..fcde67c 100644 --- a/test/plugin.js +++ b/test/plugin.js @@ -12,7 +12,7 @@ describe('Plugin validation', function () { }); }); -describe('Plugin list of names', function () { +describe('Plugins list', function () { var firstDefault = _.first(Plugin.defaults); it('should convert string to array', function() { @@ -27,6 +27,15 @@ describe('Plugin list of names', function () { it('should remove name starting with -', function() { assert(!_.contains(Plugin.normalizeNames(["-"+firstDefault]), firstDefault)); }); + + it('should accept version', function() { + var _name = "test@0.3.0,exercises@1.2.0,test2"; + var plugins = Plugin.normalizeList(_name); + + assert(_.find(plugins, {'name': "test"}).version = "0.3.0"); + assert(_.find(plugins, {'name': "exercises"}).version = "1.2.0"); + assert(!_.find(plugins, {'name': "test2"}).version); + }); }); describe('Plugin defaults loading', function () { diff --git a/theme/templates/ebook/glossary.html b/theme/templates/ebook/glossary.html new file mode 100644 index 0000000..8076c4d --- /dev/null +++ b/theme/templates/ebook/glossary.html @@ -0,0 +1,15 @@ +{% extends "./layout.html" %} + +{% block title %}Glossary | {{ title }}{% endblock %} + +{% block content %} +<div class="page page-toc"> + <h1>Glossary</h1> + {% for item in glossaryIndex %} + <section class="normal glossary" id="{{ item.id }}"> + <h2><a href="#{{ item.id }}">{{ item.name }}</a></h2> + <p>{{ item.description }}</p> + </section> + {% endfor %} +</div> +{% endblock %} diff --git a/theme/templates/ebook/summary.html b/theme/templates/ebook/summary.html index 85388f9..dfbba2c 100644 --- a/theme/templates/ebook/summary.html +++ b/theme/templates/ebook/summary.html @@ -40,6 +40,10 @@ <h1>Table of Contents</h1> <ol> {{ articles(summary.chapters) }} + + {% if glossary.length > 0 %} + <li><a href="{{ basePath }}/GLOSSARY.html">Glossary</a></li> + {% endif %} </ol> </div> {% endblock %} diff --git a/theme/templates/layout.html b/theme/templates/layout.html index b7afea3..5dc7c5e 100755 --- a/theme/templates/layout.html +++ b/theme/templates/layout.html @@ -4,6 +4,7 @@ <head> {{ htmlSnippet("head:start")|default("") }} <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=11; IE=10; IE=9; IE=8; IE=7; IE=EDGE" /> <title>{% block title %}{% endblock %}</title> <meta content="text/html; charset=utf-8" http-equiv="Content-Type"> <meta name="description" content="{% block description %}{% endblock %}"> @@ -14,6 +15,9 @@ <meta name="apple-mobile-web-app-status-bar-style" content="black"> <link rel="apple-touch-icon-precomposed" sizes="152x152" href="{{ staticBase }}/images/apple-touch-icon-precomposed-152.png"> <link rel="shortcut icon" href="{{ staticBase }}/images/favicon.ico" type="image/x-icon"> + {% if options.isbn %} + <meta name="identifier" content="{{ options.isbn }}" scheme="ISBN"> + {% endif %} {% block head %}{% endblock %} {{ htmlSnippet("head:end")|default("") }} </head> |