diff options
-rw-r--r-- | lib/book.js | 32 | ||||
-rw-r--r-- | lib/parsers.js (renamed from lib/parser.js) | 0 | ||||
-rw-r--r-- | lib/plugin.js | 227 | ||||
-rw-r--r-- | package.json | 9 |
4 files changed, 263 insertions, 5 deletions
diff --git a/lib/book.js b/lib/book.js index 8fbac81..1a8643f 100644 --- a/lib/book.js +++ b/lib/book.js @@ -5,7 +5,8 @@ var path = require("path"); var fs = require("./utils/fs"); var Configuration = require("./configuration"); var TemplateEngine = require("./template"); -var parser = require("./parser"); +var Plugin = require("./plugin"); +var parsers = require("./parsers"); var Book = function(root, options, parent) { // Root folder of the book @@ -39,6 +40,9 @@ var Book = function(root, options, parent) { // Files in the book this.files = []; + + // List of plugins + this.plugins = {}; }; // Initialize and parse the book: config, summary, glossary @@ -49,6 +53,10 @@ Book.prototype.parse = function() { return this.config.load() .then(function() { + return that.parsePlugins(); + }) + + .then(function() { return that.parseLangs() .then(function() { multilingal = that.langs.length > 0; @@ -124,6 +132,24 @@ Book.prototype.generateMultiLingual = function() { }); }; +// Parse list of plugins +Book.prototype.parsePlugins = function() { + var that = this; + var names = Plugin.normalizeNames(this.options.plugins); + + var failed = []; + + // Load plugins + that.plugins = _.map(names, function(name) { + var plugin = new Plugin(that, name); + if (!plugin.isValid()) failed.push(name); + return plugin; + }); + + if (_.size(failed) > 0) return Q.reject(new Error("Error loading plugins: "+failed.join(",")+". Run 'gitbook install' to install plugins from NPM.")); + return Q(); +}; + // Parse readme to extract defaults title and description Book.prototype.parseReadme = function() { var that = this; @@ -205,7 +231,7 @@ Book.prototype.parseGlossary = function() { Book.prototype.findFile = function(filename) { var that = this; - return _.reduce(parser.extensions, function(prev, ext) { + return _.reduce(parsers.extensions, function(prev, ext) { return prev.then(function(output) { // Stop if already find a parser if (output) return output; @@ -216,7 +242,7 @@ Book.prototype.findFile = function(filename) { .then(function(exists) { if (!exists) return null; return { - parser: parser.get(ext).parser, + parser: parsers.get(ext).parser, path: filepath }; }) diff --git a/lib/parser.js b/lib/parsers.js index da9732a..da9732a 100644 --- a/lib/parser.js +++ b/lib/parsers.js diff --git a/lib/plugin.js b/lib/plugin.js new file mode 100644 index 0000000..c2cc668 --- /dev/null +++ b/lib/plugin.js @@ -0,0 +1,227 @@ +var _ = require("lodash"); +var Q = require("q"); +var semver = require("semver"); +var path = require("path"); +var url = require("url"); +var fs = require("./utils/fs"); +var npmi = require('npmi'); +var resolve = require('resolve'); + +var pkg = require("../package.json"); + +var Plugin = function(book, name) { + this.book = book; + this.name = name; + this.packageInfos = {}; + this.infos = {}; + + // Bind methods + _.bindAll(this); + + _.each([ + "gitbook-plugin-"+name, + "gitbook-"+name, + name, + ], function(_name) { + if (this.load(_name, __dirname)) return false; + if (this.load(_name, book.root)) return false; + }, this); +}; + +// Type of plugins resources +Plugin.RESOURCES = ["js", "css"]; + +// Default plugins added to each books +Plugin.defaults = ["mathjax"]; + +// Load from a name +Plugin.prototype.load = function(name, baseDir) { + try { + var res = resolve.sync(name+"/package.json", { basedir: baseDir }); + + this.baseDir = path.dirname(res); + this.packageInfos = require(res); + this.infos = require(resolve.sync(name, { basedir: baseDir })); + this.name = name; + + return true; + } catch (e) { + return false; + } +}; + +Plugin.prototype.normalizeResource = function(resource) { + // Parse the resource path + var parsed = url.parse(resource); + + // This is a remote resource + // so we will simply link to using it's URL + if (parsed.protocol) { + return { + "url": resource + }; + } + + // This will be copied over from disk + // and shipped with the book's build + return { "path": this.name+"/"+resource }; +}; + +// Return resources +Plugin.prototype._getResources = function(base) { + base = base || "book"; + var book = this.infos[base]; + + // Nothing specified, fallback to default + if (!book) { + return Q({}); + } + + // Dynamic function + if(typeof book === "function") { + // Call giving it the context of our generator + return Q().then(book.bind(this.book)); + } + + // Plain data object + return Q(_.cloneDeep(book)); +}; + +// Normalize resources and return them +Plugin.prototype.getResources = function(base) { + var that = this; + + return this._getResources(base) + .then(function(resources) { + + _.each(RESOURCES, function(resourceType) { + resources[resourceType] = (resources[resourceType] || []).map(that.normalizeResource); + }); + + return resources; + }); +}; + +// 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(this.baseDir, filename); +}; + +// Resolve file path +Plugin.prototype.callHook = function(name, data) { + // Our generator will be the context to apply + var context = this.book; + + var hookFunc = this.infos.hooks? this.infos.hooks[name] : null; + data = data || {}; + + if (!hookFunc) return Q(data); + + return Q() + .then(function() { + return hookFunc.apply(context, [data]); + }); +}; + +// Copy plugin assets fodler +Plugin.prototype.copyAssets = function(out, options) { + var that = this; + options = _.defaults(options || {}, { + base: "book" + }); + + return this.getResources(options.base) + .get('assets') + .then(function(assets) { + // Assets are undefined + if(!assets) return false; + + return fs.copy( + that.resolveFile(assets), + out + ).then(_.constant(true)); + }, _.constant(false)); +}; + + +// 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 + 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(plugins) + .filter(function(plugin) { + return plugin.name.length > 0 && plugin.name[0] == "-"; + }) + .map(function(plugin) { + return plugin.name.slice(1); + }) + .value(); + + // Merge with defaults + plugins = _.chain(plugins) + .concat(_.map(Plugin.defaults, function(plugin) { + return { 'name': plugin } + })) + .uniq() + .value(); + + // Build final list + plugins = _.filter(plugins, function(plugin) { + return !_.contains(toremove, plugin.name) && !(plugin.name.length > 0 && plugin.name[0] == "-"); + }); + + return plugins; +}; + +// Normalize a list of plugin name to use +Plugin.normalizeNames = function(plugins) { + return _.pluck(Plugin.normalizeList(plugins), "name"); +}; + +module.exports = Plugin; diff --git a/package.json b/package.json index dd4a540..11ef3fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gitbook", - "version": "1.5.0", + "version": "2.0.0-alpha", "homepage": "https://www.gitbook.com", "description": "Library and cmd utility to generate GitBooks", "main": "lib/index.js", @@ -8,10 +8,15 @@ "q": "1.0.1", "lodash": "2.4.1", "graceful-fs": "3.0.5", + "resolve": "0.6.3", "fs-extra": "0.14.0", "fstream-ignore": "1.0.2", "gitbook-markdown": "1.0.0", - "nunjucks": "git+https://github.com/SamyPesse/nunjucks.git#4019d1b7379372336b86ce1b0bf84352a2029747" + "nunjucks": "git+https://github.com/SamyPesse/nunjucks.git#4019d1b7379372336b86ce1b0bf84352a2029747", + "semver": "2.2.1", + "npmi": "0.1.1", + "gitbook-plugin-mathjax": "0.0.6", + "gitbook-plugin-livereload": "0.0.1" }, "devDependencies": { "mocha": "1.18.2" |