mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-20 01:25:16 +00:00
Add base structure for templating
This commit is contained in:
+1
-1
@@ -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)
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
var Immutable = require('immutable');
|
||||
|
||||
var TemplateBlock = require('./templateBlock');
|
||||
var PREFIX = require('../constants/pluginPrefix');
|
||||
var DEFAULT_VERSION = '*';
|
||||
|
||||
@@ -72,6 +73,29 @@ Plugin.prototype.getHooks = function() {
|
||||
return this.getContent().get('hooks');
|
||||
};
|
||||
|
||||
/**
|
||||
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
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user