diff options
author | Samy Pessé <samypesse@gmail.com> | 2016-02-26 09:41:26 +0100 |
---|---|---|
committer | Samy Pessé <samypesse@gmail.com> | 2016-02-26 09:41:26 +0100 |
commit | d3d64f636c859f7f01a64f7774cf70bd8ccdc562 (patch) | |
tree | 4f7731f37c3a793d187b0ab1cd77680e69534c6c /lib/config | |
parent | 4cb9cbb5ae3aa8f9211ffa3ac5e3d34232c0ca4f (diff) | |
parent | eef072693b17526347c37b66078a5059c71caa31 (diff) | |
download | gitbook-d3d64f636c859f7f01a64f7774cf70bd8ccdc562.zip gitbook-d3d64f636c859f7f01a64f7774cf70bd8ccdc562.tar.gz gitbook-d3d64f636c859f7f01a64f7774cf70bd8ccdc562.tar.bz2 |
Merge pull request #1109 from GitbookIO/3.0.0
Version 3.0.0
Diffstat (limited to 'lib/config')
-rw-r--r-- | lib/config/index.js | 132 | ||||
-rw-r--r-- | lib/config/plugins.js | 67 | ||||
-rw-r--r-- | lib/config/schema.js | 188 | ||||
-rw-r--r-- | lib/config/validator.js | 28 |
4 files changed, 415 insertions, 0 deletions
diff --git a/lib/config/index.js b/lib/config/index.js new file mode 100644 index 0000000..7f75733 --- /dev/null +++ b/lib/config/index.js @@ -0,0 +1,132 @@ +var _ = require('lodash'); +var semver = require('semver'); + +var gitbook = require('../gitbook'); +var Promise = require('../utils/promise'); +var validator = require('./validator'); +var plugins = require('./plugins'); + +// Config files to tested (sorted) +var CONFIG_FILES = [ + 'book.js', + 'book.json' +]; + +/* +Config is an interface for the book's configuration stored in "book.json" (or "book.js") +*/ + +function Config(book, baseConfig) { + this.book = book; + this.fs = book.fs; + this.log = book.log; + this.path = ''; + + this.baseConfig = baseConfig || {}; + this.replace({}); +} + +// Load configuration of the book +// and verify that the configuration is satisfying +Config.prototype.load = function() { + var that = this; + var isLanguageBook = this.book.isLanguageBook(); + + // Try all potential configuration file + return Promise.some(CONFIG_FILES, function(filename) { + that.log.debug.ln('try loading configuration from', filename); + + return that.fs.loadAsObject(that.book.resolve(filename)) + .then(function(_config) { + that.log.debug.ln('configuration loaded from', filename); + + that.path = filename; + return that.replace(_config); + }) + .fail(function(err) { + if (err.code != 'MODULE_NOT_FOUND') throw(err); + else return Promise(false); + }); + }) + .then(function() { + if (!isLanguageBook) { + if (!gitbook.satisfies(that.options.gitbook)) { + throw new Error('GitBook version doesn\'t satisfy version required by the book: '+that.options.gitbook); + } + if (that.options.gitbook != '*' && !semver.satisfies(semver.inc(gitbook.version, 'patch'), that.options.gitbook)) { + that.log.warn.ln('gitbook version specified in your book.json might be too strict for future patches, \''+(_.first(gitbook.version.split('.'))+'.x.x')+'\' is more adequate'); + } + + that.options.plugins = plugins.toList(that.options.plugins); + } else { + // Multilingual book should inherits the plugins list from parent + that.options.plugins = that.book.parent.config.get('plugins'); + } + + that.options.gitbook = gitbook.version; + }); +}; + +// Replace the whole configuration +Config.prototype.replace = function(options) { + var that = this; + + // Extend base config + options = _.defaults(_.cloneDeep(options), this.baseConfig); + + // Validate the config + this.options = validator.validate(options); + + // options.input == book.root + Object.defineProperty(this.options, 'input', { + get: function () { + return that.book.root; + } + }); + + // options.originalInput == book.parent.root + Object.defineProperty(this.options, 'originalInput', { + get: function () { + return that.book.parent? that.book.parent.root : undefined; + } + }); +}; + +// Return true if book has a configuration file +Config.prototype.exists = function() { + return Boolean(this.path); +}; + +// Return path to a structure file +// Strip the extension by default +Config.prototype.getStructure = function(name, dontStripExt) { + var filename = this.options.structure[name]; + if (dontStripExt) return filename; + + filename = filename.split('.').slice(0, -1).join('.'); + return filename; +}; + +// Return a configuration using a key and a default value +Config.prototype.get = function(key, def) { + return _.get(this.options, key, def); +}; + +// Update a configuration +Config.prototype.set = function(key, value) { + return _.set(this.options, key, value); +}; + +// Return a dump of the configuration +Config.prototype.dump = function() { + return _.cloneDeep(this.options); +}; + +// Return templating context +Config.prototype.getContext = function() { + return { + config: this.book.config.dump() + }; +}; + +module.exports = Config; diff --git a/lib/config/plugins.js b/lib/config/plugins.js new file mode 100644 index 0000000..5d98736 --- /dev/null +++ b/lib/config/plugins.js @@ -0,0 +1,67 @@ +var _ = require('lodash'); + +// Default plugins added to each books +var DEFAULT_PLUGINS = ['highlight', 'search', 'sharing', 'fontsettings', 'theme-default']; + +// Return true if a plugin is a default plugin +function isDefaultPlugin(name, version) { + return _.contains(DEFAULT_PLUGINS, name); +} + +// Normalize a list of plugins to use +function normalizePluginsList(plugins) { + // Normalize list to an array + plugins = _.isString(plugins) ? plugins.split(',') : (plugins || []); + + // Remove empty parts + plugins = _.compact(plugins); + + // Divide as {name, version} to handle format like 'myplugin@1.0.0' + plugins = _.map(plugins, function(plugin) { + if (plugin.name) return plugin; + + var parts = plugin.split('@'); + var name = parts[0]; + var version = parts[1]; + return { + 'name': name, + 'version': version // 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 + _.each(DEFAULT_PLUGINS, function(plugin) { + if (_.find(plugins, { name: plugin })) { + return; + } + + plugins.push({ + 'name': plugin + }); + }); + // Remove plugin that start with '-' + plugins = _.filter(plugins, function(plugin) { + return !_.contains(toremove, plugin.name) && !(plugin.name.length > 0 && plugin.name[0] == '-'); + }); + + // Remove duplicates + plugins = _.uniq(plugins, 'name'); + + return plugins; +} + +module.exports = { + isDefaultPlugin: isDefaultPlugin, + toList: normalizePluginsList +}; + diff --git a/lib/config/schema.js b/lib/config/schema.js new file mode 100644 index 0000000..34a6c76 --- /dev/null +++ b/lib/config/schema.js @@ -0,0 +1,188 @@ +module.exports = { + '$schema': 'http://json-schema.org/schema#', + 'id': 'https://gitbook.com/schemas/book.json', + 'title': 'GitBook Configuration', + 'type': 'object', + 'properties': { + 'title': { + 'type': 'string', + 'title': 'Title of the book, default is extracted from README' + }, + 'title': { + 'type': 'string', + 'title': 'Description of the book, default is extracted from README' + }, + 'isbn': { + 'type': 'string', + 'title': 'ISBN for published book' + }, + 'author': { + 'type': 'string', + 'title': 'Name of the author' + }, + 'gitbook': { + 'type': 'string', + 'default': '*', + 'title': 'GitBook version to match' + }, + 'direction': { + 'type': 'string', + 'enum': ['ltr', 'rtl'], + 'title': 'Direction of texts, default is detected in the pages' + }, + 'theme': { + 'type': 'string', + 'default': 'default', + 'title': 'Name of the theme plugin to use' + }, + 'variables': { + 'type': 'object', + 'title': 'Templating context variables' + }, + 'plugins': { + 'oneOf': [ + { '$ref': '#/definitions/pluginsArray' }, + { '$ref': '#/definitions/pluginsString' } + ], + 'default': [] + }, + 'pluginsConfig': { + 'type': 'object', + 'title': 'Configuration for plugins' + }, + 'structure': { + 'type': 'object', + 'properties': { + 'langs': { + 'default': 'LANGS.md', + 'type': 'string', + 'title': 'File to use as languages index', + 'pattern': '^[0-9a-zA-Z ... ]+$' + }, + 'readme': { + 'default': 'README.md', + 'type': 'string', + 'title': 'File to use as preface', + 'pattern': '^[0-9a-zA-Z ... ]+$' + }, + 'glossary': { + 'default': 'GLOSSARY.md', + 'type': 'string', + 'title': 'File to use as glossary index', + 'pattern': '^[0-9a-zA-Z ... ]+$' + }, + 'summary': { + 'default': 'SUMMARY.md', + 'type': 'string', + 'title': 'File to use as table of contents', + 'pattern': '^[0-9a-zA-Z ... ]+$' + } + }, + 'additionalProperties': false + }, + 'pdf': { + 'type': 'object', + 'title': 'PDF specific configurations', + 'properties': { + 'pageNumbers': { + 'type': 'boolean', + 'default': true, + 'title': 'Add page numbers to the bottom of every page' + }, + 'fontSize': { + 'type': 'integer', + 'minimum': 8, + 'maximum': 30, + 'default': 12, + 'title': 'Font size for the PDF output' + }, + 'fontFamily': { + 'type': 'string', + 'default': 'Arial', + 'title': 'Font family for the PDF output' + }, + 'paperSize': { + 'type': 'string', + 'enum': ['a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'b0', 'b1', 'b2', 'b3', 'b4', 'b5', 'b6', 'legal', 'letter'], + 'default': 'a4', + 'title': 'Paper size for the PDF' + }, + 'chapterMark': { + 'type': 'string', + 'enum': ['pagebreak', 'rule', 'both', 'none'], + 'default': 'pagebreak', + 'title': 'How to mark detected chapters' + }, + 'pageBreaksBefore': { + 'type': 'string', + 'default': '/', + 'title': 'An XPath expression. Page breaks are inserted before the specified elements. To disable use the expression: "/"' + }, + 'margin': { + 'type': 'object', + 'properties': { + 'right': { + 'type': 'integer', + 'title': 'Right Margin', + 'minimum': 0, + 'maximum': 100, + 'default': 62 + }, + 'left': { + 'type': 'integer', + 'title': 'Left Margin', + 'minimum': 0, + 'maximum': 100, + 'default': 62 + }, + 'top': { + 'type': 'integer', + 'title': 'Top Margin', + 'minimum': 0, + 'maximum': 100, + 'default': 56 + }, + 'bottom': { + 'type': 'integer', + 'title': 'Bottom Margin', + 'minimum': 0, + 'maximum': 100, + 'default': 56 + } + } + } + } + } + }, + 'required': [], + 'definitions': { + 'pluginsArray': { + 'type': 'array', + 'items': { + 'oneOf': [ + { '$ref': '#/definitions/pluginObject' }, + { '$ref': '#/definitions/pluginString' } + ] + } + }, + 'pluginsString': { + 'type': 'string' + }, + 'pluginString': { + 'type': 'string' + }, + 'pluginObject': { + 'type': 'object', + 'properties': { + 'name': { + 'type': 'string' + }, + 'version': { + 'type': 'string' + } + }, + 'additionalProperties': false, + 'required': ['name'] + } + } +}; diff --git a/lib/config/validator.js b/lib/config/validator.js new file mode 100644 index 0000000..764b19a --- /dev/null +++ b/lib/config/validator.js @@ -0,0 +1,28 @@ +var jsonschema = require('jsonschema'); +var jsonSchemaDefaults = require('json-schema-defaults'); +var mergeDefaults = require('merge-defaults'); + +var schema = require('./schema'); +var error = require('../utils/error'); + +// Validate a book.json content +// And return a mix with the default value +function validate(bookJson) { + var v = new jsonschema.Validator(); + var result = v.validate(bookJson, schema, { + propertyName: 'config' + }); + + // Throw error + if (result.errors.length > 0) { + throw new error.ConfigurationError(new Error(result.errors[0].stack)); + } + + // Insert default values + var defaults = jsonSchemaDefaults(schema); + return mergeDefaults(bookJson, defaults); +} + +module.exports = { + validate: validate +}; |