summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGES.md12
-rw-r--r--README.md15
-rw-r--r--bin/build.js7
-rwxr-xr-xbin/gitbook.js66
-rw-r--r--bin/utils.js30
-rw-r--r--lib/generate/config.js32
-rw-r--r--lib/generate/ebook/index.js1
-rw-r--r--lib/generate/fs.js15
-rw-r--r--lib/generate/index.js36
-rw-r--r--lib/generate/page/index.js10
-rw-r--r--lib/generate/plugin.js73
-rw-r--r--package.json3
-rw-r--r--test/plugin.js11
-rw-r--r--theme/templates/ebook/glossary.html15
-rw-r--r--theme/templates/ebook/summary.html4
-rwxr-xr-xtheme/templates/layout.html4
16 files changed, 259 insertions, 75 deletions
diff --git a/CHANGES.md b/CHANGES.md
index 67c4441..6b587b3 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -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
diff --git a/README.md b/README.md
index 43f35e9..dcf974b 100644
--- a/README.md
+++ b/README.md
@@ -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>