summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/themes/README.md4
-rw-r--r--lib/output/json.js1
-rw-r--r--lib/output/website.js61
-rw-r--r--lib/plugins/index.js54
-rw-r--r--lib/plugins/plugin.js33
-rw-r--r--lib/plugins/registry.js54
-rw-r--r--lib/template/fs-loader.js61
-rw-r--r--package.json3
-rw-r--r--test/output-website.js25
-rw-r--r--test/plugins.js16
10 files changed, 215 insertions, 97 deletions
diff --git a/docs/themes/README.md b/docs/themes/README.md
index c013b57..59e684c 100644
--- a/docs/themes/README.md
+++ b/docs/themes/README.md
@@ -2,13 +2,11 @@
Since version 3.0.0, GitBook can be easily themed. Books are using by default the [theme-default](https://github.com/GitbookIO/theme-default).
-The theme to use is specified in the [book's configuration](../config.md) using key `theme`.
-
> **Caution**: Custom theming can block some plugins from working correctly.
### Structure of a theme
-A theme is a folder containing templates and assets. All the templates are optionnal, since theme are always extending the default theme.
+A theme is a plugin containing templates and assets. All the templates are optionnal, since theme are always extending the default theme.
| Folder | Description |
| -------- | ----------- |
diff --git a/lib/output/json.js b/lib/output/json.js
index b66e593..7061141 100644
--- a/lib/output/json.js
+++ b/lib/output/json.js
@@ -1,4 +1,3 @@
-var _ = require('lodash');
var conrefsLoader = require('./conrefs');
var JSONOutput = conrefsLoader();
diff --git a/lib/output/website.js b/lib/output/website.js
index e298b69..43f732c 100644
--- a/lib/output/website.js
+++ b/lib/output/website.js
@@ -8,13 +8,10 @@ var Promise = require('../utils/promise');
var location = require('../utils/location');
var fs = require('../utils/fs');
var defaultFilters = require('../template/filters');
+var FSLoader = require('../template/fs-loader');
var conrefsLoader = require('./conrefs');
var Output = require('./base');
-// Tranform a theme ID into a plugin
-function themeID(plugin) {
- return 'theme-' + plugin;
-}
// Directory for a theme with the templates
function templatesPath(dir) {
@@ -57,32 +54,11 @@ WebsiteOutput.prototype.prepare = function() {
})
.then(function() {
- var themeName = that.book.config.get('theme');
- that.theme = that.plugins.get(themeID(themeName));
- that.themeDefault = that.plugins.get(themeID('default'));
-
- if (!that.theme) {
- throw new Error('Theme "' + themeName + '" is not installed, add "' + themeID(themeName) + '" to your "book.json"');
- }
-
- if (that.themeDefault.root != that.theme.root) {
- that.log.info.ln('build using theme "' + themeName + '"');
- }
-
// This list is ordered to give priority to templates in the book
- var searchPaths = _.chain([
- // The book itself can contains a "_layouts" folder
- that.book.root,
-
- // Installed plugin (it can be identical to themeDefault.root)
- that.theme.root,
+ var searchPaths = _.pluck(that.plugins.list(), 'root');
- // Is default theme still installed
- that.themeDefault? that.themeDefault.root : null
- ])
- .compact()
- .uniq()
- .value();
+ // The book itself can contains a "_layouts" folder
+ searchPaths.unshift(that.book.root);
// Load i18n
_.each(searchPaths.concat().reverse(), function(searchPath) {
@@ -92,7 +68,7 @@ WebsiteOutput.prototype.prepare = function() {
that.i18n.load(i18nRoot);
});
- that.env = new nunjucks.Environment(new nunjucks.FileSystemLoader(_.map(searchPaths, templatesPath)));
+ that.env = new nunjucks.Environment(new FSLoader(_.map(searchPaths, templatesPath)));
// Add GitBook default filters
_.each(defaultFilters, function(fn, filter) {
@@ -142,21 +118,11 @@ WebsiteOutput.prototype.prepare = function() {
.then(function() {
if (that.book.isLanguageBook()) return;
- return Promise.serie([
- // Assets from the book are already copied
- // The order is reversed from the template's one
-
- // Is default theme still installed
- that.themeDefault && that.themeDefault.root != that.theme.root?
- that.themeDefault.root : null,
-
- // Installed plugin (it can be identical to themeDefault.root)
- that.theme.root
- ], function(folder) {
- if (!folder) return;
-
+ // Assets from the book are already copied
+ // Copy assets from plugins
+ return Promise.serie(that.plugins.list(), function(plugin) {
// Copy assets only if exists (don't fail otherwise)
- var assetFolder = path.join(folder, '_assets', that.name);
+ var assetFolder = path.join(plugin.root, '_assets', that.name);
if (!fs.existsSync(assetFolder)) return;
that.log.debug.ln('copy assets from theme', assetFolder);
@@ -164,7 +130,7 @@ WebsiteOutput.prototype.prepare = function() {
assetFolder,
that.resolve('gitbook'),
{
- deleteFirst: false, // Delete "to" before
+ deleteFirst: false,
overwrite: true,
confirm: true
}
@@ -243,13 +209,10 @@ WebsiteOutput.prototype.outputMultilingualIndex = function() {
// Templates are stored in `_layouts` folders
WebsiteOutput.prototype.render = function(tpl, context) {
var filename = this.templateName(tpl);
+
context = _.extend(context, {
template: {
- // Same template but in the default theme
- default: this.themeDefault? path.resolve(templatesPath(this.themeDefault.root), filename) : null,
-
- // Same template but in the theme
- theme: path.resolve(templatesPath(this.theme.root), filename)
+ self: filename
},
plugins: {
diff --git a/lib/plugins/index.js b/lib/plugins/index.js
index 8280542..f897d9c 100644
--- a/lib/plugins/index.js
+++ b/lib/plugins/index.js
@@ -21,6 +21,11 @@ function PluginsManager(book) {
_.bindAll(this);
}
+// Returns the list of plugins
+PluginsManager.prototype.list = function() {
+ return this.plugins;
+};
+
// Return count of plugins loaded
PluginsManager.prototype.count = function() {
return _.size(this.plugins);
@@ -33,24 +38,21 @@ PluginsManager.prototype.get = function(name) {
});
};
-// Load a plugin, or a list of plugins
-PluginsManager.prototype.load = function(name) {
+// Load a plugin (could be a BookPlugin or {name,path})
+PluginsManager.prototype.load = function(plugin) {
var that = this;
- if (_.isArray(name)) {
- return Promise.serie(name, function(_name) {
- return that.load(_name);
- });
+ if (_.isArray(plugin)) {
+ return Promise.serie(plugin, that.load);
}
return Promise()
// Initiate and load the plugin
.then(function() {
- var plugin;
-
- if (!_.isString(name)) plugin = name;
- else plugin = new BookPlugin(that.book, name);
+ if (!(plugin instanceof BookPlugin)) {
+ plugin = new BookPlugin(that.book, plugin.name, plugin.path);
+ }
if (that.get(plugin.id)) {
throw new Error('Plugin "'+plugin.id+'" is already loaded');
@@ -68,10 +70,36 @@ PluginsManager.prototype.load = function(name) {
// Load all plugins from the book's configuration
PluginsManager.prototype.loadAll = function() {
- var plugins = _.pluck(this.book.config.get('plugins'), 'name');
+ var that = this;
+ var pluginNames = _.pluck(this.book.config.get('plugins'), 'name');
+
+ return registry.list(this.book)
+ .then(function(plugins) {
+ // Filter out plugins not listed of first level
+ // (aka pre-installed plugins)
+ plugins = _.filter(plugins, function(plugin) {
+ return (
+ plugin.depth > 1 ||
+ _.contains(pluginNames, plugin.name)
+ );
+ });
- this.log.info.ln('loading', plugins.length, 'plugins');
- return this.load(plugins);
+ // Log state
+ that.log.info.ln(_.size(plugins) + ' are installed');
+ if (_.size(pluginNames) != _.size(plugins)) that.log.info.ln(_.size(pluginNames) + ' explicitly listed');
+
+ // Verify that all plugins are present
+ var notInstalled = _.filter(pluginNames, function(name) {
+ return !_.find(plugins, { name: name });
+ });
+
+ if (_.size(notInstalled) > 0) {
+ throw new Error('Couldn\'t locate plugins "' + notInstalled.join(', ') + '", Run \'gitbook install\' to install plugins from registry.');
+ }
+
+ // Load plugins
+ return that.load(plugins);
+ });
};
// Setup a plugin
diff --git a/lib/plugins/plugin.js b/lib/plugins/plugin.js
index d707e5c..d1c00d8 100644
--- a/lib/plugins/plugin.js
+++ b/lib/plugins/plugin.js
@@ -24,13 +24,14 @@ function isModuleNotFound(err) {
return err.message.indexOf('Cannot find module') >= 0;
}
-function BookPlugin(book, pluginId) {
+function BookPlugin(book, pluginId, pluginFolder) {
this.book = book;
this.log = this.book.log.prefix(pluginId);
+
this.id = pluginId;
this.npmId = registry.npmId(pluginId);
- this.root;
+ this.root = pluginFolder;
this.packageInfos = undefined;
this.content = undefined;
@@ -51,8 +52,7 @@ BookPlugin.prototype.bind = function(fn) {
return fn.bind(compatibility.pluginCtx(this));
};
-// Load this plugin
-// An optional folder to search in can be passed
+// Load this plugin from its root folder
BookPlugin.prototype.load = function(folder) {
var that = this;
@@ -60,18 +60,12 @@ BookPlugin.prototype.load = function(folder) {
return Promise.reject(new Error('Plugin "' + this.id + '" is already loaded'));
}
- // Fodlers to search plugins in
- var searchPaths = _.compact([
- folder,
- this.book.resolve('node_modules'),
- __dirname
- ]);
-
// Try loading plugins from different location
- var p = Promise.some(searchPaths, function(baseDir) {
+ var p = Promise()
+ .then(function() {
// Locate plugin and load pacjage.json
try {
- var res = resolve.sync(that.npmId + '/package.json', { basedir: baseDir });
+ var res = resolve.sync('./package.json', { basedir: that.root });
that.root = path.dirname(res);
that.packageInfos = require(res);
@@ -81,12 +75,12 @@ BookPlugin.prototype.load = function(folder) {
that.packageInfos = undefined;
that.content = undefined;
- return false;
+ return;
}
// Load plugin JS content
try {
- that.content = require(resolve.sync(that.npmId, { basedir: baseDir }));
+ that.content = require(that.root);
} catch(err) {
// It's no big deal if the plugin doesn't have an "index.js"
// (For example: themes)
@@ -98,8 +92,6 @@ BookPlugin.prototype.load = function(folder) {
});
}
}
-
- return true;
})
.then(that.validate)
@@ -122,18 +114,15 @@ BookPlugin.prototype.load = function(folder) {
// This method throws erros if plugin is invalid
BookPlugin.prototype.validate = function() {
var isValid = (
+ this.isLoaded() &&
this.packageInfos &&
this.packageInfos.name &&
this.packageInfos.engines &&
this.packageInfos.engines.gitbook
);
- if (!this.isLoaded()) {
- throw new Error('Couldn\'t locate plugin "' + this.id + '", Run \'gitbook install\' to install plugins from registry.');
- }
-
if (!isValid) {
- throw new Error('Invalid plugin "' + this.id + '"');
+ throw new Error('Error loading plugin "' + this.id + '" at "' + this.root + '"');
}
if (!gitbook.satisfies(this.packageInfos.engines.gitbook)) {
diff --git a/lib/plugins/registry.js b/lib/plugins/registry.js
index abb8215..ea172c4 100644
--- a/lib/plugins/registry.js
+++ b/lib/plugins/registry.js
@@ -1,7 +1,9 @@
var npm = require('npm');
var npmi = require('npmi');
+var path = require('path');
var semver = require('semver');
var _ = require('lodash');
+var readInstalled = require('read-installed');
var Promise = require('../utils/promise');
var gitbook = require('../gitbook');
@@ -21,7 +23,7 @@ function pluginId(name) {
// Validate an NPM plugin ID
function validateId(name) {
- return name.indexOf(PLUGIN_PREFIX) === 0;
+ return name && name.indexOf(PLUGIN_PREFIX) === 0;
}
// Initialize NPM for operations
@@ -104,6 +106,52 @@ function installPlugin(book, plugin, version) {
});
}
+// List all packages installed inside a folder
+// Returns an ordered list of plugins
+function listInstalled(folder) {
+ var options = {
+ dev: false,
+ log: function() {},
+ depth: 4
+ };
+ var results = [];
+
+ function onPackage(pkg, isRoot) {
+ if (!validateId(pkg.name)){
+ if (!isRoot) return;
+ } else {
+ results.push({
+ name: pluginId(pkg.name),
+ version: pkg.version,
+ path: pkg.realPath,
+ depth: pkg.depth
+ });
+ }
+
+ _.each(pkg.dependencies, function(dep) {
+ onPackage(dep);
+ });
+ }
+
+ return Promise.nfcall(readInstalled, folder, options)
+ .then(function(data) {
+ onPackage(data, true);
+ return _.uniq(results, 'name');
+ });
+}
+
+// List installed plugins for a book (defaults and installed)
+function listPlugins(book) {
+ return Promise.all([
+ listInstalled(path.resolve(__dirname, '../..')),
+ listInstalled(book.root)
+ ])
+ .spread(function(defaultPlugins, plugins) {
+ var results = plugins.concat(defaultPlugins);
+ return _.uniq(results, 'name');
+ });
+}
+
module.exports = {
npmId: npmId,
pluginId: pluginId,
@@ -111,5 +159,7 @@ module.exports = {
resolve: resolveVersion,
link: linkPlugin,
- install: installPlugin
+ install: installPlugin,
+ list: listPlugins,
+ listInstalled: listInstalled
};
diff --git a/lib/template/fs-loader.js b/lib/template/fs-loader.js
new file mode 100644
index 0000000..00c4743
--- /dev/null
+++ b/lib/template/fs-loader.js
@@ -0,0 +1,61 @@
+var _ = require('lodash');
+var fs = require('fs');
+var path = require('path');
+var nunjucks = require('nunjucks');
+
+/*
+ Nunjucks loader similar to FileSystemLoader, but avoid infinite looping
+*/
+
+function isRelative(filename) {
+ return (filename.indexOf('./') === 0 || filename.indexOf('../') === 0);
+}
+
+var Loader = nunjucks.Loader.extend({
+ init: function(searchPaths) {
+ this.searchPaths = searchPaths.map(path.normalize);
+ },
+
+ getSource: function(fullpath) {
+ if (!fullpath) return null;
+
+ fullpath = this.resolve(null, fullpath);
+
+ if(!fullpath) {
+ return null;
+ }
+
+ return {
+ src: fs.readFileSync(fullpath, 'utf-8'),
+ path: fullpath,
+ noCache: true
+ };
+ },
+
+ // We handle absolute paths ourselves in ".resolve"
+ isRelative: function() {
+ return true;
+ },
+
+ resolve: function(from, to) {
+ // Relative template like "./test.html"
+ if (isRelative(to) && from) {
+ return path.resolve(path.dirname(from), to);
+ }
+
+ // Absolute template to resolve in root folder
+ var resultFolder = _.find(this.searchPaths, function(basePath) {
+ var p = path.resolve(basePath, to);
+
+ return (
+ p.indexOf(basePath) === 0
+ && p != from
+ && fs.existsSync(p)
+ );
+ });
+ if (!resultFolder) return null;
+ return path.resolve(resultFolder, to);
+ }
+});
+
+module.exports = Loader;
diff --git a/package.json b/package.json
index bf24b55..2f11e51 100644
--- a/package.json
+++ b/package.json
@@ -40,9 +40,10 @@
"moment": "2.11.2",
"npm": "3.7.5",
"npmi": "1.0.1",
- "nunjucks": "2.3.0",
+ "nunjucks": "2.4.0",
"nunjucks-autoescape": "1.0.1",
"q": "1.4.1",
+ "read-installed": "^4.0.3",
"request": "2.69.0",
"resolve": "0.6.3",
"rmdir": "1.2.0",
diff --git a/test/output-website.js b/test/output-website.js
index 19459b3..2d936be 100644
--- a/test/output-website.js
+++ b/test/output-website.js
@@ -1,3 +1,5 @@
+var fs = require('fs');
+
var mock = require('./mock');
var WebsiteOutput = require('../lib/output/website');
@@ -95,5 +97,28 @@ describe('Website Output', function() {
});
});
+ describe('Theming', function() {
+ var output;
+
+ before(function() {
+ return mock.outputDefaultBook(WebsiteOutput, {
+ '_layouts/website/page.html': '{% extends "website/page.html" %}{% block body %}{{ super() }}<div id="theming-added"></div>{% endblock %}'
+
+ })
+ .then(function(_output) {
+ output = _output;
+ });
+ });
+
+ it('should extend default theme', function() {
+ var readme = fs.readFileSync(output.resolve('index.html'), 'utf-8');
+
+ readme.should.be.html({
+ '#theming-added': {
+ count: 1
+ }
+ });
+ });
+ });
});
diff --git a/test/plugins.js b/test/plugins.js
index 5d10031..399cdc5 100644
--- a/test/plugins.js
+++ b/test/plugins.js
@@ -9,6 +9,10 @@ var BookPlugin = require('../lib/plugins/plugin');
var PLUGINS_ROOT = path.resolve(__dirname, 'node_modules');
+function TestPlugin(book, name) {
+ return new BookPlugin(book, name, path.resolve(PLUGINS_ROOT, 'gitbook-plugin-'+name));
+}
+
describe('Plugins', function() {
var book;
@@ -90,7 +94,7 @@ describe('Plugins', function() {
describe('Configuration', function() {
it('should fail loading a plugin with an invalid configuration', function() {
- var plugin = new BookPlugin(book, 'test-config');
+ var plugin = TestPlugin(book, 'test-config');
return plugin.load(PLUGINS_ROOT)
.should.be.rejectedWith('Error with book\'s configuration: pluginsConfig.test-config.myProperty is required');
});
@@ -108,7 +112,7 @@ describe('Plugins', function() {
.then(function(book2) {
return book2.prepareConfig()
.then(function() {
- var plugin = new BookPlugin(book2, 'test-config');
+ var plugin = TestPlugin(book2, 'test-config');
return plugin.load(PLUGINS_ROOT);
})
.then(function() {
@@ -122,7 +126,7 @@ describe('Plugins', function() {
var plugin;
before(function() {
- plugin = new BookPlugin(book, 'test-resources');
+ plugin = TestPlugin(book, 'test-resources');
return plugin.load(PLUGINS_ROOT);
});
@@ -146,7 +150,7 @@ describe('Plugins', function() {
var plugin, filters;
before(function() {
- plugin = new BookPlugin(book, 'test-filters');
+ plugin = TestPlugin(book, 'test-filters');
return plugin.load(PLUGINS_ROOT)
.then(function() {
@@ -171,7 +175,7 @@ describe('Plugins', function() {
var plugin, blocks;
before(function() {
- plugin = new BookPlugin(book, 'test-blocks');
+ plugin = TestPlugin(book, 'test-blocks');
return plugin.load(PLUGINS_ROOT)
.then(function() {
@@ -196,7 +200,7 @@ describe('Plugins', function() {
var plugin;
before(function() {
- plugin = new BookPlugin(book, 'test-hooks');
+ plugin = TestPlugin(book, 'test-hooks');
return plugin.load(PLUGINS_ROOT);
});