diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/book.js | 17 | ||||
-rw-r--r-- | lib/config_default.js | 108 | ||||
-rw-r--r-- | lib/configuration.js | 130 | ||||
-rw-r--r-- | lib/generator.js | 26 | ||||
-rw-r--r-- | lib/plugin.js | 107 | ||||
-rw-r--r-- | lib/pluginslist.js | 56 |
6 files changed, 273 insertions, 171 deletions
diff --git a/lib/book.js b/lib/book.js index 72f0ec2..39d4719 100644 --- a/lib/book.js +++ b/lib/book.js @@ -178,6 +178,14 @@ Book.prototype.generate = function(generator) { return generator.prepare(); }) + // Transform configuration + .then(function() { + return that.callHook('config', that.config.dump()) + .then(function(newConfig) { + that.config.replace(newConfig); + }); + }) + // Generate content .then(function() { if (that.isMultilingual()) { @@ -238,13 +246,13 @@ Book.prototype.generate = function(generator) { // Finish generation .then(function() { - return generator.callHook('finish:before'); + return that.callHook('finish:before'); }) .then(function() { return generator.finish(); }) .then(function() { - return generator.callHook('finish'); + return that.callHook('finish'); }) .then(function() { that.log.info.ln('generation is finished'); @@ -793,4 +801,9 @@ Book.prototype.normError = function(err, opts, defs) { return err; }; +// Call a hook in plugins +Book.prototype.callHook = function(name, data) { + return this.plugins.hook(name, data); +}; + module.exports= Book; diff --git a/lib/config_default.js b/lib/config_default.js new file mode 100644 index 0000000..328531c --- /dev/null +++ b/lib/config_default.js @@ -0,0 +1,108 @@ +var path = require('path'); + +module.exports = { + // Options that can't be extend + 'configFile': 'book', + 'generator': 'website', + 'extension': null, + + // Book metadats (somes are extracted from the README by default) + 'title': null, + 'description': null, + 'isbn': null, + 'language': 'en', + 'direction': null, + 'author': null, + + // version of gitbook to use + 'gitbook': '*', + + // Structure + 'structure': { + 'langs': 'LANGS.md', + 'readme': 'README.md', + 'glossary': 'GLOSSARY.md', + 'summary': 'SUMMARY.md' + }, + + // CSS Styles + 'styles': { + 'website': 'styles/website.css', + 'print': 'styles/print.css', + 'ebook': 'styles/ebook.css', + 'pdf': 'styles/pdf.css', + 'mobi': 'styles/mobi.css', + 'epub': 'styles/epub.css' + }, + + // Plugins list, can contain '-name' for removing default plugins + 'plugins': [], + + // Global configuration for plugins + 'pluginsConfig': {}, + + // Variables for templating + 'variables': {}, + + // Set another theme with your own layout + // It's recommended to use plugins or add more options for default theme, though + // See https://github.com/GitbookIO/gitbook/issues/209 + 'theme': path.resolve(__dirname, '../theme'), + + // Links in template (null: default, false: remove, string: new value) + 'links': { + // Custom links at top of sidebar + 'sidebar': { + // 'Custom link name': 'https://customlink.com' + }, + + // Sharing links + 'sharing': { + 'google': null, + 'facebook': null, + 'twitter': null, + 'weibo': null, + 'all': null + } + }, + + + // Options for PDF generation + 'pdf': { + // Add toc at the end of the file + 'toc': true, + + // Add page numbers to the bottom of every page + 'pageNumbers': false, + + // Font size for the file content + 'fontSize': 12, + + // Paper size for the pdf + // Choices are [u’a0’, u’a1’, u’a2’, u’a3’, u’a4’, u’a5’, u’a6’, u’b0’, u’b1’, u’b2’, u’b3’, u’b4’, u’b5’, u’b6’, u’legal’, u’letter’] + 'paperSize': 'a4', + + // How to mark detected chapters. + // Choices are “pagebreak”, “rule”, 'both' or “none”. + 'chapterMark' : 'pagebreak', + + // An XPath expression. Page breaks are inserted before the specified elements. + // To disable use the expression: '/' + 'pageBreaksBefore': '/', + + // Margin (in pts) + // Note: 72 pts equals 1 inch + 'margin': { + 'right': 62, + 'left': 62, + 'top': 56, + 'bottom': 56 + }, + + // Header HTML template. Available variables: _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_. + 'headerTemplate': null, + + // Footer HTML template. Available variables: _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_. + 'footerTemplate': null + } +}; diff --git a/lib/configuration.js b/lib/configuration.js index 7488fae..d514720 100644 --- a/lib/configuration.js +++ b/lib/configuration.js @@ -1,12 +1,13 @@ var _ = require('lodash'); var Q = require('q'); -var fs = require('fs'); var path = require('path'); var semver = require('semver'); var pkg = require('../package.json'); var i18n = require('./utils/i18n'); +var DEFAULT_CONFIG = require('./config_default'); + // Default plugins added to each books var DEFAULT_PLUGINS = ['highlight', 'search', 'sharing', 'fontsettings']; @@ -85,9 +86,7 @@ var Configuration = function(book, options) { var that = this; this.book = book; - - this.options = _.cloneDeep(Configuration.DEFAULT); - this.options = _.merge(this.options, options || {}); + this.replace(options); // options.input == book.root Object.defineProperty(this.options, 'input', { @@ -169,6 +168,17 @@ Configuration.prototype.extend = function(options) { _.extend(this.options, options); }; +// Replace the whole configuration +Configuration.prototype.replace = function(options) { + this.options = _.cloneDeep(DEFAULT_CONFIG); + this.options = _.merge(this.options, options || {}); +}; + +// Dump configuration as json object +Configuration.prototype.dump = function() { + return _.cloneDeep(this.options); +}; + // Get structure file Configuration.prototype.getStructure = function(name) { return this.options.structure[name].split('.').slice(0, -1).join('.'); @@ -184,112 +194,12 @@ Configuration.prototype.get = function(key, def) { return _.get(this.options, key, def); }; -// Default configuration -Configuration.DEFAULT = { - // Options that can't be extend - 'configFile': 'book', - 'generator': 'website', - 'extension': null, - - // Book metadats (somes are extracted from the README by default) - 'title': null, - 'description': null, - 'isbn': null, - 'language': 'en', - 'direction': null, - 'author': null, - - // version of gitbook to use - 'gitbook': '*', - - // Structure - 'structure': { - 'langs': 'LANGS.md', - 'readme': 'README.md', - 'glossary': 'GLOSSARY.md', - 'summary': 'SUMMARY.md' - }, - - // CSS Styles - 'styles': { - 'website': 'styles/website.css', - 'print': 'styles/print.css', - 'ebook': 'styles/ebook.css', - 'pdf': 'styles/pdf.css', - 'mobi': 'styles/mobi.css', - 'epub': 'styles/epub.css' - }, - - // Plugins list, can contain '-name' for removing default plugins - 'plugins': [], - - // Global configuration for plugins - 'pluginsConfig': {}, - - // Variables for templating - 'variables': {}, - - // Set another theme with your own layout - // It's recommended to use plugins or add more options for default theme, though - // See https://github.com/GitbookIO/gitbook/issues/209 - 'theme': path.resolve(__dirname, '../theme'), - - // Links in template (null: default, false: remove, string: new value) - 'links': { - // Custom links at top of sidebar - 'sidebar': { - // 'Custom link name': 'https://customlink.com' - }, - - // Sharing links - 'sharing': { - 'google': null, - 'facebook': null, - 'twitter': null, - 'weibo': null, - 'all': null - } - }, - - - // Options for PDF generation - 'pdf': { - // Add toc at the end of the file - 'toc': true, - - // Add page numbers to the bottom of every page - 'pageNumbers': false, - - // Font size for the file content - 'fontSize': 12, - - // Paper size for the pdf - // Choices are [u’a0’, u’a1’, u’a2’, u’a3’, u’a4’, u’a5’, u’a6’, u’b0’, u’b1’, u’b2’, u’b3’, u’b4’, u’b5’, u’b6’, u’legal’, u’letter’] - 'paperSize': 'a4', - - // How to mark detected chapters. - // Choices are “pagebreak”, “rule”, 'both' or “none”. - 'chapterMark' : 'pagebreak', - - // An XPath expression. Page breaks are inserted before the specified elements. - // To disable use the expression: '/' - 'pageBreaksBefore': '/', - - // Margin (in pts) - // Note: 72 pts equals 1 inch - 'margin': { - 'right': 62, - 'left': 62, - 'top': 56, - 'bottom': 56 - }, - - // Header HTML template. Available variables: _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_. - 'headerTemplate': null, - - // Footer HTML template. Available variables: _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_. - 'footerTemplate': null - } +// Update a configuration +Configuration.prototype.set = function(key, value) { + return _.set(this.options, key, value); }; +// Default configuration +Configuration.DEFAULT = DEFAULT_CONFIG; + module.exports= Configuration; diff --git a/lib/generator.js b/lib/generator.js index fca5b3c..4e280d8 100644 --- a/lib/generator.js +++ b/lib/generator.js @@ -1,12 +1,12 @@ -var _ = require("lodash"); -var path = require("path"); -var Q = require("q"); -var fs = require("./utils/fs"); +var _ = require('lodash'); +var path = require('path'); +var Q = require('q'); +var fs = require('./utils/fs'); var BaseGenerator = function(book) { this.book = book; - Object.defineProperty(this, "options", { + Object.defineProperty(this, 'options', { get: function () { return this.book.options; } @@ -16,19 +16,19 @@ var BaseGenerator = function(book) { }; BaseGenerator.prototype.callHook = function(name, data) { - return this.book.plugins.hook(name, data); + return this.book.callHook(name, data); }; // Prepare the genertor BaseGenerator.prototype.prepare = function() { var that = this; - return that.callHook("init"); + return that.callHook('init'); }; // Write a parsed file to the output BaseGenerator.prototype.convertFile = function(input) { - return Q.reject(new Error("Could not convert "+input)); + return Q.reject(new Error('Could not convert '+input)); }; // Copy file to the output (non parsable) @@ -51,16 +51,16 @@ BaseGenerator.prototype.copyCover = function() { var that = this; return Q.all([ - fs.copy(that.book.resolve("cover.jpg"), path.join(that.options.output, "cover.jpg")), - fs.copy(that.book.resolve("cover_small.jpg"), path.join(that.options.output, "cover_small.jpg")) + fs.copy(that.book.resolve('cover.jpg'), path.join(that.options.output, 'cover.jpg')), + fs.copy(that.book.resolve('cover_small.jpg'), path.join(that.options.output, 'cover_small.jpg')) ]) .fail(function() { // If orignaly from multi-lang, try copy from parent if (!that.book.isSubBook()) return; return Q.all([ - fs.copy(path.join(that.book.parentRoot(), "cover.jpg"), path.join(that.options.output, "cover.jpg")), - fs.copy(path.join(that.book.parentRoot(), "cover_small.jpg"), path.join(that.options.output, "cover_small.jpg")) + fs.copy(path.join(that.book.parentRoot(), 'cover.jpg'), path.join(that.options.output, 'cover.jpg')), + fs.copy(path.join(that.book.parentRoot(), 'cover_small.jpg'), path.join(that.options.output, 'cover_small.jpg')) ]); }) .fail(function() { @@ -70,7 +70,7 @@ BaseGenerator.prototype.copyCover = function() { // At teh end of the generation BaseGenerator.prototype.finish = function() { - return Q.reject(new Error("Could not finish generation")); + return Q.reject(new Error('Could not finish generation')); }; module.exports = BaseGenerator; diff --git a/lib/plugin.js b/lib/plugin.js index 5e1c427..b4d4aa1 100644 --- a/lib/plugin.js +++ b/lib/plugin.js @@ -1,16 +1,28 @@ -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 resolve = require("resolve"); +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 resolve = require('resolve'); +var mergeDefaults = require('merge-defaults'); +var jsonschema = require('jsonschema'); +var jsonSchemaDefaults = require('json-schema-defaults'); + +var pkg = require('../package.json'); + +var PLUGIN_PREFIX = 'gitbook-plugin-'; + +// Return an absolute name for the plugin (the one on NPM) +function absoluteName(name) { + if (name.indexOf(PLUGIN_PREFIX) === 0) return name; + return [PLUGIN_PREFIX, name].join(''); +} -var pkg = require("../package.json"); var Plugin = function(book, name) { this.book = book; - this.name = name; + this.name = absoluteName(name); this.packageInfos = {}; this.infos = {}; @@ -18,9 +30,8 @@ var Plugin = function(book, name) { _.bindAll(this); _.each([ - "gitbook-plugin-"+name, - "gitbook-"+name, - name, + absoluteName(name), + name ], function(_name) { // Load from the book if (this.load(_name, book.root)) return false; @@ -31,20 +42,27 @@ var Plugin = function(book, name) { }; // Type of plugins resources -Plugin.RESOURCES = ["js", "css"]; +Plugin.RESOURCES = ['js', 'css']; Plugin.HOOKS = [ - "init", "finish", "finish:before", "page", "page:before" + 'init', 'finish', 'finish:before', 'config', 'page', 'page:before' ]; +// Return the reduce name for the plugin +// "gitbook-plugin-test" -> "test" +// Return a relative name for the plugin (the one on GitBook) +Plugin.prototype.reducedName = function() { + return this.name.replace(PLUGIN_PREFIX, ''); +}; + // Load from a name Plugin.prototype.load = function(name, baseDir) { try { - var res = resolve.sync(name+"/package.json", { basedir: baseDir }); + 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; + this.name = this.packageInfos.name; return true; } catch (e) { @@ -62,13 +80,13 @@ Plugin.prototype.normalizeResource = function(resource) { // so we will simply link to using it's URL if (parsed.protocol) { return { - "url": resource + 'url': resource }; } // This will be copied over from disk // and shipped with the book's build - return { "path": this.name+"/"+resource }; + return { 'path': this.name+'/'+resource }; }; // Return resources @@ -77,7 +95,7 @@ Plugin.prototype._getResources = function(base) { var book = this.infos[base]; // Compatibility with version 1.x.x - if (base == "website") book = book || this.infos.book; + if (base == 'website') book = book || this.infos.book; // Nothing specified, fallback to default if (!book) { @@ -85,7 +103,7 @@ Plugin.prototype._getResources = function(base) { } // Dynamic function - if(typeof book === "function") { + if(typeof book === 'function') { // Call giving it the context of our book return Q().then(book.bind(this.book)); } @@ -133,12 +151,43 @@ Plugin.prototype.isValid = function() { // Valid hooks _.each(this.infos.hooks, function(hook, hookName) { if (_.contains(Plugin.HOOKS, hookName)) return; - that.book.log.warn.ln("Hook '"+hookName+" 'used by plugin '"+that.packageInfos.name+"' has been removed or is deprecated"); + that.book.log.warn.ln('Hook "'+hookName+'"" used by plugin "'+that.packageInfos.name+'" has been removed or is deprecated'); }); return isValid; }; +// Normalize, validate configuration for this plugin using its schema +// Throw an error when shcema is not respected +Plugin.prototype.validateConfig = function(config) { + var that = this; + + return Q() + .then(function() { + var schema = that.packageInfos.gitbook || {}; + if (!schema) return config; + + // Normalize schema + schema.id = '/pluginsConfig.'+that.reducedName(); + schema.type = 'object'; + + // Validate and throw if invalid + var v = new jsonschema.Validator(); + var result = v.validate(config, schema, { + propertyName: 'pluginsConfig.'+that.reducedName() + }); + + // Throw error + if (result.errors.length > 0) { + throw new Error('Configuration Error: '+result.errors[0].stack); + } + + // Insert default values + var defaults = jsonSchemaDefaults(schema); + return mergeDefaults(config, defaults); + }); +}; + // Resolve file path Plugin.prototype.resolveFile = function(filename) { return path.resolve(this.baseDir, filename); @@ -154,8 +203,8 @@ Plugin.prototype.callHook = function(name, data) { if (!hookFunc) return Q(data); - this.book.log.debug.ln("call hook", name); - if (!_.contains(Plugin.HOOKS, name)) this.book.log.warn.ln("hook '"+name+"' used by plugin '"+this.name+"' is deprecated, and will be removed in the coming versions"); + this.book.log.debug.ln('call hook', name); + if (!_.contains(Plugin.HOOKS, name)) this.book.log.warn.ln('hook "'+name+'" used by plugin "'+this.name+'" is deprecated, and will be removed in the coming versions'); return Q() .then(function() { @@ -168,7 +217,7 @@ Plugin.prototype.copyAssets = function(out, base) { var that = this; return this.getResources(base) - .get("assets") + .get('assets') .then(function(assets) { // Assets are undefined if(!assets) return false; @@ -180,4 +229,14 @@ Plugin.prototype.copyAssets = function(out, base) { }, _.constant(false)); }; +// Get config from book +Plugin.prototype.getConfig = function() { + return this.book.config.get('pluginsConfig.'+this.reducedName(), {}); +}; + +// Set configuration for this plugin +Plugin.prototype.setConfig = function(values) { + return this.book.config.set('pluginsConfig.'+this.reducedName(), values); +}; + module.exports = Plugin; diff --git a/lib/pluginslist.js b/lib/pluginslist.js index e4594d6..8830950 100644 --- a/lib/pluginslist.js +++ b/lib/pluginslist.js @@ -78,34 +78,46 @@ PluginsList.prototype.load = function(plugin) { that.list.push(plugin); } - // Extract filters - that.book.template.addFilters(plugin.getFilters()); + return Q() - // Extract blocks - that.book.template.addBlocks(plugin.getBlocks()); + // Validate and normalize configuration + .then(function() { + var config = plugin.getConfig(); + return plugin.validateConfig(config); + }) + .then(function(config) { + // Update configuration + plugin.setConfig(config); - return _.reduce(_.keys(that.namespaces), function(prev, namespaceName) { - return prev.then(function() { - return plugin.getResources(namespaceName) - .then(function(plResources) { - var namespace = that.namespaces[namespaceName]; - - // Extract js and css - _.each(Plugin.RESOURCES, function(resourceType) { - namespace.resources[resourceType] = (namespace.resources[resourceType] || []).concat(plResources[resourceType] || []); - }); + // Extract filters + that.book.template.addFilters(plugin.getFilters()); - // Map of html resources by name added by each plugin - _.each(plResources.html || {}, function(value, tag) { - // Turn into function if not one already - if (!_.isFunction(value)) value = _.constant(value); + // Extract blocks + that.book.template.addBlocks(plugin.getBlocks()); - namespace.html[tag] = namespace.html[tag] || []; - namespace.html[tag].push(value); + return _.reduce(_.keys(that.namespaces), function(prev, namespaceName) { + return prev.then(function() { + return plugin.getResources(namespaceName) + .then(function(plResources) { + var namespace = that.namespaces[namespaceName]; + + // Extract js and css + _.each(Plugin.RESOURCES, function(resourceType) { + namespace.resources[resourceType] = (namespace.resources[resourceType] || []).concat(plResources[resourceType] || []); + }); + + // Map of html resources by name added by each plugin + _.each(plResources.html || {}, function(value, tag) { + // Turn into function if not one already + if (!_.isFunction(value)) value = _.constant(value); + + namespace.html[tag] = namespace.html[tag] || []; + namespace.html[tag].push(value); + }); }); }); - }); - }, Q()); + }, Q()); + }); }; // Call a hook |