diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/backbone/page.js | 2 | ||||
-rw-r--r-- | lib/config/index.js | 12 | ||||
-rw-r--r-- | lib/generators/json.js | 2 | ||||
-rw-r--r-- | lib/output.js | 19 | ||||
-rw-r--r-- | lib/plugins/index.js | 61 | ||||
-rw-r--r-- | lib/plugins/manager.js | 61 | ||||
-rw-r--r-- | lib/template/blocks.js (renamed from lib/templating/blocks.js) | 0 | ||||
-rw-r--r-- | lib/template/index.js | 333 | ||||
-rw-r--r-- | lib/template/loader.js (renamed from lib/templating/loader.js) | 0 | ||||
-rw-r--r-- | lib/templating/index.js | 27 | ||||
-rw-r--r-- | lib/utils/error.js | 13 |
11 files changed, 433 insertions, 97 deletions
diff --git a/lib/backbone/page.js b/lib/backbone/page.js index eb3cd61..13b9c59 100644 --- a/lib/backbone/page.js +++ b/lib/backbone/page.js @@ -27,7 +27,7 @@ Page.prototype.read = function() { // Parse the page and return its content Page.prototype.parse = function() { - + return this.read(); }; diff --git a/lib/config/index.js b/lib/config/index.js index 7734392..563f03b 100644 --- a/lib/config/index.js +++ b/lib/config/index.js @@ -36,14 +36,15 @@ Config.prototype.load = function() { that.log.debug.ln('try loading configuration from', filename); return that.fs.loadAsObject(that.book.resolve(filename)) + .then(function(_config) { + that.filename = filename; + return that.replace(_config); + }) .fail(function(err) { if (err.code != 'MODULE_NOT_FOUND') throw(err); else return Promise(false); }); }) - .then(function(_config) { - return that.replace(_config); - }) .then(function() { if (!that.book.isLanguageBook()) { if (!gitbook.satisfies(that.options.gitbook)) { @@ -124,4 +125,9 @@ 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); +}; + module.exports = Config; diff --git a/lib/generators/json.js b/lib/generators/json.js index b76a62e..0cfaeb7 100644 --- a/lib/generators/json.js +++ b/lib/generators/json.js @@ -17,7 +17,7 @@ JSONGenerator.prototype.writePage = function(page) { .then(function() { var json = {}; - return this.output.writeFile( + return that.output.writeFile( page.withExtension('.json'), JSON.stringify(json, null, 4) ); diff --git a/lib/output.js b/lib/output.js index 89233da..01610c6 100644 --- a/lib/output.js +++ b/lib/output.js @@ -11,20 +11,22 @@ function Output(book, type) { if (!generators[type]) throw new Error('Generator not found"' + type + '"'); this.book = book; + this.log = this.book.log; + this.type = type; this.plugins = new PluginsManager(book); this.generator = new generators[type](this, type); // Files to ignore in output this.ignore = Ignore(); - this.ignore.addPattern([ + this.ignore.addPattern(_.compact([ '.gitignore', '.ignore', '.bookignore', // The configuration file should not be copied in the output this.book.config.filename - ]); + ])); } @@ -36,16 +38,25 @@ Output.prototype.resolve = function(filename) { // Write a file/buffer to the output folder Output.prototype.writeFile = function(filename, buf) { filename = this.resolve(filename); - return Promise.nfcall(fs.writeFileSync, filename, buf); + return Promise.nfcall(fs.writeFile, filename, buf); }; // Start the generation, for a parsed book Output.prototype.generate = function() { var that = this; - var isMultilingual = this.isMultilingual(); + var isMultilingual = this.book.isMultilingual(); return Promise() + // Load all plugins + .then(function() { + that.log.info.ln('Loading and preparing plugins'); + + var plugins = _.pluck(that.book.config.get('plugins'), 'name'); + + return that.plugins.load(plugins); + }) + // Initialize the generation .then(function() { return that.generator.prepare(); diff --git a/lib/plugins/index.js b/lib/plugins/index.js index e69de29..33a2047 100644 --- a/lib/plugins/index.js +++ b/lib/plugins/index.js @@ -0,0 +1,61 @@ +var _ = require('lodash'); + +var Promise = require('../utils/promise'); +var BookPlugin = require('./plugin'); + + +/* +PluginsManager is an interface to work with multiple plugins at once: +- Extract assets from plugins +- Call hooks for all plugins, etc +*/ + +function PluginsManager(book) { + this.book = book; + this.plugins = []; +} + +// Returns a plugin by its name +PluginsManager.prototype.get = function(name) { + return _.find(this.plugins, { + id: name + }); +}; + +// Load a plugin, or a list of plugins +PluginsManager.prototype.load = function(name) { + var that = this; + + if (_.isArray(name)) { + return Promise.serie(name, function(_name) { + return that.load(_name); + }); + } + + return Promise() + + // Initiate and load the plugin + .then(function() { + var plugin; + + if (!_.isString(name)) plugin = name; + else plugin = new BookPlugin(that.book, name); + + if (that.get(plugin.id)) { + throw new Error('Plugin "'+plugin.id+'" is already loaded'); + } + + + if (plugin.isLoaded()) return plugin; + else return plugin.load() + .thenResolve(plugin); + }) + + .then(function(plugin) { + that.plugins.push(plugin); + }); +}; + + + +module.exports = PluginsManager; diff --git a/lib/plugins/manager.js b/lib/plugins/manager.js deleted file mode 100644 index b6549d6..0000000 --- a/lib/plugins/manager.js +++ /dev/null @@ -1,61 +0,0 @@ -var _ = require('lodash'); - -var Promise = require('../utils/promise'); -var BookPlugin = require('./plugin'); - - -/* -PluginsManager is an interface to work with multiple plugins at once: -- Extract assets from plugins -- Call hooks for all plugins, etc -*/ - -function PluginsManager(book) { - this.book = book; - this.plugins = []; -} - -// Returns a plugin by its name -PluginsManager.prototype.get = function(name) { - return _.find(this.plugins, { - id: name - }); -}; - -// Load a plugin, or a list of plugins -PluginsManager.prototype.load = function(name) { - var that = this; - - if (!_.isArray(name)) { - return Promise.serie(name, function(_name) { - return that.load(_name); - }); - } - - return Promise() - - // Initiate and load the plugin - .then(function() { - var plugin; - - if (!_.isString(name)) plugin = name; - else plugin = new BookPlugin(that.book, name); - - if (that.get(plugin.id)) { - throw new Error('Plugin "'+plugin.id+'" is already loaded'); - } - - - if (plugin.isLoaded()) return plugin; - else return plugin.load() - .thenResolve(plugin); - }) - - .then(function(plugin) { - that.plugins.push(plugin); - }); -}; - - - -module.exports = PluginsManager; diff --git a/lib/templating/blocks.js b/lib/template/blocks.js index 92097a7..92097a7 100644 --- a/lib/templating/blocks.js +++ b/lib/template/blocks.js diff --git a/lib/template/index.js b/lib/template/index.js new file mode 100644 index 0000000..128e171 --- /dev/null +++ b/lib/template/index.js @@ -0,0 +1,333 @@ +var _ = require('lodash'); +var path = require('path'); +var nunjucks = require('nunjucks'); +var parsers = require('gitbook-parsers'); + +var Promise = require('../utils/promise'); +var error = require('../utils/error'); +var gitbook = require('../gitbook'); +var defaultBlocks = require('./blocks'); + +// Return extension name for a specific block +function blockExtName(name) { + return 'Block'+name+'Extension'; +} + +// Normalize the result of block process function +function normBlockResult(blk) { + if (_.isString(blk)) blk = { body: blk }; + return blk; +} + +function TemplateEngine(book) { + this.book = book; + this.log = book.log; + + this.env = new nunjucks.Environment( + this.loader, + { + // Escaping is done after by the asciidoc/markdown parser + autoescape: false, + + // Syntax + tags: { + blockStart: '{%', + blockEnd: '%}', + variableStart: '{{', + variableEnd: '}}', + commentStart: '{###', + commentEnd: '###}' + } + } + ); + + // List of tags shortcuts + this.shortcuts = []; + + // Map of blocks bodies (that requires post-processing) + this.blockBodies = {}; + + // Map of added blocks + this.blocks = {}; + + // Bind methods + _.bindAll(this); + + // Add default blocks + this.addBlocks(defaultBlocks); +} + +// Add a new custom filter +TemplateEngine.prototype.addFilter = function(filterName, func) { + try { + this.env.getFilter(filterName); + this.log.error.ln('conflict in filters, "'+filterName+'" is already set'); + return false; + } catch(e) { + // Filter doesn't exist + } + + this.log.debug.ln('add filter "'+filterName+'"'); + this.env.addFilter(filterName, this.bindContext(function() { + var ctx = this; + var args = Array.prototype.slice.apply(arguments); + var callback = _.last(args); + + Promise() + .then(function() { + return func.apply(ctx, args.slice(0, -1)); + }) + .nodeify(callback); + }), true); + return true; +}; + +// Add multiple filters at once +TemplateEngine.prototype.addFilters = function(filters) { + _.each(filters, function(filter, name) { + this.addFilter(name, filter); + }, this); +}; + +// Return true if a block is defined +TemplateEngine.prototype.hasBlock = function(name) { + return this.env.hasExtension(blockExtName(name)); +}; + +// Remove/Disable a block +TemplateEngine.prototype.removeBlock = function(name) { + if (!this.hasBlock(name)) return; + + // Remove nunjucks extension + this.env.removeExtension(blockExtName(name)); + + // Cleanup shortcuts + this.shortcuts = _.reject(this.shortcuts, { + block: name + }); +}; + +// Add a block +// Using the extensions of nunjucks: https://mozilla.github.io/nunjucks/api.html#addextension +TemplateEngine.prototype.addBlock = function(name, block) { + var that = this, Ext, extName; + + // Block can be a simple function + if (_.isFunction(block)) block = { process: block }; + + block = _.defaults(block || {}, { + shortcuts: [], + end: 'end'+name, + process: _.identity, + blocks: [] + }); + + extName = blockExtName(name); + + if (this.hasBlock(name) && !defaultBlocks[name]) { + this.log.warn.ln('conflict in blocks, "'+name+'" is already defined'); + } + + // Cleanup previous block + this.removeBlock(name); + + this.log.debug.ln('add block \''+name+'\''); + this.blocks[name] = block; + + Ext = function () { + this.tags = [name]; + + this.parse = function(parser, nodes) { + var body = null; + var lastBlockName = null; + var lastBlockArgs = null; + var allBlocks = block.blocks.concat([block.end]); + var subbodies = {}; + + var tok = parser.nextToken(); + var args = parser.parseSignature(null, true); + parser.advanceAfterBlockEnd(tok.value); + + while (1) { + // Read body + var currentBody = parser.parseUntilBlocks.apply(parser, allBlocks); + + // Handle body with previous block name and args + if (lastBlockName) { + subbodies[lastBlockName] = subbodies[lastBlockName] || []; + subbodies[lastBlockName].push({ + body: currentBody, + args: lastBlockArgs + }); + } else { + body = currentBody; + } + + // Read new block + lastBlockName = parser.peekToken().value; + if (lastBlockName == block.end) { + break; + } + + // Parse signature and move to the end of the block + lastBlockArgs = parser.parseSignature(null, true); + parser.advanceAfterBlockEnd(lastBlockName); + } + parser.advanceAfterBlockEnd(); + + var bodies = [body]; + _.each(block.blocks, function(blockName) { + subbodies[blockName] = subbodies[blockName] || []; + if (subbodies[blockName].length === 0) { + subbodies[blockName].push({ + args: new nodes.NodeList(), + body: new nodes.NodeList() + }); + } + + bodies.push(subbodies[blockName][0].body); + }); + + return new nodes.CallExtensionAsync(this, 'run', args, bodies); + }; + + this.run = function(context) { + var args = Array.prototype.slice.call(arguments, 1); + var callback = args.pop(); + + // Extract blocks + var blocks = args + .concat([]) + .slice(-block.blocks.length); + + // Eliminate blocks from list + if (block.blocks.length > 0) args = args.slice(0, -block.blocks.length); + + // Extract main body and kwargs + var body = args.pop(); + var kwargs = _.isObject(_.last(args))? args.pop() : {}; + + // Extract blocks body + var _blocks = _.map(block.blocks, function(blockName, i){ + return { + name: blockName, + body: blocks[i]() + }; + }); + + Promise() + .then(function() { + return that.applyBlock(name, { + body: body(), + args: args, + kwargs: kwargs, + blocks: _blocks + }, context); + }) + + // process the block returned + .then(that.processBlock) + .nodeify(callback); + }; + }; + + // Add the Extension + this.env.addExtension(extName, new Ext()); + + // Add shortcuts if any + if (!_.isArray(block.shortcuts)) { + block.shortcuts = [block.shortcuts]; + } + + _.each(block.shortcuts, function(shortcut) { + this.log.debug.ln('add template shortcut from "'+shortcut.start+'" to block "'+name+'" for parsers ', shortcut.parsers); + this.shortcuts.push({ + block: name, + parsers: shortcut.parsers, + start: shortcut.start, + end: shortcut.end, + tag: { + start: name, + end: block.end + } + }); + }, this); +}; + +// Add multiple blocks at once +TemplateEngine.prototype.addBlocks = function(blocks) { + _.each(blocks, function(block, name) { + this.addBlock(name, block); + }, this); +}; + +// Apply a block to some content +// This method result depends on the type of block (async or sync) +TemplateEngine.prototype.applyBlock = function(name, blk, ctx) { + var func, block, r; + + block = this.blocks[name]; + if (!block) throw new Error('Block not found "'+name+'"'); + if (_.isString(blk)) { + blk = { + body: blk + }; + } + + blk = _.defaults(blk, { + args: [], + kwargs: {}, + blocks: [] + }); + + // Bind and call block processor + func = this.bindContext(block.process); + r = func.call(ctx || {}, blk); + + if (Promise.isPromise(r)) return r.then(normBlockResult); + else return normBlockResult(r); +}; + + +// Render a string +TemplateEngine.prototype.renderString = function(content, context, options) { + options = _.defaults(options || {}, { + path: null, + type: null + }); + + // Setup context for the template + context = _.extend({}, context, { + // Variables from book.json + book: this.book.config.get('variables'), + + // Complete book.json + config: this.book.config.dump(), + + // infos about gitbook + gitbook: { + version: gitbook.version, + generator: this.book.config.get('generator') + } + }); + + // Setup path and type + if (options.path) { + options.path = this.book.resolve(options.path); + } + if (!options.type && options.path) { + var parser = parsers.get(path.extname(options.path)); + options.type = parser? parser.name : null; + } + + // Replace shortcuts + //content = this.applyShortcuts(options.type, content); + + return Promise.nfcall(this.env.renderString.bind(this.env), content, context, options) + .fail(function(err) { + throw error.enforce(err); + }); +}; + + +module.exports = TemplateEngine; diff --git a/lib/templating/loader.js b/lib/template/loader.js index e69de29..e69de29 100644 --- a/lib/templating/loader.js +++ b/lib/template/loader.js diff --git a/lib/templating/index.js b/lib/templating/index.js deleted file mode 100644 index 3a0fc16..0000000 --- a/lib/templating/index.js +++ /dev/null @@ -1,27 +0,0 @@ -var nunjucks = require('nunjucks'); - -function TemplatingEngine(book) { - this.book = book; - this.log = book.log; - - this.nunjucks = new nunjucks.Environment( - this.loader, - { - // Escaping is done after by the asciidoc/markdown parser - autoescape: false, - - // Syntax - tags: { - blockStart: '{%', - blockEnd: '%}', - variableStart: '{{', - variableEnd: '}}', - commentStart: '{###', - commentEnd: '###}' - } - } - ); -} - - -module.exports = TemplatingEngine; diff --git a/lib/utils/error.js b/lib/utils/error.js new file mode 100644 index 0000000..66e20db --- /dev/null +++ b/lib/utils/error.js @@ -0,0 +1,13 @@ +var _ = require('lodash'); + +// Enforce as an Error object, and cleanup message +function enforce(err) { + if (_.isString(err)) err = new Error(err); + err.message = err.message.replace(/^Error: /, ''); + + return err; +} + +module.exports = { + enforce: enforce +}; |