mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-24 11:26:31 +00:00
Merge pull request #1181 from GitbookIO/feature/allplugins_tpl
All plugins can extend templates/theme
This commit is contained in:
Vendored
+1
-3
@@ -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 |
|
||||
| -------- | ----------- |
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
var _ = require('lodash');
|
||||
var conrefsLoader = require('./conrefs');
|
||||
|
||||
var JSONOutput = conrefsLoader();
|
||||
|
||||
+12
-49
@@ -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,
|
||||
var searchPaths = _.pluck(that.plugins.list(), 'root');
|
||||
|
||||
// Installed plugin (it can be identical to themeDefault.root)
|
||||
that.theme.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: {
|
||||
|
||||
+41
-13
@@ -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');
|
||||
|
||||
this.log.info.ln('loading', plugins.length, 'plugins');
|
||||
return this.load(plugins);
|
||||
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)
|
||||
);
|
||||
});
|
||||
|
||||
// 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
|
||||
|
||||
+11
-22
@@ -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)) {
|
||||
|
||||
+52
-2
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
+2
-1
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+10
-6
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user