diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/generate/generator.js | 21 | ||||
-rw-r--r-- | lib/generate/index.js | 16 | ||||
-rw-r--r-- | lib/generate/plugin.js | 180 | ||||
-rw-r--r-- | lib/generate/site/index.js | 39 | ||||
-rw-r--r-- | lib/parse/navigation.js | 75 | ||||
-rw-r--r-- | lib/parse/page.js | 27 | ||||
-rw-r--r-- | lib/parse/renderer.js | 10 |
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); |