summaryrefslogtreecommitdiffstats
path: root/lib/config
diff options
context:
space:
mode:
authorSamy Pessé <samypesse@gmail.com>2016-02-26 09:41:26 +0100
committerSamy Pessé <samypesse@gmail.com>2016-02-26 09:41:26 +0100
commitd3d64f636c859f7f01a64f7774cf70bd8ccdc562 (patch)
tree4f7731f37c3a793d187b0ab1cd77680e69534c6c /lib/config
parent4cb9cbb5ae3aa8f9211ffa3ac5e3d34232c0ca4f (diff)
parenteef072693b17526347c37b66078a5059c71caa31 (diff)
downloadgitbook-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.js132
-rw-r--r--lib/config/plugins.js67
-rw-r--r--lib/config/schema.js188
-rw-r--r--lib/config/validator.js28
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
+};