diff options
author | Samy Pesse <samypesse@gmail.com> | 2016-04-24 18:52:37 +0200 |
---|---|---|
committer | Samy Pesse <samypesse@gmail.com> | 2016-04-24 18:52:37 +0200 |
commit | 19a1db8cb43431352d30dbc48aebb9cc8bf58eec (patch) | |
tree | 582b10a38610420b44fe91c3647c7fc448655d5c | |
parent | c3275a4aa985710c0fcc9d3f7104bc5ebed2eb04 (diff) | |
download | gitbook-19a1db8cb43431352d30dbc48aebb9cc8bf58eec.zip gitbook-19a1db8cb43431352d30dbc48aebb9cc8bf58eec.tar.gz gitbook-19a1db8cb43431352d30dbc48aebb9cc8bf58eec.tar.bz2 |
Add base structure for templating
-rw-r--r-- | lib/models/book.js | 2 | ||||
-rw-r--r-- | lib/models/plugin.js | 24 | ||||
-rw-r--r-- | lib/models/templateBlock.js | 264 | ||||
-rw-r--r-- | lib/models/templateEngine.js | 106 | ||||
-rw-r--r-- | lib/output/createTemplateEngine.js | 31 | ||||
-rw-r--r-- | lib/utils/genKey.js | 13 |
6 files changed, 439 insertions, 1 deletions
diff --git a/lib/models/book.js b/lib/models/book.js index 62faba6..9b9f769 100644 --- a/lib/models/book.js +++ b/lib/models/book.js @@ -174,7 +174,7 @@ Book.createFromParent = function createFromParent(parent, basePath) { return new Book({ logger: parent.getLogger(), parent: parent, - fs: FS.reduceScope(book.getFS(), basePath) + fs: FS.reduceScope(parent.getFS(), basePath) }); }; diff --git a/lib/models/plugin.js b/lib/models/plugin.js index 6d322f4..2f791dc 100644 --- a/lib/models/plugin.js +++ b/lib/models/plugin.js @@ -1,5 +1,6 @@ var Immutable = require('immutable'); +var TemplateBlock = require('./templateBlock'); var PREFIX = require('../constants/pluginPrefix'); var DEFAULT_VERSION = '*'; @@ -73,6 +74,29 @@ Plugin.prototype.getHooks = function() { }; /** + Return map of filters + @return {Map<String:Function>} +*/ +Plugin.prototype.getFilters = function() { + return this.getContent().get('filters'); +}; + +/** + Return map of blocks + @return {List<TemplateBlock>} +*/ +Plugin.prototype.getBlocks = function() { + var blocks = this.getContent().get('blocks'); + + return blocks + .map(function(block, blockName) { + block.name = blockName; + return new TemplateBlock(block); + }) + .toList(); +}; + +/** Return a specific hook @param {String} name diff --git a/lib/models/templateBlock.js b/lib/models/templateBlock.js new file mode 100644 index 0000000..ec7dc7c --- /dev/null +++ b/lib/models/templateBlock.js @@ -0,0 +1,264 @@ +var is = require('is'); +var Immutable = require('immutable'); + +var Promise = require('../utils/promise'); +var genKey = require('../utils/genKey'); + +var NODE_ENDARGS = '%%endargs%%'; + +var blockBodies = {}; + +var TemplateBlock = Immutable.Record({ + name: String(), + end: String(), + process: Function(), + blocks: Immutable.List(), + shortcuts: Immutable.List(), + post: null, + parse: true +}); + +TemplateBlock.prototype.getName = function() { + return this.get('name'); +}; + +TemplateBlock.prototype.getPost = function() { + return this.get('post'); +}; + +TemplateBlock.prototype.getParse = function() { + return this.get('parse'); +}; + +TemplateBlock.prototype.getEndTag = function() { + return this.get('end') || ('end' + this.getName()); +}; + +TemplateBlock.prototype.getProcess = function() { + return this.get('process'); +}; + +TemplateBlock.prototype.getBlocks = function() { + return this.get('blocks'); +}; + +TemplateBlock.prototype.getShortcuts = function() { + return this.get('shortcuts'); +}; + +/** + Return name for the nunjucks extension + + @return {String} +*/ +TemplateBlock.prototype.getExtensionName = function() { + return 'Block' + this.getName() + 'Extension'; +}; + +/** + Return a nunjucks extension to represents this block + + @return {Nunjucks.Extension} +*/ +TemplateBlock.prototype.toNunjucksExt = function() { + var that = this; + var name = this.getName(); + var endTag = this.getEndTag(); + var blocks = this.getBlocks(); + + var Ext = function () { + this.tags = [name]; + + this.parse = function(parser, nodes) { + var lastBlockName = null; + var lastBlockArgs = null; + var allBlocks = blocks.concat([endTag]); + + // Parse first block + var tok = parser.nextToken(); + lastBlockArgs = parser.parseSignature(null, true); + parser.advanceAfterBlockEnd(tok.value); + + var args = new nodes.NodeList(); + var bodies = []; + var blockNamesNode = new nodes.Array(tok.lineno, tok.colno); + var blockArgCounts = new nodes.Array(tok.lineno, tok.colno); + + // Parse while we found "end<block>" + do { + // Read body + var currentBody = parser.parseUntilBlocks.apply(parser, allBlocks); + + // Handle body with previous block name and args + blockNamesNode.addChild(new nodes.Literal(args.lineno, args.colno, lastBlockName)); + blockArgCounts.addChild(new nodes.Literal(args.lineno, args.colno, lastBlockArgs.children.length)); + bodies.push(currentBody); + + // Append arguments of this block as arguments of the run function + lastBlockArgs.children.forEach(function(child) { + args.addChild(child); + }); + + // Read new block + lastBlockName = parser.nextToken().value; + + // Parse signature and move to the end of the block + if (lastBlockName != endTag) { + lastBlockArgs = parser.parseSignature(null, true); + } + + parser.advanceAfterBlockEnd(lastBlockName); + } while (lastBlockName != endTag); + + args.addChild(blockNamesNode); + args.addChild(blockArgCounts); + args.addChild(new nodes.Literal(args.lineno, args.colno, NODE_ENDARGS)); + + return new nodes.CallExtensionAsync(this, 'run', args, bodies); + }; + + this.run = function(context) { + var fnArgs = Array.prototype.slice.call(arguments, 1); + + var args; + var blocks = []; + var bodies = []; + var blockNames; + var blockArgCounts; + var callback; + + // Extract callback + callback = fnArgs.pop(); + + // Detect end of arguments + var endArgIndex = fnArgs.indexOf(NODE_ENDARGS); + + // Extract arguments and bodies + args = fnArgs.slice(0, endArgIndex); + bodies = fnArgs.slice(endArgIndex + 1); + + // Extract block counts + blockArgCounts = args.pop(); + blockNames = args.pop(); + + // Recreate list of blocks + blockNames.forEach(function(name, i) { + var countArgs = blockArgCounts[i]; + var blockBody = bodies.shift(); + + var blockArgs = countArgs > 0? args.slice(0, countArgs) : []; + args = args.slice(countArgs); + var blockKwargs = extractKwargs(blockArgs); + + blocks.push({ + name: name, + body: blockBody(), + args: blockArgs, + kwargs: blockKwargs + }); + }); + + var mainBlock = blocks.shift(); + mainBlock.blocks = blocks; + + Promise() + .then(function() { + return that.applyBlock(mainBlock, context); + }) + .then(function(result) { + return that.blockResultToHtml(result); + }) + .nodeify(callback); + }; + }; + + return Ext; +}; + +/** + Apply a block to a content + @param {Object} inner + @param {Object} context + @return {Promise<String>|String} +*/ +TemplateBlock.prototype.applyBlock = function(inner, context) { + var processFn = this.getProcess(); + + inner = inner || {}; + inner.args = inner.args || []; + inner.kwargs = inner.kwargs || {}; + inner.blocks = inner.blocks || []; + + var r = processFn.call(context, inner); + + if (Promise.isPromiseAlike(r)) { + return r.then(this.handleBlockResult); + } else { + return this.handleBlockResult(r); + } +}; + +/** + Handle result from a block process function + + @param {Object} result + @return {Object} +*/ +TemplateBlock.prototype.handleBlockResult = function(result) { + if (is.string(result)) { + result = { body: result }; + } + + return result; +}; + +/** + Convert a block result to HTML + + @param {Object} result + @return {String} +*/ +TemplateBlock.prototype.blockResultToHtml = function(result) { + var parse = this.getParse(); + var indexedKey; + var toIndex = (!parse) || (this.getPost() !== undefined); + + if (toIndex) { + indexedKey = TemplateBlock.indexBlockResult(result); + } + + // Parsable block, just return it + if (parse) { + return result.body; + } + + // Return it as a position marker + return '{{-%' + indexedKey + '%-}}'; + +}; + +/** + Index a block result, and return the indexed key + + @param {Object} blk + @return {String} +*/ +TemplateBlock.indexBlockResult = function(blk) { + var key = genKey(); + blockBodies[key] = blk; + + return key; +}; + +/** + Extract kwargs from an arguments array + + @param {Array} args + @return {Object} +*/ +function extractKwargs(args) { + var last = args[args.length - 1]; + return (is.object(last) && last.__keywords)? args.pop() : {}; +} + +module.exports = TemplateBlock; diff --git a/lib/models/templateEngine.js b/lib/models/templateEngine.js new file mode 100644 index 0000000..1f93879 --- /dev/null +++ b/lib/models/templateEngine.js @@ -0,0 +1,106 @@ +var nunjucks = require('nunjucks'); +var Immutable = require('immutable'); + +var TemplateEngine = Immutable.Record({ + // List of {TemplateBlock} + blocks: Immutable.List(), + + // Map of filters: {String} name -> {Function} fn + filters: Immutable.Map(), + + // Map of globals: {String} name -> {Mixed} + globals: Immutable.Map(), + + // Context for filters / blocks + context: Object(), + + // Nunjucks loader + loader: nunjucks.FileSystemLoader('views') +}); + +TemplateEngine.prototype.getBlocks = function() { + return this.get('blocks'); +}; + +TemplateEngine.prototype.getGlobals = function() { + return this.get('globals'); +}; + +TemplateEngine.prototype.getFilters = function() { + return this.get('filters'); +}; + +TemplateEngine.prototype.getShortcuts = function() { + return this.get('shortcuts'); +}; + +TemplateEngine.prototype.getLoader = function() { + return this.get('loader'); +}; + +TemplateEngine.prototype.getContext = function() { + return this.get('context'); +}; + +/** + Return a nunjucks environment from this configuration + + @return {Nunjucks.Environment} +*/ +TemplateEngine.prototype.toNunjucks = function() { + var that = this; + var loader = this.getLoader(); + var blocks = this.getBlocks(); + var filters = this.getFilters(); + var globals = this.getGlobals(); + + var env = new nunjucks.Environment( + loader, + { + // Escaping is done after by the asciidoc/markdown parser + autoescape: false, + + // Syntax + tags: { + blockStart: '{%', + blockEnd: '%}', + variableStart: '{{', + variableEnd: '}}', + commentStart: '{###', + commentEnd: '###}' + } + } + ); + + // Add filters + filters.forEach(function(filterFn, filterName) { + env.addFilter(filterName, that.bindToContext(filterFn)); + }); + + // Add blocks + blocks.forEach(function(block) { + var extName = block.getExtensionName(); + var Ext = block.toNunjucksExt(); + + env.addExtension(extName, new Ext()); + }); + + // Add globals + globals.forEach(function(globalName, globalValue) { + env.addGlobal(globalName, globalValue); + }); + + return env; +}; + +/** + Bind a function to the context + + @param {Function} fn + @return {Function} +*/ +TemplateEngine.prototype.bindToContext = function(fn) { + return fn.bind(this.getContext()); +}; + +module.exports = TemplateEngine; diff --git a/lib/output/createTemplateEngine.js b/lib/output/createTemplateEngine.js new file mode 100644 index 0000000..810c41e --- /dev/null +++ b/lib/output/createTemplateEngine.js @@ -0,0 +1,31 @@ +var Immutable = require('immutable'); +var TemplateEngine = require('../models/templateEngine'); + +/** + Create template engine for an output. + It adds default filters/blocks, then add the ones from plugins + + @param {Output} output + @return {TemplateEngine} +*/ +function createTemplateEngine(output) { + var plugins = output.getPlugins(); + + var filters = plugins + .reduce(function(result, plugin) { + return result.merge(plugin.getFilters()); + }, Immutable.Map()); + + var blocks = plugins + .map(function(plugin) { + return plugin.getBlocks(); + }) + .flatten(); + + return new TemplateEngine({ + filters: filters, + blocks: blocks + }); +} + +module.exports = createTemplateEngine; diff --git a/lib/utils/genKey.js b/lib/utils/genKey.js new file mode 100644 index 0000000..0650011 --- /dev/null +++ b/lib/utils/genKey.js @@ -0,0 +1,13 @@ +var lastKey = 0; + +/* + Generate a random key + @return {String} +*/ +function generateKey() { + lastKey += 1; + var str = lastKey.toString(16); + return '00000'.slice(str.length) + str; +} + +module.exports = generateKey; |