Compare commits

...

14 Commits

Author SHA1 Message Date
Samy Pesse fb862f466f Bump version to 3.0.0-pre.9 2016-04-19 14:18:01 +02:00
Samy Pesse 8d51300386 Bump version to 2.1.0 2016-04-19 14:16:51 +02:00
Samy Pesse c4b413563d Update theme-default@1.0.0-pre.8 2016-04-19 14:14:59 +02:00
Samy Pesse 8f852971f9 Cleanup book.js 2016-04-19 14:12:01 +02:00
Johan Preynat 3c54c89464 Use page.content if modified or fallback to page.sections in plugins hooks 2016-04-19 10:02:41 +02:00
Samy Pessé d07aa13822 Add tests for page hook 2016-04-18 17:14:55 +02:00
Samy Pessé c218f7d0e3 Correctly set template.self in all cases 2016-04-18 16:56:50 +02:00
Samy Pessé 72ad872e90 Fix js error when using output without root 2016-04-18 14:26:24 +02:00
Samy Pessé 854f244d22 Fix infinite loop on this.options 2016-04-18 14:17:51 +02:00
Samy Pessé 9acbb223db Add generator and output property as deprecated in config 2016-04-18 14:16:22 +02:00
Johan Preynat c59fc687db Ensure plugins compatibility with page.sections 2016-04-18 14:10:19 +02:00
Samy Pessé 0791b917f1 Add context for readme 2016-04-18 12:49:31 +02:00
Samy Pessé ba182ce8e4 Fix tests for copy of website theme 2016-04-15 11:33:38 +02:00
Samy Pessé 663291c515 Output context as global for themes 2016-04-15 11:33:24 +02:00
22 changed files with 414 additions and 173 deletions
+2
View File
@@ -19,6 +19,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Fix error in code highlighting for unknown languages
- Accept SSH url as plugin version
- Add templating blocks `markdown`, `asciidoc` and `markup`
- Better search experience
- Better default theme, more responsive and cleaner
## 2.6.7
- Fix bug with filenames including spaces
+1 -2
View File
@@ -6,11 +6,10 @@ module.exports = {
title: 'GitBook Toolchain Documentation',
// Enforce use of GitBook v3
gitbook: pkg.version,
gitbook: '>=3.0.0-pre.0',
// Use the "official" theme
plugins: ['theme-official', 'sitemap'],
theme: 'official',
variables: {
version: pkg.version
+13
View File
@@ -10,6 +10,8 @@ The following is a reference of the available data during book's parsing and the
| `gitbook` | GitBook specific information |
| `page` | Current page specific information |
| `file` | File associated with the current page specific information |
| `readme` | Information about the Readme |
| `glossary` | Information about the Glossary |
| `summary` | Information about the table of contents |
| `languages` | List of languages for multi-lingual books |
| `output` | Information about the output generator |
@@ -71,3 +73,14 @@ Languages are defined by `{ id: 'en', title: 'English' }`.
| `output.name` | Name of the output generator, possible values are `website`, `json`, `ebook` |
| `output.format` | When `output.name == "ebook"`, `format` defines the ebook format that will be generated, possible values are `pdf`, `epub` or `mobi` |
### Readme Variables
| Variable | Description |
| -------- | ----------- |
| `readme.path` | Path to the Readme in the book |
### Glossary Variables
| Variable | Description |
| -------- | ----------- |
| `glossary.path` | Path to the Glossary in the book |
+19 -1
View File
@@ -11,7 +11,25 @@ util.inherits(Readme, BackboneFile);
Readme.prototype.type = 'readme';
// Parse the readme content
/*
Return and extension of context to define the readme
@retrun {Object}
*/
Readme.prototype.getContext = function() {
return {
readme: {
path: this.path
}
};
};
/*
Parse the readme content
@param {String} content
@retrun {Promise}
*/
Readme.prototype.parse = function(content) {
var that = this;
+1
View File
@@ -231,6 +231,7 @@ Summary.prototype.getContext = function() {
return {
summary: {
path: this.path,
parts: _.map(this.parts, function(part) {
return part.getContext(onArticle);
})
+1 -10
View File
@@ -97,16 +97,7 @@ function Book(opts) {
Object.defineProperty(this, 'options', {
get: function () {
this.log.warn.ln('"options" property is deprecated, use config.get(key) instead');
var cfg = this.config.dump();
error.deprecateField(cfg, 'book', (this.output? this.output.name : null), '"options.generator" property is deprecated, use "output.name" instead');
// options.generator
cfg.generator = this.output? this.output.name : null;
// options.output
cfg.output = this.output? this.output.root() : null;
return cfg;
return this.config.options;
}
});
+6 -1
View File
@@ -3,6 +3,7 @@ var semver = require('semver');
var gitbook = require('../gitbook');
var Promise = require('../utils/promise');
var error = require('../utils/error');
var validator = require('./validator');
var plugins = require('./plugins');
@@ -90,6 +91,9 @@ Config.prototype.replace = function(options) {
return that.book.parent? that.book.parent.root : undefined;
}
});
error.deprecateField(this.options, 'generator', (this.book.output? this.book.output.name : null), '"options.generator" property is deprecated, use "output.name" instead');
error.deprecateField(this.options, 'output', (this.book.output && this.book.output.root? this.book.output.root() : null), '"options.output" property is deprecated, use "output.root()" instead');
};
// Return true if book has a configuration file
@@ -119,7 +123,8 @@ Config.prototype.set = function(key, value) {
// Return a dump of the configuration
Config.prototype.dump = function() {
return _.cloneDeep(this.options);
var opts = _.omit(this.options, 'generator', 'output');
return _.cloneDeep(opts);
};
// Return templating context
+1
View File
@@ -248,6 +248,7 @@ Output.prototype.getContext = function() {
},
this.book.getContext(),
(this.book.isLanguageBook()? this.book.parent: this.book).langs.getContext(),
this.book.readme.getContext(),
this.book.summary.getContext(),
this.book.glossary.getContext(),
this.book.config.getContext(),
+2 -8
View File
@@ -44,13 +44,7 @@ EbookOutput.prototype.finish = function() {
// Generate SUMMARY.html
.then(function() {
return that.render('summary', that.getContext())
.then(function(html) {
return that.writeFile(
'SUMMARY.html',
html
);
});
return that.render('summary', 'SUMMARY.html', that.getContext());
})
// Start ebook-convert
@@ -100,7 +94,7 @@ EbookOutput.prototype.getPDFTemplate = function(tpl) {
this.getContext()
);
return this.render('pdf_'+tpl, context)
return this.renderAsString('pdf_'+tpl, context)
// Inline css, include css relative to the output folder
.then(function(output) {
@@ -1,22 +1,14 @@
var _ = require('lodash');
var path = require('path');
var util = require('util');
var nunjucks = require('nunjucks');
var I18n = require('i18n-t');
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');
// Directory for a theme with the templates
function templatesPath(dir) {
return path.join(dir, '_layouts');
}
var Promise = require('../../utils/promise');
var location = require('../../utils/location');
var fs = require('../../utils/fs');
var conrefsLoader = require('../conrefs');
var Output = require('../base');
var setupTemplateEnv = require('./templateEnv');
function _WebsiteOutput() {
Output.apply(this, arguments);
@@ -68,58 +60,7 @@ WebsiteOutput.prototype.prepare = function() {
that.i18n.load(i18nRoot);
});
that.env = new nunjucks.Environment(new FSLoader(_.map(searchPaths, templatesPath)));
// Add GitBook default filters
_.each(defaultFilters, function(fn, filter) {
that.env.addFilter(filter, fn);
});
// Translate using _i18n locales
that.env.addFilter('t', function(s) {
return that.i18n.t(that.book.config.get('language'), s);
});
// Transform an absolute path into a relative path
// using this.ctx.page.path
that.env.addFilter('resolveFile', function(href) {
return location.normalize(that.resolveForPage(this.ctx.file.path, href));
});
// Test if a file exists
that.env.addFilter('fileExists', function(href) {
return fs.existsSync(that.resolve(href));
});
// Transform a '.md' into a '.html' (README -> index)
that.env.addFilter('contentURL', function(s) {
return that.toURL(s);
});
// Get an article using its path
that.env.addFilter('getArticleByPath', function(s) {
var article = that.book.summary.getArticle(s);
if (!article) return undefined;
return article.getContext();
});
// Relase path to an asset
that.env.addFilter('resolveAsset', function(href) {
href = path.join('gitbook', href);
// Resolve for current file
if (this.ctx.file) {
href = that.resolveForPage(this.ctx.file.path, '/' + href);
}
// Use assets from parent
if (that.book.isLanguageBook()) {
href = path.join('../', href);
}
return location.normalize(href);
});
that.searchPaths = searchPaths;
})
// Copy assets from themes before copying files from book
@@ -164,15 +105,7 @@ WebsiteOutput.prototype.onPage = function(page) {
// Render the page template with the same context as the json output
.then(function() {
return that.render('page', page.getOutputContext(that));
})
// Write the HTML file
.then(function(html) {
return that.writeFile(
that.outputPath(page.path),
html
);
return that.render('page', that.outputPath(page.path), page.getOutputContext(that));
});
};
@@ -204,38 +137,24 @@ WebsiteOutput.prototype.finish = function() {
WebsiteOutput.prototype.outputMultilingualIndex = function() {
var that = this;
return that.render('languages', that.getContext())
.then(function(html) {
return that.writeFile(
'index.html',
html
);
});
return that.render('languages', 'index.html', that.getContext());
};
// Render a template using nunjucks
// Templates are stored in `_layouts` folders
WebsiteOutput.prototype.render = function(tpl, context) {
var that = this;
/*
Render a template as an HTML string
Templates are stored in `_layouts` folders
@param {String} tpl: template name (ex: "page")
@param {String} outputFile: filename to write, relative to the output
@param {Object} context: context for the page
@return {Promise}
*/
WebsiteOutput.prototype.renderAsString = function(tpl, context) {
// Calcul template name
var filename = this.templateName(tpl);
context = _.extend(context, {
template: {
self: filename,
getJSContext: function() {
return {
page: _.omit(context.page, 'content'),
config: context.config,
file: context.file,
gitbook: context.gitbook,
basePath: location.normalize(that.resolveForPage(context.file.path, './')),
book: {
language: context.book.language
}
};
}
},
plugins: {
resources: this.resources
},
@@ -243,7 +162,56 @@ WebsiteOutput.prototype.render = function(tpl, context) {
options: this.opts
});
return Promise.nfcall(this.env.render.bind(this.env), filename, context);
// Create environment
var env = setupTemplateEnv(this, context);
return Promise.nfcall(env.render.bind(env), filename, context);
};
/*
Render a template using nunjucks
Templates are stored in `_layouts` folders
@param {String} tpl: template name (ex: "page")
@param {String} outputFile: filename to write, relative to the output
@param {Object} context: context for the page
@return {Promise}
*/
WebsiteOutput.prototype.render = function(tpl, outputFile, context) {
var that = this;
// Calcul relative path to the root
var outputDirName = path.dirname(outputFile);
var basePath = location.normalize(path.relative(outputDirName, './'));
// Setup complete context
context = _.extend(context, {
basePath: basePath,
template: {
getJSContext: function() {
return {
page: _.omit(context.page, 'content'),
config: context.config,
file: context.file,
gitbook: context.gitbook,
basePath: basePath,
book: {
language: context.book.language
}
};
}
}
});
return this.renderAsString(tpl, context)
.then(function(html) {
return that.writeFile(
outputFile,
html
);
});
};
// Return a complete name for a template
@@ -252,3 +220,6 @@ WebsiteOutput.prototype.templateName = function(name) {
};
module.exports = WebsiteOutput;
+91
View File
@@ -0,0 +1,91 @@
var _ = require('lodash');
var nunjucks = require('nunjucks');
var path = require('path');
var fs = require('fs');
var DoExtension = require('nunjucks-do')(nunjucks);
var location = require('../../utils/location');
var defaultFilters = require('../../template/filters');
var ThemeLoader = require('./themeLoader');
// Directory for a theme with the templates
function templatesPath(dir) {
return path.join(dir, '_layouts');
}
/*
Create and setup at Nunjucks template environment
@return {Nunjucks.Environment}
*/
function setupTemplateEnv(output, context) {
var loader = new ThemeLoader(
_.map(output.searchPaths, templatesPath)
);
var env = new nunjucks.Environment(loader);
env.addExtension('DoExtension', new DoExtension());
// Add context as global
_.each(context, function(value, key) {
env.addGlobal(key, value);
});
// Add GitBook default filters
_.each(defaultFilters, function(fn, filter) {
env.addFilter(filter, fn);
});
// Translate using _i18n locales
env.addFilter('t', function t(s) {
return output.i18n.t(output.book.config.get('language'), s);
});
// Transform an absolute path into a relative path
// using this.ctx.page.path
env.addFilter('resolveFile', function resolveFile(href) {
return location.normalize(output.resolveForPage(context.file.path, href));
});
// Test if a file exists
env.addFilter('fileExists', function fileExists(href) {
return fs.existsSync(output.resolve(href));
});
// Transform a '.md' into a '.html' (README -> index)
env.addFilter('contentURL', function contentURL(s) {
return output.toURL(s);
});
// Get an article using its path
env.addFilter('getArticleByPath', function getArticleByPath(s) {
var article = output.book.summary.getArticle(s);
if (!article) return undefined;
return article.getContext();
});
// Relase path to an asset
env.addFilter('resolveAsset', function resolveAsset(href) {
href = path.join('gitbook', href);
// Resolve for current file
if (context.file) {
href = output.resolveForPage(context.file.path, '/' + href);
}
// Use assets from parent
if (output.book.isLanguageBook()) {
href = path.join('../', href);
}
return location.normalize(href);
});
return env;
}
module.exports = setupTemplateEnv;
@@ -7,36 +7,86 @@ var nunjucks = require('nunjucks');
Nunjucks loader similar to FileSystemLoader, but avoid infinite looping
*/
/*
Return true if a filename is relative.
*/
function isRelative(filename) {
return (filename.indexOf('./') === 0 || filename.indexOf('../') === 0);
}
var Loader = nunjucks.Loader.extend({
var ThemeLoader = nunjucks.Loader.extend({
init: function(searchPaths) {
this.searchPaths = _.map(searchPaths, path.normalize);
},
/*
Read source of a resolved filepath
@param {String}
@return {Object}
*/
getSource: function(fullpath) {
if (!fullpath) return null;
fullpath = this.resolve(null, fullpath);
var templateName = this.getTemplateName(fullpath);
if(!fullpath) {
return null;
}
return {
src: fs.readFileSync(fullpath, 'utf-8'),
src: '{% do %}template = template || {}; template.stack = template.stack || []; template.stack.push(template.self); template.self = ' + JSON.stringify(templateName) + '{% enddo %}\n' +
fs.readFileSync(fullpath, 'utf-8') +
'\n{% do %}template.self = template.stack.pop();{% enddo %}',
path: fullpath,
noCache: true
};
},
// We handle absolute paths ourselves in ".resolve"
/*
Nunjucks calls "isRelative" to determine when to call "resolve".
We handle absolute paths ourselves in ".resolve" so we always return true
*/
isRelative: function() {
return true;
},
/*
Get original search path containing a template
@param {String} filepath
@return {String} searchPath
*/
getSearchPath: function(filepath) {
return _.chain(this.searchPaths)
.sortBy(function(s) {
return -s.length;
})
.find(function(basePath) {
return (filepath && filepath.indexOf(basePath) === 0);
})
.value();
},
/*
Get template name from a filepath
@param {String} filepath
@return {String} name
*/
getTemplateName: function(filepath) {
var originalSearchPath = this.getSearchPath(filepath);
return originalSearchPath? path.relative(originalSearchPath, filepath) : null;
},
/*
Resolve a template from a current template
@param {String|null} from
@param {String} to
@return {String|null}
*/
resolve: function(from, to) {
var searchPaths = this.searchPaths;
@@ -46,15 +96,8 @@ var Loader = nunjucks.Loader.extend({
}
// Determine in which search folder we currently are
var originalSearchPath = _.chain(this.searchPaths)
.sortBy(function(s) {
return -s.length;
})
.find(function(basePath) {
return (from && from.indexOf(basePath) === 0);
})
.value();
var originalFilename = originalSearchPath? path.relative(originalSearchPath, from) : null;
var originalSearchPath = this.getSearchPath(from);
var originalFilename = this.getTemplateName(from);
// If we are including same file from a different search path
// Slice the search paths to avoid including from previous ones
@@ -77,4 +120,4 @@ var Loader = nunjucks.Loader.extend({
}
});
module.exports = Loader;
module.exports = ThemeLoader;
+18 -3
View File
@@ -12,7 +12,12 @@ function pluginCtx(plugin) {
return ctx;
}
// Call a function "fn" with a context of page similar to the one in GitBook v2
/*
Call a function "fn" with a context of page similar to the one in GitBook v2
@params {Page}
@returns {String|undefined} new content of the page
*/
function pageHook(page, fn) {
// Get page context
var ctx = page.getContext().page;
@@ -24,16 +29,26 @@ function pageHook(page, fn) {
// Deprecate sections
error.deprecateField(ctx, 'sections', [
{ content: ctx.content }
{ content: ctx.content, type: 'normal' }
], '"sections" property is deprecated, use page.content instead');
// Keep reference of original content for compatibility
var originalContent = ctx.content;
return fn(ctx)
.then(function(result) {
// No returned value
// Existing content will be used
if (!result) return undefined;
if (result.content) {
// GitBook 3
// Use returned page.content if different from original content
if (result.content != originalContent) {
return result.content;
}
// GitBook 2 compatibility
// Finally, use page.sections
if (result.sections) {
return _.pluck(result.sections, 'content').join('\n');
}
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "gitbook",
"version": "3.0.0-pre.8",
"version": "3.0.0-pre.9",
"homepage": "https://www.gitbook.com",
"description": "Library and cmd utility to generate GitBooks",
"main": "lib/index.js",
@@ -24,9 +24,9 @@
"gitbook-plugin-fontsettings": "1.0.3",
"gitbook-plugin-highlight": "2.0.2",
"gitbook-plugin-livereload": "0.0.1",
"gitbook-plugin-search": "2.0.0",
"gitbook-plugin-search": "2.1.0",
"gitbook-plugin-lunr": "1.0.0",
"gitbook-plugin-theme-default": "1.0.0-pre.7",
"gitbook-plugin-theme-default": "1.0.0-pre.8",
"gitbook-plugin-sharing": "1.0.2",
"github-slugid": "1.0.1",
"graceful-fs": "4.1.3",
@@ -42,7 +42,7 @@
"npm": "3.8.6",
"npmi": "1.0.1",
"nunjucks": "2.4.1",
"nunjucks-autoescape": "1.0.1",
"nunjucks-do": "1.0.0",
"q": "1.4.1",
"read-installed": "^4.0.3",
"request": "2.70.0",
+43 -22
View File
@@ -68,24 +68,8 @@ function setupDefaultBook(files, summary, opts) {
}), opts);
}
// Output a book with a specific generator
function outputDefaultBook(Output, files, summary, opts) {
return setupDefaultBook(files, summary, opts)
.then(function(book) {
// Parse the book
return book.parse()
// Start generation
.then(function() {
var output = new Output(book);
return output.generate()
.thenResolve(output);
});
});
}
// Output a book with a specific generator
function outputBook(Output, files, opts) {
// Prepare output for a book
function setupOutput(Output, files, opts) {
return setupBook(files, opts)
.then(function(book) {
// Parse the book
@@ -93,13 +77,43 @@ function outputBook(Output, files, opts) {
// Start generation
.then(function() {
var output = new Output(book);
return output.generate()
.thenResolve(output);
return new Output(book);
});
});
}
// Prepare output for a book
function setupDefaultOutput(Output, files, summary, opts) {
return setupDefaultBook(files, summary, opts)
.then(function(book) {
// Parse the book
return book.parse()
// Start generation
.then(function() {
return new Output(book);
});
});
}
// Output a book with a specific generator
function outputDefaultBook(Output, files, summary, opts) {
return setupDefaultOutput(Output, files, summary, opts)
.then(function(output) {
return output.generate()
.thenResolve(output);
});
}
// Output a book with a specific generator
function outputBook(Output, files, opts) {
return setupOutput(Output, files, opts)
.then(function(output) {
return output.generate()
.thenResolve(output);
});
}
// Log an error
function logError(err) {
console.log(err.stack || err);
@@ -107,10 +121,17 @@ function logError(err) {
module.exports = {
fs: nodeFS,
setupFS: setupFS,
setupBook: setupBook,
outputBook: outputBook,
setupDefaultBook: setupDefaultBook,
setupOutput: setupOutput,
setupDefaultOutput: setupDefaultOutput,
outputBook: outputBook,
outputDefaultBook: outputDefaultBook,
logError: logError
};
+10
View File
@@ -0,0 +1,10 @@
{
"name": "gitbook-plugin-dep1",
"version": "1.0.0",
"engines": {
"gitbook": "*"
},
"dependencies": {
"gitbook-plugin-dep2": "*"
}
}
+7
View File
@@ -0,0 +1,7 @@
{
"name": "gitbook-plugin-dep2",
"version": "1.0.0",
"engines": {
"gitbook": "*"
}
}
+8
View File
@@ -0,0 +1,8 @@
module.exports = {
hooks: {
page: function(page) {
page.sections[0].content = 'Hello (sections) ' + page.content;
return page;
}
}
};
+7
View File
@@ -0,0 +1,7 @@
{
"name": "gitbook-plugin-test-deprecated",
"version": "1.0.0",
"engines": {
"gitbook": "*"
}
}
+4 -2
View File
@@ -1,5 +1,3 @@
var should = require('should');
module.exports = {
hooks: {
'init': function() {
@@ -11,6 +9,10 @@ module.exports = {
},
'finish:before': function() {
global._hooks.push('finish:before');
},
page: function(page) {
page.content = 'Hello ' + page.content;
return page;
}
}
};
+9 -4
View File
@@ -20,7 +20,8 @@ describe('Website Output', function() {
});
it('should correctly copy assets', function() {
output.should.have.file('gitbook/app.js');
output.should.have.file('gitbook/gitbook.js');
output.should.have.file('gitbook/theme.js');
output.should.have.file('gitbook/images/favicon.ico');
});
@@ -84,12 +85,16 @@ describe('Website Output', function() {
});
it('should correctly copy assets', function() {
output.should.have.file('gitbook/app.js');
output.should.have.file('gitbook/gitbook.js');
output.should.have.file('gitbook/theme.js');
});
it('should not copy assets for each language', function() {
output.should.have.not.file('en/gitbook/app.js');
output.should.have.not.file('fr/gitbook/app.js');
output.should.have.not.file('en/gitbook/gitbook.js');
output.should.have.not.file('fr/gitbook/gitbook.js');
output.should.have.not.file('en/gitbook/theme.js');
output.should.have.not.file('fr/gitbook/theme.js');
});
it('should correctly generate an index.html', function() {
+37
View File
@@ -6,6 +6,7 @@ var registry = require('../lib/plugins/registry');
var Output = require('../lib/output/base');
var PluginsManager = require('../lib/plugins');
var BookPlugin = require('../lib/plugins/plugin');
var JSONOutput = require('../lib/output/json');
var PLUGINS_ROOT = path.resolve(__dirname, 'node_modules');
@@ -229,6 +230,42 @@ describe('Plugins', function() {
global._hooks.should.deepEqual(['init']);
});
});
describe('Hook "page"', function() {
var pluginDeprecated;
before(function() {
pluginDeprecated = TestPlugin(book, 'test-deprecated');
return pluginDeprecated.load(PLUGINS_ROOT);
});
it('should update content using "content" property', function() {
return mock.setupDefaultOutput(JSONOutput)
.then(function(output) {
output.plugins.load(plugin);
return output.generate()
.then(function() {
var json = require(output.resolve('README.json'));
json.page.content.should.equal('Hello <p>Hello</p>\n');
});
});
});
it('should update content using deprecated "sections" property', function() {
return mock.setupDefaultOutput(JSONOutput)
.then(function(output) {
output.plugins.load(pluginDeprecated);
return output.generate()
.then(function() {
var json = require(output.resolve('README.json'));
json.page.content.should.equal('Hello (sections) <p>Hello</p>\n');
});
});
});
});
});
});