88e7003331
* made it possible to define an alternate name lookup scheme (so that {{foo}} does not have to be
context.foo, but can instead be something like context.get('foo'))
* made it possible to substitute an alternate buffer instead of the default empty String and override how
the compiled template appends to the buffer
* Added the concept of template-local data. In order to enable template-local data, pass true
as the second parameter to the template compiler. Then, pass in the data as the fourth
parameter (context, helpers, partials, data). These signatures may change before the 1.0 release.
653 lines
18 KiB
JavaScript
653 lines
18 KiB
JavaScript
var Handlebars = require("handlebars");
|
|
|
|
// BEGIN(BROWSER)
|
|
Handlebars.Compiler = function() {};
|
|
Handlebars.JavaScriptCompiler = function() {};
|
|
|
|
(function(Compiler, JavaScriptCompiler) {
|
|
Compiler.OPCODE_MAP = {
|
|
appendContent: 1,
|
|
getContext: 2,
|
|
lookupWithFallback: 3,
|
|
lookup: 4,
|
|
append: 5,
|
|
invokeMustache: 6,
|
|
appendEscaped: 7,
|
|
pushString: 8,
|
|
truthyOrFallback: 9,
|
|
functionOrFallback: 10,
|
|
invokeProgram: 11,
|
|
invokePartial: 12,
|
|
push: 13,
|
|
invokeInverse: 14
|
|
};
|
|
|
|
Compiler.MULTI_PARAM_OPCODES = {
|
|
appendContent: 1,
|
|
getContext: 1,
|
|
lookupWithFallback: 1,
|
|
lookup: 1,
|
|
invokeMustache: 2,
|
|
pushString: 1,
|
|
truthyOrFallback: 1,
|
|
functionOrFallback: 1,
|
|
invokeProgram: 2,
|
|
invokePartial: 1,
|
|
push: 1,
|
|
invokeInverse: 1
|
|
};
|
|
|
|
Compiler.DISASSEMBLE_MAP = {};
|
|
|
|
for(var prop in Compiler.OPCODE_MAP) {
|
|
var value = Compiler.OPCODE_MAP[prop];
|
|
Compiler.DISASSEMBLE_MAP[value] = prop;
|
|
}
|
|
|
|
Compiler.multiParamSize = function(code) {
|
|
return Compiler.MULTI_PARAM_OPCODES[Compiler.DISASSEMBLE_MAP[code]];
|
|
};
|
|
|
|
Compiler.prototype = {
|
|
disassemble: function() {
|
|
var opcodes = this.opcodes, opcode, nextCode;
|
|
var out = [], str, name, value;
|
|
|
|
for(var i=0, l=opcodes.length; i<l; i++) {
|
|
opcode = opcodes[i];
|
|
|
|
if(opcode === 'DECLARE') {
|
|
name = opcodes[++i];
|
|
value = opcodes[++i];
|
|
out.push("DECLARE " + name + " = " + value);
|
|
} else {
|
|
str = Compiler.DISASSEMBLE_MAP[opcode];
|
|
|
|
var extraParams = Compiler.multiParamSize(opcode);
|
|
var codes = [];
|
|
|
|
for(var j=0; j<extraParams; j++) {
|
|
nextCode = opcodes[++i];
|
|
|
|
if(typeof nextCode === "string") {
|
|
nextCode = "\"" + nextCode.replace("\n", "\\n") + "\"";
|
|
}
|
|
|
|
codes.push(nextCode);
|
|
}
|
|
|
|
str = str + " " + codes.join(" ");
|
|
|
|
out.push(str);
|
|
}
|
|
}
|
|
|
|
return out.join("\n");
|
|
},
|
|
|
|
guid: 0,
|
|
|
|
compile: function(program) {
|
|
this.children = [];
|
|
this.depths = {list: []};
|
|
return this.program(program);
|
|
},
|
|
|
|
accept: function(node) {
|
|
return this[node.type](node);
|
|
},
|
|
|
|
program: function(program) {
|
|
var statements = program.statements, statement;
|
|
this.opcodes = [];
|
|
|
|
for(var i=0, l=statements.length; i<l; i++) {
|
|
statement = statements[i];
|
|
this[statement.type](statement);
|
|
}
|
|
|
|
this.depths.list = this.depths.list.sort(function(a, b) {
|
|
return a - b;
|
|
});
|
|
|
|
return this;
|
|
},
|
|
|
|
compileProgram: function(program) {
|
|
var result = new Compiler().compile(program);
|
|
var guid = this.guid++;
|
|
|
|
this.usePartial = this.usePartial || result.usePartial;
|
|
|
|
this.children[guid] = result;
|
|
|
|
for(var i=0, l=result.depths.list.length; i<l; i++) {
|
|
depth = result.depths.list[i];
|
|
|
|
if(depth < 2) { continue; }
|
|
else { this.addDepth(depth - 1); }
|
|
}
|
|
|
|
return guid;
|
|
},
|
|
|
|
block: function(block) {
|
|
var mustache = block.mustache;
|
|
var params = mustache.params, depth, child, inverse, inverseGuid;
|
|
|
|
this.pushParams(params);
|
|
|
|
// ID lookup is now on the stack
|
|
this.ID(mustache.id);
|
|
|
|
var programGuid = this.compileProgram(block.program);
|
|
|
|
if(block.program.inverse) {
|
|
inverseGuid = this.compileProgram(block.program.inverse);
|
|
}
|
|
|
|
if(block.program.inverse) {
|
|
this.declare('inverse', inverseGuid);
|
|
}
|
|
|
|
this.opcode('invokeProgram', programGuid, params.length);
|
|
this.declare('inverse', null);
|
|
this.opcode('append');
|
|
},
|
|
|
|
inverse: function(block) {
|
|
this.ID(block.mustache.id);
|
|
var programGuid = this.compileProgram(block.program);
|
|
|
|
this.opcode('invokeInverse', programGuid);
|
|
this.opcode('append');
|
|
},
|
|
|
|
partial: function(partial) {
|
|
var id = partial.id;
|
|
this.usePartial = true;
|
|
|
|
if(partial.context) {
|
|
this.ID(partial.context);
|
|
} else {
|
|
this.opcode('push', 'context');
|
|
}
|
|
|
|
this.opcode('invokePartial', id.original);
|
|
this.opcode('append');
|
|
},
|
|
|
|
content: function(content) {
|
|
this.opcode('appendContent', content.string);
|
|
},
|
|
|
|
mustache: function(mustache) {
|
|
var params = mustache.params;
|
|
|
|
this.pushParams(params);
|
|
this.ID(mustache.id);
|
|
|
|
this.opcode('invokeMustache', params.length, mustache.id.original);
|
|
|
|
if(mustache.escaped) {
|
|
this.opcode('appendEscaped');
|
|
} else {
|
|
this.opcode('append');
|
|
}
|
|
},
|
|
|
|
ID: function(id) {
|
|
this.addDepth(id.depth);
|
|
|
|
this.opcode('getContext', id.depth);
|
|
|
|
this.opcode('lookupWithFallback', id.parts[0] || null);
|
|
|
|
for(var i=1, l=id.parts.length; i<l; i++) {
|
|
this.opcode('lookup', id.parts[i]);
|
|
}
|
|
},
|
|
|
|
STRING: function(string) {
|
|
this.opcode('pushString', string.string);
|
|
},
|
|
|
|
comment: function() {},
|
|
|
|
pushParams: function(params) {
|
|
var i = params.length, param;
|
|
|
|
while(i--) {
|
|
param = params[i];
|
|
this[param.type](param);
|
|
}
|
|
},
|
|
|
|
opcode: function(name, val1, val2) {
|
|
this.opcodes.push(Compiler.OPCODE_MAP[name]);
|
|
if(val1 !== undefined) { this.opcodes.push(val1); }
|
|
if(val2 !== undefined) { this.opcodes.push(val2); }
|
|
},
|
|
|
|
declare: function(name, value) {
|
|
this.opcodes.push('DECLARE');
|
|
this.opcodes.push(name);
|
|
this.opcodes.push(value);
|
|
},
|
|
|
|
addDepth: function(depth) {
|
|
if(depth === 0) { return; }
|
|
|
|
if(!this.depths[depth]) {
|
|
this.depths[depth] = true;
|
|
this.depths.list.push(depth);
|
|
}
|
|
}
|
|
};
|
|
|
|
JavaScriptCompiler.prototype = {
|
|
// PUBLIC API: You can override these methods in a subclass to provide
|
|
// alternative compiled forms for name lookup and buffering semantics
|
|
nameLookup: function(parent, name, type) {
|
|
if(JavaScriptCompiler.RESERVED_WORDS[name]) {
|
|
return parent + "['" + name + "']";
|
|
} else {
|
|
return parent + "." + name;
|
|
}
|
|
},
|
|
|
|
appendToBuffer: function(string) {
|
|
return "buffer = buffer + " + string + ";";
|
|
},
|
|
|
|
initializeBuffer: function() {
|
|
return this.quotedString("");
|
|
},
|
|
// END PUBLIC API
|
|
|
|
compile: function(environment, data) {
|
|
this.environment = environment;
|
|
this.data = data;
|
|
|
|
this.preamble();
|
|
|
|
this.stackSlot = 0;
|
|
this.stackVars = [];
|
|
this.registers = {list: []};
|
|
|
|
this.compileChildren(environment, data);
|
|
|
|
Handlebars.log(Handlebars.logger.DEBUG, environment.disassemble() + "\n\n");
|
|
|
|
var opcodes = environment.opcodes, opcode, name, declareName, declareVal;
|
|
|
|
this.i = 0;
|
|
|
|
for(l=opcodes.length; this.i<l; this.i++) {
|
|
opcode = this.nextOpcode(0);
|
|
|
|
if(opcode[0] === 'DECLARE') {
|
|
this.i = this.i + 2;
|
|
this[opcode[1]] = opcode[2];
|
|
} else {
|
|
this.i = this.i + opcode[1].length;
|
|
this[opcode[0]].apply(this, opcode[1]);
|
|
}
|
|
}
|
|
|
|
return this.createFunction();
|
|
},
|
|
|
|
nextOpcode: function(n) {
|
|
var opcodes = this.environment.opcodes, opcode = opcodes[this.i + n], name, val;
|
|
var extraParams, codes;
|
|
|
|
if(opcode === 'DECLARE') {
|
|
name = opcodes[this.i + 1];
|
|
val = opcodes[this.i + 2];
|
|
return ['DECLARE', name, val];
|
|
} else {
|
|
name = Compiler.DISASSEMBLE_MAP[opcode];
|
|
|
|
extraParams = Compiler.multiParamSize(opcode);
|
|
codes = [];
|
|
|
|
for(var j=0; j<extraParams; j++) {
|
|
codes.push(opcodes[this.i + j + 1 + n]);
|
|
}
|
|
|
|
return [name, codes];
|
|
}
|
|
},
|
|
|
|
eat: function(opcode) {
|
|
this.i = this.i + opcode.length;
|
|
},
|
|
|
|
preamble: function() {
|
|
var out = [];
|
|
out.push("var buffer = " + this.initializeBuffer() + ", currentContext = context");
|
|
|
|
var copies = "helpers = helpers || Handlebars.helpers;";
|
|
if(this.environment.usePartial) { copies = copies + " partials = partials || Handlebars.partials;"; }
|
|
out.push(copies);
|
|
|
|
// track the last context pushed into place to allow skipping the
|
|
// getContext opcode when it would be a noop
|
|
this.lastContext = 0;
|
|
this.source = out;
|
|
},
|
|
|
|
createFunction: function() {
|
|
var container = {
|
|
escapeExpression: Handlebars.Utils.escapeExpression,
|
|
invokePartial: Handlebars.VM.invokePartial,
|
|
programs: [],
|
|
program: function(i, helpers, partials, data) {
|
|
var programWrapper = this.programs[i];
|
|
if(data) {
|
|
return Handlebars.VM.program(this.children[i], helpers, partials, data);
|
|
} else if(programWrapper) {
|
|
return programWrapper;
|
|
} else {
|
|
programWrapper = this.programs[i] = Handlebars.VM.program(this.children[i], helpers, partials);
|
|
return programWrapper;
|
|
}
|
|
},
|
|
programWithDepth: Handlebars.VM.programWithDepth,
|
|
noop: Handlebars.VM.noop
|
|
};
|
|
var locals = this.stackVars.concat(this.registers.list);
|
|
|
|
if(locals.length > 0) {
|
|
this.source[0] = this.source[0] + ", " + locals.join(", ");
|
|
}
|
|
|
|
this.source[0] = this.source[0] + ";";
|
|
|
|
this.source.push("return buffer;");
|
|
|
|
var params = ["Handlebars", "context", "helpers", "partials"];
|
|
|
|
if(this.data) { params.push("data"); }
|
|
|
|
for(var i=0, l=this.environment.depths.list.length; i<l; i++) {
|
|
params.push("depth" + this.environment.depths.list[i]);
|
|
}
|
|
|
|
|
|
if(params.length === 4 && !this.environment.usePartial) { params.pop(); }
|
|
|
|
params.push(this.source.join("\n"));
|
|
|
|
var fn = Function.apply(this, params);
|
|
fn.displayName = "Handlebars.js";
|
|
|
|
Handlebars.log(Handlebars.logger.DEBUG, fn.toString() + "\n\n");
|
|
|
|
container.render = fn;
|
|
|
|
container.children = this.environment.children;
|
|
|
|
return function(context, helpers, partials, data, $depth) {
|
|
try {
|
|
var args = Array.prototype.slice.call(arguments);
|
|
args.unshift(Handlebars);
|
|
return container.render.apply(container, args);
|
|
} catch(e) {
|
|
throw e;
|
|
}
|
|
};
|
|
},
|
|
|
|
appendContent: function(content) {
|
|
this.source.push(this.appendToBuffer(this.quotedString(content)));
|
|
},
|
|
|
|
append: function() {
|
|
var local = this.popStack();
|
|
this.source.push("if(" + local + " || " + local + " === 0) { " + this.appendToBuffer(local) + " }");
|
|
},
|
|
|
|
appendEscaped: function() {
|
|
var opcode = this.nextOpcode(1), extra = "";
|
|
|
|
if(opcode[0] === 'appendContent') {
|
|
extra = " + " + this.quotedString(opcode[1][0]);
|
|
this.eat(opcode);
|
|
}
|
|
|
|
this.source.push(this.appendToBuffer("this.escapeExpression(" + this.popStack() + ")" + extra));
|
|
},
|
|
|
|
getContext: function(depth) {
|
|
if(this.lastContext !== depth) {
|
|
this.lastContext = depth;
|
|
|
|
if(depth === 0) {
|
|
this.source.push("currentContext = context;");
|
|
} else {
|
|
this.source.push("currentContext = depth" + depth + ";");
|
|
}
|
|
}
|
|
},
|
|
|
|
lookupWithFallback: function(name) {
|
|
if(name) {
|
|
this.pushStack(this.nameLookup('currentContext', name, 'context'));
|
|
var topStack = this.topStack();
|
|
this.source.push("if(" + topStack + " === undefined) { " + topStack + " = " + this.nameLookup('helpers', name, 'helper') + "; }");
|
|
} else {
|
|
this.pushStack("currentContext");
|
|
}
|
|
},
|
|
|
|
lookup: function(name) {
|
|
var topStack = this.topStack();
|
|
this.source.push(topStack + " = " + this.nameLookup(topStack, name, 'context') + ";");
|
|
},
|
|
|
|
pushString: function(string) {
|
|
this.pushStack(this.quotedString(string));
|
|
},
|
|
|
|
push: function(name) {
|
|
this.pushStack(name);
|
|
},
|
|
|
|
invokeMustache: function(paramSize, original) {
|
|
var params = ["context"], fn, slot;
|
|
|
|
fn = this.popStack();
|
|
|
|
for(var i=0; i<paramSize; i++) {
|
|
params.push(this.popStack());
|
|
}
|
|
|
|
if(this.data) { params.push("data"); }
|
|
|
|
var paramString = params.join(", ");
|
|
var helperMissing = ["context"].concat(this.quotedString(original)).concat(params.slice(1));
|
|
|
|
slot = this.nextStack();
|
|
|
|
if(paramSize === 0) {
|
|
// TODO: This case is not listed in the mustache spec. Is it important?
|
|
this.source.push("if(typeof " + fn + " === 'function') { " + slot + " = " + fn + ".call(" + paramString + "); }");
|
|
} else {
|
|
this.source.push("if(typeof " + fn + " === 'function') { " + slot + " = " + fn + ".call(" + paramString + "); } else { " + slot + " = helpers.helperMissing.call(" + helperMissing + ") }");
|
|
}
|
|
},
|
|
|
|
invokeProgram: function(guid, paramSize) {
|
|
var inverse = this.programExpression(this.inverse);
|
|
|
|
var fn = this.popStack();
|
|
var id = fn;
|
|
|
|
var params = ["context"];
|
|
var blockMissingParams = ["context", id];
|
|
|
|
for(var i=0; i<paramSize; i++) {
|
|
var param = this.popStack();
|
|
params.push(param);
|
|
blockMissingParams.push(param);
|
|
}
|
|
|
|
var mainProgram = this.programExpression(guid);
|
|
|
|
params.push(mainProgram, inverse);
|
|
blockMissingParams.push(mainProgram, inverse);
|
|
|
|
var nextStack = this.nextStack();
|
|
|
|
this.source.push("if(typeof " + id + " === 'function') { " + nextStack + " = " + id + ".call(" + params.join(", ") + "); }");
|
|
this.source.push("else { " + nextStack + " = helpers.blockHelperMissing.call(" + blockMissingParams.join(", ") + "); }");
|
|
},
|
|
|
|
invokeInverse: function(guid) {
|
|
var program = this.programExpression(guid);
|
|
|
|
var blockMissingParams = ["context", this.topStack(), "this.noop", program];
|
|
this.pushStack("helpers.blockHelperMissing.call(" + blockMissingParams.join(", ") + ")");
|
|
},
|
|
|
|
invokePartial: function(context) {
|
|
this.pushStack("this.invokePartial(" + this.nameLookup('partials', context, 'partial') + ", '" + context + "', " + this.popStack() + ", helpers, partials);");
|
|
},
|
|
|
|
// HELPERS
|
|
|
|
compileChildren: function(environment, data) {
|
|
var children = environment.children, child, compiler;
|
|
var compiled = [];
|
|
|
|
for(var i=0, l=children.length; i<l; i++) {
|
|
child = children[i];
|
|
compiler = new JavaScriptCompiler();
|
|
|
|
compiled[i] = compiler.compile(child, data);
|
|
}
|
|
|
|
environment.rawChildren = children;
|
|
environment.children = compiled;
|
|
},
|
|
|
|
programExpression: function(guid) {
|
|
if(guid == null) { return "this.noop"; }
|
|
|
|
var programParams = [guid, "helpers", "partials"];
|
|
|
|
var depths = this.environment.rawChildren[guid].depths.list;
|
|
|
|
if(this.data) { programParams.push("data"); }
|
|
|
|
for(var i=0, l = depths.length; i<l; i++) {
|
|
depth = depths[i];
|
|
|
|
if(depth === 1) { programParams.push("context"); }
|
|
else { programParams.push("depth" + (depth - 1)); }
|
|
}
|
|
|
|
if(!this.environment.usePartial) {
|
|
if(programParams[3]) {
|
|
programParams[2] = "null";
|
|
} else {
|
|
programParams.pop();
|
|
}
|
|
}
|
|
|
|
if(depths.length === 0) {
|
|
return "this.program(" + programParams.join(", ") + ")";
|
|
} else {
|
|
programParams[0] = "this.children[" + guid + "]";
|
|
return "this.programWithDepth(" + programParams.join(", ") + ")";
|
|
}
|
|
},
|
|
|
|
register: function(name, val) {
|
|
this.useRegister(name);
|
|
this.source.push(name + " = " + val + ";");
|
|
},
|
|
|
|
useRegister: function(name) {
|
|
if(!this.registers[name]) {
|
|
this.registers[name] = true;
|
|
this.registers.list.push(name);
|
|
}
|
|
},
|
|
|
|
pushStack: function(item) {
|
|
this.source.push(this.nextStack() + " = " + item + ";");
|
|
return "stack" + this.stackSlot;
|
|
},
|
|
|
|
nextStack: function() {
|
|
this.stackSlot++;
|
|
if(this.stackSlot > this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); }
|
|
return "stack" + this.stackSlot;
|
|
},
|
|
|
|
popStack: function() {
|
|
return "stack" + this.stackSlot--;
|
|
},
|
|
|
|
topStack: function() {
|
|
return "stack" + this.stackSlot;
|
|
},
|
|
|
|
quotedString: function(str) {
|
|
return '"' + str
|
|
.replace(/\\/, '\\\\')
|
|
.replace(/"/g, '\\"')
|
|
.replace(/\n/g, '\\n')
|
|
.replace(/\r/g, '\\r') + '"';
|
|
}
|
|
};
|
|
|
|
var reservedWords = ("break case catch continue default delete do else finally " +
|
|
"for function if in instanceof new return switch this throw " +
|
|
"try typeof var void while with null true false").split(" ");
|
|
|
|
compilerWords = JavaScriptCompiler.RESERVED_WORDS = {};
|
|
|
|
for(var i=0, l=reservedWords.length; i<l; i++) {
|
|
compilerWords[reservedWords[i]] = true;
|
|
}
|
|
|
|
})(Handlebars.Compiler, Handlebars.JavaScriptCompiler);
|
|
|
|
Handlebars.VM = {
|
|
programWithDepth: function(fn) {
|
|
var args = Array.prototype.slice.call(arguments, 1);
|
|
return function(context) {
|
|
return fn.apply(this, [context].concat(args));
|
|
};
|
|
},
|
|
program: function(fn, helpers, partials, data) {
|
|
return function(context) {
|
|
return fn(context, helpers, partials, data);
|
|
};
|
|
},
|
|
noop: function() { return ""; },
|
|
compile: function(string, data) {
|
|
var ast = Handlebars.parse(string);
|
|
var environment = new Handlebars.Compiler().compile(ast);
|
|
return new Handlebars.JavaScriptCompiler().compile(environment, data);
|
|
},
|
|
invokePartial: function(partial, name, context, helpers, partials) {
|
|
if(partial === undefined) {
|
|
throw new Handlebars.Exception("The partial " + name + " could not be found");
|
|
} else if(partial instanceof Function) {
|
|
return partial(context, helpers, partials);
|
|
} else {
|
|
partials[name] = Handlebars.VM.compile(partial);
|
|
return partials[name](context, helpers, partials);
|
|
}
|
|
}
|
|
};
|
|
|
|
Handlebars.compile = Handlebars.VM.compile;
|
|
// END(BROWSER)
|
|
|