summaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/generate/generator.js21
-rw-r--r--lib/generate/index.js16
-rw-r--r--lib/generate/plugin.js180
-rw-r--r--lib/generate/site/index.js39
-rw-r--r--lib/parse/navigation.js75
-rw-r--r--lib/parse/page.js27
-rw-r--r--lib/parse/renderer.js10
7 files changed, 298 insertions, 70 deletions
diff --git a/lib/generate/generator.js b/lib/generate/generator.js
index 13022a5..e303345 100644
--- a/lib/generate/generator.js
+++ b/lib/generate/generator.js
@@ -1,9 +1,30 @@
+var _ = require("lodash");
var path = require("path");
var Q = require("q");
var fs = require("./fs");
+var Plugin = require("./plugin");
+
var BaseGenerator = function(options) {
this.options = options;
+
+ this.options.plugins = Plugin.normalizeNames(this.options.plugins);
+ this.plugins = [];
+};
+
+BaseGenerator.prototype.callHook = function(name) {
+ return this.plugins.hook(name, this);
+};
+
+BaseGenerator.prototype.loadPlugins = function() {
+ var that = this;
+
+ return Plugin.fromList(this.options.plugins)
+ .then(function(_plugins) {
+ that.plugins = _plugins;
+
+ return that.callHook("init");
+ });
};
BaseGenerator.prototype.convertFile = function(content, input) {
diff --git a/lib/generate/index.js b/lib/generate/index.js
index 33edc1f..bbe5ec2 100644
--- a/lib/generate/index.js
+++ b/lib/generate/index.js
@@ -6,6 +6,7 @@ var tmp = require('tmp');
var fs = require("./fs");
var parse = require("../parse");
+var Plugin = require("./plugin");
var generators = {
"site": require("./site"),
@@ -42,7 +43,11 @@ var generate = function(options) {
githubHost: 'https://github.com/',
// Theming
- theme: path.resolve(__dirname, '../../theme')
+ theme: path.resolve(__dirname, '../../theme'),
+
+ // Plugins
+ plugins: [],
+ pluginsConfig: {}
});
if (!options.input) {
@@ -70,9 +75,10 @@ var generate = function(options) {
})
})
- // Create the generator
+ // Create the generator and load plugins
.then(function() {
generator = new generators[options.generator](options);
+ return generator.loadPlugins();
})
// Detect multi-languages book
@@ -163,6 +169,9 @@ var generate = function(options) {
// Finish gneration
.then(function() {
return generator.finish();
+ })
+ .then(function() {
+ return generator.callHook("finish");
});
}
})
@@ -234,5 +243,6 @@ var generateFile = function(options) {
module.exports = {
generators: generators,
folder: generate,
- file: generateFile
+ file: generateFile,
+ Plugin: Plugin
};
diff --git a/lib/generate/plugin.js b/lib/generate/plugin.js
new file mode 100644
index 0000000..5bd332b
--- /dev/null
+++ b/lib/generate/plugin.js
@@ -0,0 +1,180 @@
+var _ = require("lodash");
+var Q = require("q");
+var semver = require("semver");
+var path = require("path");
+var url = require("url");
+var fs = require("./fs");
+
+var pkg = require("../../package.json");
+
+var RESOURCES = ["js", "css"];
+
+var Plugin = function(name) {
+ this.name = name;
+ this.packageInfos = {};
+ this.infos = {};
+
+ _.each([
+ name,
+ "gitbook-"+name,
+ "gitbook-plugin-"+name,
+ "gitbook-theme-"+name
+ ], function(_name) {
+ if (this.load(_name)) return false;
+ }.bind(this));
+};
+
+// Load from a name
+Plugin.prototype.load = function(name) {
+ try {
+ this.packageInfos = require(name+"/package.json");
+ this.infos = require(name);
+ this.name = name;
+ return true;
+ } catch (e) {
+ return false;
+ }
+};
+
+// Return resources
+Plugin.prototype.getResources = function(resource) {
+ if (!this.infos.book || !this.infos.book[resource]) {
+ return [];
+ }
+ return _.chain(this.infos.book[resource])
+ .map(function(resource) {
+ var parsed = url.parse(resource);
+ if (parsed.protocol) return {"url": resource}
+ else return { "path": this.name+"/"+resource };
+ }.bind(this))
+ .value();
+};
+
+// Test if it's a valid plugin
+Plugin.prototype.isValid = function() {
+ return (
+ this.packageInfos
+ && this.packageInfos.name
+ && this.packageInfos.engines
+ && this.packageInfos.engines.gitbook
+ && semver.satisfies(pkg.version, this.packageInfos.engines.gitbook)
+ );
+};
+
+// Resolve file path
+Plugin.prototype.resolveFile = function(filename) {
+ return path.resolve(path.dirname(require.resolve(this.name)), filename);
+};
+
+// Resolve file path
+Plugin.prototype.callHook = function(name, args) {
+ var hookFunc = this.infos.hooks? this.infos.hooks[name] : null;
+ args = _.isArray(args) ? args : [args];
+
+ if (!hookFunc) return Q();
+
+ return Q()
+ .then(function() {
+ return hookFunc.apply(null, args);
+ });
+};
+
+// Copy plugin assets fodler
+Plugin.prototype.copyAssets = function(out) {
+ if (!this.infos.book || !this.infos.book.assets) return Q();
+ return fs.copy(
+ this.resolveFile(this.infos.book.assets),
+ out
+ );
+};
+
+
+
+
+// Normalize a list of plugin name to use
+Plugin.normalizeNames = function(names) {
+ // Normalize list to an array
+ names = _.isString(names) ? names.split(":") : (names || []);
+
+ // List plugins to remove
+ var toremove = _.chain(names)
+ .filter(function(name) {
+ return name.length > 0 && name[0] == "-";
+ })
+ .map(function(name) {
+ return name.slice(1)
+ })
+ .value();
+
+ // Merge with defaults
+ names = _.chain(names)
+ .concat(Plugin.defaults)
+ .uniq()
+ .value();
+
+ // Remove plugins starting with
+ names = _.filter(names, function(name) {
+ return !_.contains(toremove, name) && !(name.length > 0 && name[0] == "-");
+ });
+
+ return names;
+};
+
+// Extract data from a list of plugin
+Plugin.fromList = function(names) {
+ var failed = [];
+
+ // Load plugins
+ var plugins = _.map(names, function(name) {
+ var plugin = new Plugin(name);
+ if (!plugin.isValid()) failed.push(name);
+ return plugin;
+ });
+
+ if (_.size(failed) > 0) return Q.reject(new Error("Error loading plugins: "+failed.join(",")));
+
+ // Get all resources
+ var resources = _.chain(RESOURCES)
+ .map(function(resource) {
+ return [
+ resource,
+ _.chain(plugins)
+ .map(function(plugin) {
+ return plugin.getResources(resource);
+ })
+ .flatten()
+ .value()
+ ];
+ })
+ .object()
+ .value();
+
+ return Q({
+ 'list': plugins,
+ 'resources': resources,
+ 'hook': function(name, args) {
+ return _.reduce(plugins, function(prev, plugin) {
+ return prev.then(function() {
+ return plugin.callHook(name, args);
+ })
+ }, Q());
+ },
+ 'template': function(name) {
+ var withTpl = _.find(plugins, function(plugin) {
+ return (plugin.infos.templates
+ && plugin.infos.templates[name]);
+ });
+
+ if (!withTpl) return null;
+ return withTpl.resolveFile(withTpl.infos.templates[name]);
+ }
+ });
+};
+
+// Default plugins
+Plugin.defaults = [
+ "mixpanel",
+ "mathjax"
+];
+
+module.exports = Plugin; \ No newline at end of file
diff --git a/lib/generate/site/index.js b/lib/generate/site/index.js
index 4bea547..68328e7 100644
--- a/lib/generate/site/index.js
+++ b/lib/generate/site/index.js
@@ -30,13 +30,28 @@ var Generator = function() {
this.revision = Date.now();
this.indexer = indexer();
-
- // Load base template
- this.template = swig.compileFile(path.resolve(this.options.theme, 'templates/site.html'));
- this.langsTemplate = swig.compileFile(path.resolve(this.options.theme, 'templates/langs.html'));
};
util.inherits(Generator, BaseGenerator);
+// Load all templates
+Generator.prototype.loadTemplates = function() {
+ this.template = swig.compileFile(
+ this.plugins.template("site") || path.resolve(this.options.theme, 'templates/site.html')
+ );
+ this.langsTemplate = swig.compileFile(
+ this.plugins.template("langs") || path.resolve(this.options.theme, 'templates/langs.html')
+ );
+};
+
+// Load plugins
+Generator.prototype.loadPlugins = function() {
+ var that = this;
+
+ return BaseGenerator.prototype.loadPlugins.apply(this)
+ .then(function() {
+ return that.loadTemplates();
+ });
+};
// Generate a template
Generator.prototype._writeTemplate = function(tpl, options, output) {
@@ -54,7 +69,10 @@ Generator.prototype._writeTemplate = function(tpl, options, output) {
githubHost: that.options.githubHost,
summary: that.options.summary,
- allNavigation: that.options.navigation
+ allNavigation: that.options.navigation,
+
+ plugins: that.plugins,
+ pluginsConfig: JSON.stringify(that.options.pluginsConfig)
}, options));
})
.then(function(html) {
@@ -134,10 +152,19 @@ Generator.prototype.langsIndex = function(langs) {
Generator.prototype.copyAssets = function() {
var that = this;
+ // Copy gitbook assets
return fs.copy(
path.join(that.options.theme, "assets"),
path.join(that.options.output, "gitbook")
- );
+ )
+ // Copy plugins assets
+ .then(function() {
+ return Q.all(
+ _.map(that.plugins.list, function(plugin) {
+ return plugin.copyAssets(path.join(that.options.output, "gitbook/plugins/", plugin.name))
+ })
+ );
+ })
};
// Dump search index to disk
diff --git a/lib/parse/navigation.js b/lib/parse/navigation.js
index bba25ce..2f8669e 100644
--- a/lib/parse/navigation.js
+++ b/lib/parse/navigation.js
@@ -6,6 +6,12 @@ function clean(obj) {
return obj && _.omit(obj, ['articles']);
}
+function flattenChapters(chapters) {
+ return _.reduce(chapters, function(accu, chapter) {
+ return accu.concat([clean(chapter)].concat(chapter.articles));
+ }, []);
+}
+
// Returns from a summary a map of
/*
{
@@ -20,61 +26,36 @@ function navigation(summary, files) {
// Support single files as well as list
files = _.isArray(files) ? files : (_.isString(files) ? [files] : null);
- // Mapping of prev/next for a give path
- var mapping = {};
-
// Special README nav
var README_NAV = {
path: 'README.md',
title: 'Introduction',
+ level: '0',
};
- // Walk the chapter/article tree and create navigation entires
- _.each(summary.chapters, function(chapter, idx, chapters) {
- // Skip if no path
- if(!chapter.path) return;
-
- var currentChapter = clean(chapter);
- var prevChapter = (idx-1 < 0) ? README_NAV : chapters[idx-1];
- var nextChapter = (idx+1 >= chapters.length) ? null : chapters[idx+1];
-
- var prev = (!prevChapter || _.isEmpty(prevChapter.articles)) ?
- prevChapter : _.last(prevChapter.articles);
- var next = (!chapter || _.isEmpty(chapter.articles)) ?
- nextChapter : _.first(chapter.articles);
+ // List of all navNodes
+ var navNodes = [README_NAV].concat(flattenChapters(summary.chapters));
+ var prevNodes = [null].concat(navNodes.slice(0, -1));
+ var nextNodes = navNodes.slice(1).concat([null]);
- // Add chapter mapping
- mapping[chapter.path] = {
- title: chapter.title,
- prev: clean(prev),
- next: clean(next),
- level: (idx+1).toString(),
- };
-
- // Check a chapter's articles
- _.each(chapter.articles, function(article, _idx, articles) {
- // Skip if no path
- if(!article.path) return;
-
- var prev = (_idx-1 < 0) ? currentChapter : clean(articles[_idx-1]);
- var next = (_idx+1 >= articles.length) ? nextChapter : clean(articles[_idx+1]);
-
- mapping[article.path] = {
- title: article.title,
- prev: clean(prev),
- next: clean(next),
- level: [idx+1, _idx+1].join('.'),
- };
- });
- });
+ // Mapping of prev/next for a give path
+ var mapping = _.chain(_.zip(navNodes, prevNodes, nextNodes))
+ .map(function(nodes) {
+ var current = nodes[0], prev = nodes[1], next = nodes[2];
- // Hack for README.html
- mapping['README.md'] = {
- title: README_NAV.title,
- prev: null,
- next: clean(summary.chapters[0]),
- level: '0',
- };
+ // Skip if no path
+ if(!current.path) return null;
+
+ return [current.path, {
+ title: current.title,
+ prev: prev,
+ next: next,
+ level: current.level,
+ }];
+ })
+ .filter()
+ .object()
+ .value();
// Filter for only files we want
if(files) {
diff --git a/lib/parse/page.js b/lib/parse/page.js
index eb118e4..6cfd3ca 100644
--- a/lib/parse/page.js
+++ b/lib/parse/page.js
@@ -8,17 +8,6 @@ var renderer = require('./renderer');
var lnormalize = require('../utils/lang').normalize;
-// Synchronous highlighting with highlight.js
-marked.setOptions({
- highlight: function (code, lang) {
- try {
- return hljs.highlight(lang, code).value;
- } catch(e) {
- return hljs.highlightAuto(code).value;
- }
- }
-});
-
// Render a section using our custom renderer
function render(section, _options) {
@@ -30,7 +19,21 @@ function render(section, _options) {
// Build options using defaults and our custom renderer
var options = _.extend({}, marked.defaults, {
- renderer: renderer(null, _options)
+ renderer: renderer(null, _options),
+
+ // Synchronous highlighting with highlight.js
+ highlight: function (code, lang) {
+ if(!lang) return code;
+
+ // Normalize lang
+ lang = lnormalize(lang);
+
+ try {
+ return hljs.highlight(lang, code).value;
+ } catch(e) { }
+
+ return code;
+ }
});
return marked.parser(section, options);
diff --git a/lib/parse/renderer.js b/lib/parse/renderer.js
index 2a72d48..949a9ee 100644
--- a/lib/parse/renderer.js
+++ b/lib/parse/renderer.js
@@ -79,8 +79,14 @@ GitBookRenderer.prototype.image = function(href, title, text) {
// Relative image, rewrite it depending output
if(!parsed.protocol && parsed.path && parsed.path[0] != '/' && o && o.dir && o.outdir) {
- var outdir = o.outdir.charAt(o.outdir.length - 1) === '/' ? o.outdir : o.outdir + '/';
- _href = url.resolve(outdir, [o.dir, href].join('/'));
+ // o.dir: directory parent of the file currently in rendering process
+ // o.outdir: directory parent from the html output
+
+ // Absolute file in source
+ _href = path.join(o.dir, _href);
+
+ // make it relative to output
+ _href = path.relative(o.outdir, _href);
}
return GitBookRenderer.super_.prototype.image.call(this, _href, title, text);