summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSamy Pesse <samypesse@gmail.com>2016-04-24 18:52:37 +0200
committerSamy Pesse <samypesse@gmail.com>2016-04-24 18:52:37 +0200
commit19a1db8cb43431352d30dbc48aebb9cc8bf58eec (patch)
tree582b10a38610420b44fe91c3647c7fc448655d5c
parentc3275a4aa985710c0fcc9d3f7104bc5ebed2eb04 (diff)
downloadgitbook-19a1db8cb43431352d30dbc48aebb9cc8bf58eec.zip
gitbook-19a1db8cb43431352d30dbc48aebb9cc8bf58eec.tar.gz
gitbook-19a1db8cb43431352d30dbc48aebb9cc8bf58eec.tar.bz2
Add base structure for templating
-rw-r--r--lib/models/book.js2
-rw-r--r--lib/models/plugin.js24
-rw-r--r--lib/models/templateBlock.js264
-rw-r--r--lib/models/templateEngine.js106
-rw-r--r--lib/output/createTemplateEngine.js31
-rw-r--r--lib/utils/genKey.js13
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;