5f664dd78b
The idea is that the environment wraps up the mutable stuff in Handlebars (like the helpers) and that you could theoretically create a new one at any time and pass it in to Handlebars.template. Every test makes a new environment and uses it in template compilation.
461 lines
12 KiB
JavaScript
461 lines
12 KiB
JavaScript
import { Exception } from "../utils";
|
|
import { template } from "../runtime";
|
|
import { parse } from "./base";
|
|
import { JavaScriptCompiler } from "./javascript-compiler";
|
|
import AST from "./ast";
|
|
|
|
/*jshint eqnull:true*/
|
|
|
|
export function Compiler() {};
|
|
|
|
// the foundHelper register will disambiguate helper lookup from finding a
|
|
// function in a context. This is necessary for mustache compatibility, which
|
|
// requires that context functions in blocks are evaluated by blockHelperMissing,
|
|
// and then proceed as if the resulting value was provided to blockHelperMissing.
|
|
|
|
Compiler.prototype = {
|
|
compiler: Compiler,
|
|
|
|
disassemble: function() {
|
|
var opcodes = this.opcodes, opcode, out = [], params, param;
|
|
|
|
for (var i=0, l=opcodes.length; i<l; i++) {
|
|
opcode = opcodes[i];
|
|
|
|
if (opcode.opcode === 'DECLARE') {
|
|
out.push("DECLARE " + opcode.name + "=" + opcode.value);
|
|
} else {
|
|
params = [];
|
|
for (var j=0; j<opcode.args.length; j++) {
|
|
param = opcode.args[j];
|
|
if (typeof param === "string") {
|
|
param = "\"" + param.replace("\n", "\\n") + "\"";
|
|
}
|
|
params.push(param);
|
|
}
|
|
out.push(opcode.opcode + " " + params.join(" "));
|
|
}
|
|
}
|
|
|
|
return out.join("\n");
|
|
},
|
|
|
|
equals: function(other) {
|
|
var len = this.opcodes.length;
|
|
if (other.opcodes.length !== len) {
|
|
return false;
|
|
}
|
|
|
|
for (var i = 0; i < len; i++) {
|
|
var opcode = this.opcodes[i],
|
|
otherOpcode = other.opcodes[i];
|
|
if (opcode.opcode !== otherOpcode.opcode || opcode.args.length !== otherOpcode.args.length) {
|
|
return false;
|
|
}
|
|
for (var j = 0; j < opcode.args.length; j++) {
|
|
if (opcode.args[j] !== otherOpcode.args[j]) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
len = this.children.length;
|
|
if (other.children.length !== len) {
|
|
return false;
|
|
}
|
|
for (i = 0; i < len; i++) {
|
|
if (!this.children[i].equals(other.children[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
guid: 0,
|
|
|
|
compile: function(program, options) {
|
|
this.children = [];
|
|
this.depths = {list: []};
|
|
this.options = options;
|
|
|
|
// These changes will propagate to the other compiler components
|
|
var knownHelpers = this.options.knownHelpers;
|
|
this.options.knownHelpers = {
|
|
'helperMissing': true,
|
|
'blockHelperMissing': true,
|
|
'each': true,
|
|
'if': true,
|
|
'unless': true,
|
|
'with': true,
|
|
'log': true
|
|
};
|
|
if (knownHelpers) {
|
|
for (var name in knownHelpers) {
|
|
this.options.knownHelpers[name] = knownHelpers[name];
|
|
}
|
|
}
|
|
|
|
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.isSimple = l === 1;
|
|
|
|
this.depths.list = this.depths.list.sort(function(a, b) {
|
|
return a - b;
|
|
});
|
|
|
|
return this;
|
|
},
|
|
|
|
compileProgram: function(program) {
|
|
var result = new this.compiler().compile(program, this.options);
|
|
var guid = this.guid++, depth;
|
|
|
|
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,
|
|
program = block.program,
|
|
inverse = block.inverse;
|
|
|
|
if (program) {
|
|
program = this.compileProgram(program);
|
|
}
|
|
|
|
if (inverse) {
|
|
inverse = this.compileProgram(inverse);
|
|
}
|
|
|
|
var type = this.classifyMustache(mustache);
|
|
|
|
if (type === "helper") {
|
|
this.helperMustache(mustache, program, inverse);
|
|
} else if (type === "simple") {
|
|
this.simpleMustache(mustache);
|
|
|
|
// now that the simple mustache is resolved, we need to
|
|
// evaluate it by executing `blockHelperMissing`
|
|
this.opcode('pushProgram', program);
|
|
this.opcode('pushProgram', inverse);
|
|
this.opcode('emptyHash');
|
|
this.opcode('blockValue');
|
|
} else {
|
|
this.ambiguousMustache(mustache, program, inverse);
|
|
|
|
// now that the simple mustache is resolved, we need to
|
|
// evaluate it by executing `blockHelperMissing`
|
|
this.opcode('pushProgram', program);
|
|
this.opcode('pushProgram', inverse);
|
|
this.opcode('emptyHash');
|
|
this.opcode('ambiguousBlockValue');
|
|
}
|
|
|
|
this.opcode('append');
|
|
},
|
|
|
|
hash: function(hash) {
|
|
var pairs = hash.pairs, pair, val;
|
|
|
|
this.opcode('pushHash');
|
|
|
|
for(var i=0, l=pairs.length; i<l; i++) {
|
|
pair = pairs[i];
|
|
val = pair[1];
|
|
|
|
if (this.options.stringParams) {
|
|
if(val.depth) {
|
|
this.addDepth(val.depth);
|
|
}
|
|
this.opcode('getContext', val.depth || 0);
|
|
this.opcode('pushStringParam', val.stringModeValue, val.type);
|
|
} else {
|
|
this.accept(val);
|
|
}
|
|
|
|
this.opcode('assignToHash', pair[0]);
|
|
}
|
|
this.opcode('popHash');
|
|
},
|
|
|
|
partial: function(partial) {
|
|
var partialName = partial.partialName;
|
|
this.usePartial = true;
|
|
|
|
if(partial.context) {
|
|
this.ID(partial.context);
|
|
} else {
|
|
this.opcode('push', 'depth0');
|
|
}
|
|
|
|
this.opcode('invokePartial', partialName.name);
|
|
this.opcode('append');
|
|
},
|
|
|
|
content: function(content) {
|
|
this.opcode('appendContent', content.string);
|
|
},
|
|
|
|
mustache: function(mustache) {
|
|
var options = this.options;
|
|
var type = this.classifyMustache(mustache);
|
|
|
|
if (type === "simple") {
|
|
this.simpleMustache(mustache);
|
|
} else if (type === "helper") {
|
|
this.helperMustache(mustache);
|
|
} else {
|
|
this.ambiguousMustache(mustache);
|
|
}
|
|
|
|
if(mustache.escaped && !options.noEscape) {
|
|
this.opcode('appendEscaped');
|
|
} else {
|
|
this.opcode('append');
|
|
}
|
|
},
|
|
|
|
ambiguousMustache: function(mustache, program, inverse) {
|
|
var id = mustache.id,
|
|
name = id.parts[0],
|
|
isBlock = program != null || inverse != null;
|
|
|
|
this.opcode('getContext', id.depth);
|
|
|
|
this.opcode('pushProgram', program);
|
|
this.opcode('pushProgram', inverse);
|
|
|
|
this.opcode('invokeAmbiguous', name, isBlock);
|
|
},
|
|
|
|
simpleMustache: function(mustache) {
|
|
var id = mustache.id;
|
|
|
|
if (id.type === 'DATA') {
|
|
this.DATA(id);
|
|
} else if (id.parts.length) {
|
|
this.ID(id);
|
|
} else {
|
|
// Simplified ID for `this`
|
|
this.addDepth(id.depth);
|
|
this.opcode('getContext', id.depth);
|
|
this.opcode('pushContext');
|
|
}
|
|
|
|
this.opcode('resolvePossibleLambda');
|
|
},
|
|
|
|
helperMustache: function(mustache, program, inverse) {
|
|
var params = this.setupFullMustacheParams(mustache, program, inverse),
|
|
name = mustache.id.parts[0];
|
|
|
|
if (this.options.knownHelpers[name]) {
|
|
this.opcode('invokeKnownHelper', params.length, name);
|
|
} else if (this.options.knownHelpersOnly) {
|
|
throw new Error("You specified knownHelpersOnly, but used the unknown helper " + name);
|
|
} else {
|
|
this.opcode('invokeHelper', params.length, name);
|
|
}
|
|
},
|
|
|
|
ID: function(id) {
|
|
this.addDepth(id.depth);
|
|
this.opcode('getContext', id.depth);
|
|
|
|
var name = id.parts[0];
|
|
if (!name) {
|
|
this.opcode('pushContext');
|
|
} else {
|
|
this.opcode('lookupOnContext', id.parts[0]);
|
|
}
|
|
|
|
for(var i=1, l=id.parts.length; i<l; i++) {
|
|
this.opcode('lookup', id.parts[i]);
|
|
}
|
|
},
|
|
|
|
DATA: function(data) {
|
|
this.options.data = true;
|
|
if (data.id.isScoped || data.id.depth) {
|
|
throw new Exception('Scoped data references are not supported: ' + data.original);
|
|
}
|
|
|
|
this.opcode('lookupData');
|
|
var parts = data.id.parts;
|
|
for(var i=0, l=parts.length; i<l; i++) {
|
|
this.opcode('lookup', parts[i]);
|
|
}
|
|
},
|
|
|
|
STRING: function(string) {
|
|
this.opcode('pushString', string.string);
|
|
},
|
|
|
|
INTEGER: function(integer) {
|
|
this.opcode('pushLiteral', integer.integer);
|
|
},
|
|
|
|
BOOLEAN: function(bool) {
|
|
this.opcode('pushLiteral', bool.bool);
|
|
},
|
|
|
|
comment: function() {},
|
|
|
|
// HELPERS
|
|
opcode: function(name) {
|
|
this.opcodes.push({ opcode: name, args: [].slice.call(arguments, 1) });
|
|
},
|
|
|
|
declare: function(name, value) {
|
|
this.opcodes.push({ opcode: 'DECLARE', name: name, value: value });
|
|
},
|
|
|
|
addDepth: function(depth) {
|
|
if(isNaN(depth)) { throw new Error("EWOT"); }
|
|
if(depth === 0) { return; }
|
|
|
|
if(!this.depths[depth]) {
|
|
this.depths[depth] = true;
|
|
this.depths.list.push(depth);
|
|
}
|
|
},
|
|
|
|
classifyMustache: function(mustache) {
|
|
var isHelper = mustache.isHelper;
|
|
var isEligible = mustache.eligibleHelper;
|
|
var options = this.options;
|
|
|
|
// if ambiguous, we can possibly resolve the ambiguity now
|
|
if (isEligible && !isHelper) {
|
|
var name = mustache.id.parts[0];
|
|
|
|
if (options.knownHelpers[name]) {
|
|
isHelper = true;
|
|
} else if (options.knownHelpersOnly) {
|
|
isEligible = false;
|
|
}
|
|
}
|
|
|
|
if (isHelper) { return "helper"; }
|
|
else if (isEligible) { return "ambiguous"; }
|
|
else { return "simple"; }
|
|
},
|
|
|
|
pushParams: function(params) {
|
|
var i = params.length, param;
|
|
|
|
while(i--) {
|
|
param = params[i];
|
|
|
|
if(this.options.stringParams) {
|
|
if(param.depth) {
|
|
this.addDepth(param.depth);
|
|
}
|
|
|
|
this.opcode('getContext', param.depth || 0);
|
|
this.opcode('pushStringParam', param.stringModeValue, param.type);
|
|
} else {
|
|
this[param.type](param);
|
|
}
|
|
}
|
|
},
|
|
|
|
setupMustacheParams: function(mustache) {
|
|
var params = mustache.params;
|
|
this.pushParams(params);
|
|
|
|
if(mustache.hash) {
|
|
this.hash(mustache.hash);
|
|
} else {
|
|
this.opcode('emptyHash');
|
|
}
|
|
|
|
return params;
|
|
},
|
|
|
|
// this will replace setupMustacheParams when we're done
|
|
setupFullMustacheParams: function(mustache, program, inverse) {
|
|
var params = mustache.params;
|
|
this.pushParams(params);
|
|
|
|
this.opcode('pushProgram', program);
|
|
this.opcode('pushProgram', inverse);
|
|
|
|
if(mustache.hash) {
|
|
this.hash(mustache.hash);
|
|
} else {
|
|
this.opcode('emptyHash');
|
|
}
|
|
|
|
return params;
|
|
}
|
|
};
|
|
|
|
export function precompile(input, options) {
|
|
if (input == null || (typeof input !== 'string' && input.constructor !== AST.ProgramNode)) {
|
|
throw new Exception("You must pass a string or Handlebars AST to Handlebars.precompile. You passed " + input);
|
|
}
|
|
|
|
options = options || {};
|
|
if (!('data' in options)) {
|
|
options.data = true;
|
|
}
|
|
|
|
var ast = parse(input);
|
|
var environment = new Compiler().compile(ast, options);
|
|
return new JavaScriptCompiler().compile(environment, options);
|
|
};
|
|
|
|
export function compile(input, options) {
|
|
if (input == null || (typeof input !== 'string' && input.constructor !== AST.ProgramNode)) {
|
|
throw new Exception("You must pass a string or Handlebars AST to Handlebars.compile. You passed " + input);
|
|
}
|
|
|
|
options = options || {};
|
|
|
|
if (!('data' in options)) {
|
|
options.data = true;
|
|
}
|
|
|
|
var compiled;
|
|
|
|
function compileInput() {
|
|
var ast = parse(input);
|
|
var environment = new Compiler().compile(ast, options);
|
|
var templateSpec = new JavaScriptCompiler().compile(environment, options, undefined, true);
|
|
return template(templateSpec, options.env || Handlebars, compile);
|
|
}
|
|
|
|
// Template is only compiled on first use and cached after that point.
|
|
return function(context, options) {
|
|
if (!compiled) {
|
|
compiled = compileInput();
|
|
}
|
|
return compiled.call(this, context, options);
|
|
};
|
|
};
|