summaryrefslogtreecommitdiffstats
path: root/lib/template/index.js
diff options
context:
space:
mode:
Diffstat (limited to 'lib/template/index.js')
-rw-r--r--lib/template/index.js333
1 files changed, 333 insertions, 0 deletions
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;