484 lines
12 KiB
JavaScript
484 lines
12 KiB
JavaScript
import Exception from "../exception";
|
|
import {isArray} from "../utils";
|
|
|
|
var slice = [].slice;
|
|
|
|
|
|
// a mustache is definitely a helper if:
|
|
// * it is an eligible helper, and
|
|
// * it has at least one parameter or hash segment
|
|
function helperExpr(sexpr) {
|
|
return !!(sexpr.isHelper || sexpr.params.length || sexpr.hash);
|
|
}
|
|
|
|
function scopedId(path) {
|
|
return (/^\.|this\b/).test(path.original);
|
|
}
|
|
|
|
// an ID is simple if it only has one part, and that part is not
|
|
// `..` or `this`.
|
|
function simpleId(path) {
|
|
var part = path.parts[0];
|
|
|
|
return path.parts.length === 1 && !scopedId(path) && !path.depth;
|
|
}
|
|
|
|
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,
|
|
|
|
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 || !argEquals(opcode.args, otherOpcode.args)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// We know that length is the same between the two arrays because they are directly tied
|
|
// to the opcode behavior above.
|
|
len = this.children.length;
|
|
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.sourceNode = [];
|
|
this.opcodes = [];
|
|
this.children = [];
|
|
this.depths = {list: []};
|
|
this.options = options;
|
|
this.stringParams = options.stringParams;
|
|
this.trackIds = options.trackIds;
|
|
|
|
// 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,
|
|
'lookup': true
|
|
};
|
|
if (knownHelpers) {
|
|
for (var name in knownHelpers) {
|
|
this.options.knownHelpers[name] = knownHelpers[name];
|
|
}
|
|
}
|
|
|
|
return this.accept(program);
|
|
},
|
|
|
|
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;
|
|
},
|
|
|
|
accept: function(node) {
|
|
this.sourceNode.unshift(node);
|
|
var ret = this[node.type](node);
|
|
this.sourceNode.shift();
|
|
return ret;
|
|
},
|
|
|
|
Program: function(program) {
|
|
var body = program.body;
|
|
|
|
for(var i=0, l=body.length; i<l; i++) {
|
|
this.accept(body[i]);
|
|
}
|
|
|
|
this.isSimple = l === 1;
|
|
|
|
this.depths.list = this.depths.list.sort(function(a, b) {
|
|
return a - b;
|
|
});
|
|
|
|
return this;
|
|
},
|
|
|
|
BlockStatement: function(block) {
|
|
var sexpr = block.sexpr,
|
|
program = block.program,
|
|
inverse = block.inverse;
|
|
|
|
program = program && this.compileProgram(program);
|
|
inverse = inverse && this.compileProgram(inverse);
|
|
|
|
var type = this.classifySexpr(sexpr);
|
|
|
|
if (type === 'helper') {
|
|
this.helperSexpr(sexpr, program, inverse);
|
|
} else if (type === 'simple') {
|
|
this.simpleSexpr(sexpr);
|
|
|
|
// 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', sexpr.path.original);
|
|
} else {
|
|
this.ambiguousSexpr(sexpr, 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');
|
|
},
|
|
|
|
PartialStatement: function(partial) {
|
|
var partialName = partial.sexpr.path.original;
|
|
this.usePartial = true;
|
|
|
|
var params = partial.sexpr.params;
|
|
if (params.length > 1) {
|
|
throw new Exception('Unsupported number of partial arguments: ' + params.length, partial);
|
|
} else if (!params.length) {
|
|
params.push({type: 'PathExpression', parts: [], depth: 0});
|
|
}
|
|
|
|
this.setupFullMustacheParams(partial.sexpr, undefined, undefined, true);
|
|
|
|
var indent = partial.indent || '';
|
|
if (this.options.preventIndent && indent) {
|
|
this.opcode('appendContent', indent);
|
|
indent = '';
|
|
}
|
|
this.opcode('invokePartial', partialName, indent);
|
|
this.opcode('append');
|
|
},
|
|
|
|
MustacheStatement: function(mustache) {
|
|
this.accept(mustache.sexpr);
|
|
|
|
if(mustache.escaped && !this.options.noEscape) {
|
|
this.opcode('appendEscaped');
|
|
} else {
|
|
this.opcode('append');
|
|
}
|
|
},
|
|
|
|
ContentStatement: function(content) {
|
|
if (content.value) {
|
|
this.opcode('appendContent', content.value);
|
|
}
|
|
},
|
|
|
|
CommentStatement: function() {},
|
|
|
|
SubExpression: function(sexpr) {
|
|
var type = this.classifySexpr(sexpr);
|
|
|
|
if (type === 'simple') {
|
|
this.simpleSexpr(sexpr);
|
|
} else if (type === 'helper') {
|
|
this.helperSexpr(sexpr);
|
|
} else {
|
|
this.ambiguousSexpr(sexpr);
|
|
}
|
|
},
|
|
ambiguousSexpr: function(sexpr, program, inverse) {
|
|
var path = sexpr.path,
|
|
name = path.parts[0],
|
|
isBlock = program != null || inverse != null;
|
|
|
|
this.opcode('getContext', path.depth);
|
|
|
|
this.opcode('pushProgram', program);
|
|
this.opcode('pushProgram', inverse);
|
|
|
|
this.accept(path);
|
|
|
|
this.opcode('invokeAmbiguous', name, isBlock);
|
|
},
|
|
|
|
simpleSexpr: function(sexpr) {
|
|
this.accept(sexpr.path);
|
|
this.opcode('resolvePossibleLambda');
|
|
},
|
|
|
|
helperSexpr: function(sexpr, program, inverse) {
|
|
var params = this.setupFullMustacheParams(sexpr, program, inverse),
|
|
path = sexpr.path,
|
|
name = path.parts[0];
|
|
|
|
if (this.options.knownHelpers[name]) {
|
|
this.opcode('invokeKnownHelper', params.length, name);
|
|
} else if (this.options.knownHelpersOnly) {
|
|
throw new Exception("You specified knownHelpersOnly, but used the unknown helper " + name, sexpr);
|
|
} else {
|
|
path.falsy = true;
|
|
|
|
this.accept(path);
|
|
this.opcode('invokeHelper', params.length, path.original, simpleId(path));
|
|
}
|
|
},
|
|
|
|
PathExpression: function(path) {
|
|
this.addDepth(path.depth);
|
|
this.opcode('getContext', path.depth);
|
|
|
|
var name = path.parts[0];
|
|
if (!name) {
|
|
// Context reference, i.e. `{{foo .}}` or `{{foo ..}}`
|
|
this.opcode('pushContext');
|
|
} else if (path.data) {
|
|
this.options.data = true;
|
|
this.opcode('lookupData', path.depth, path.parts);
|
|
} else {
|
|
this.opcode('lookupOnContext', path.parts, path.falsy, scopedId(path));
|
|
}
|
|
},
|
|
|
|
StringLiteral: function(string) {
|
|
this.opcode('pushString', string.value);
|
|
},
|
|
|
|
NumberLiteral: function(number) {
|
|
this.opcode('pushLiteral', number.value);
|
|
},
|
|
|
|
BooleanLiteral: function(bool) {
|
|
this.opcode('pushLiteral', bool.value);
|
|
},
|
|
|
|
Hash: function(hash) {
|
|
var pairs = hash.pairs, i, l;
|
|
|
|
this.opcode('pushHash');
|
|
|
|
for (i=0, l=pairs.length; i<l; i++) {
|
|
this.pushParam(pairs[i].value);
|
|
}
|
|
while (i--) {
|
|
this.opcode('assignToHash', pairs[i].key);
|
|
}
|
|
this.opcode('popHash');
|
|
},
|
|
|
|
// HELPERS
|
|
opcode: function(name) {
|
|
this.opcodes.push({ opcode: name, args: slice.call(arguments, 1), loc: this.sourceNode[0].loc });
|
|
},
|
|
|
|
addDepth: function(depth) {
|
|
if(depth === 0) { return; }
|
|
|
|
if(!this.depths[depth]) {
|
|
this.depths[depth] = true;
|
|
this.depths.list.push(depth);
|
|
}
|
|
},
|
|
|
|
classifySexpr: function(sexpr) {
|
|
// a mustache is an eligible helper if:
|
|
// * its id is simple (a single part, not `this` or `..`)
|
|
var isHelper = helperExpr(sexpr);
|
|
|
|
// if a mustache is an eligible helper but not a definite
|
|
// helper, it is ambiguous, and will be resolved in a later
|
|
// pass or at runtime.
|
|
var isEligible = isHelper || simpleId(sexpr.path);
|
|
|
|
var options = this.options;
|
|
|
|
// if ambiguous, we can possibly resolve the ambiguity now
|
|
// An eligible helper is one that does not have a complex path, i.e. `this.foo`, `../foo` etc.
|
|
if (isEligible && !isHelper) {
|
|
var name = sexpr.path.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) {
|
|
for(var i=0, l=params.length; i<l; i++) {
|
|
this.pushParam(params[i]);
|
|
}
|
|
},
|
|
|
|
pushParam: function(val) {
|
|
var value = val.value != null ? val.value : val.original || '';
|
|
|
|
// Force helper evaluation
|
|
if (val.type === 'SubExpression') {
|
|
val.isHelper = true;
|
|
}
|
|
|
|
if (this.stringParams) {
|
|
if (value.replace) {
|
|
value = value
|
|
.replace(/^(\.?\.\/)*/g, '')
|
|
.replace(/\//g, '.');
|
|
}
|
|
|
|
if(val.depth) {
|
|
this.addDepth(val.depth);
|
|
}
|
|
this.opcode('getContext', val.depth || 0);
|
|
this.opcode('pushStringParam', value, val.type);
|
|
|
|
if (val.type === 'SubExpression') {
|
|
// SubExpressions get evaluated and passed in
|
|
// in string params mode.
|
|
this.accept(val);
|
|
}
|
|
} else {
|
|
if (this.trackIds) {
|
|
value = val.original || value;
|
|
if (value.replace) {
|
|
value = value
|
|
.replace(/^\.\//g, '')
|
|
.replace(/^\.$/g, '');
|
|
}
|
|
|
|
this.opcode('pushId', val.type, value);
|
|
}
|
|
this.accept(val);
|
|
}
|
|
},
|
|
|
|
setupFullMustacheParams: function(sexpr, program, inverse, omitEmpty) {
|
|
var params = sexpr.params;
|
|
this.pushParams(params);
|
|
|
|
this.opcode('pushProgram', program);
|
|
this.opcode('pushProgram', inverse);
|
|
|
|
if (sexpr.hash) {
|
|
this.accept(sexpr.hash);
|
|
} else {
|
|
this.opcode('emptyHash', omitEmpty);
|
|
}
|
|
|
|
return params;
|
|
}
|
|
};
|
|
|
|
export function precompile(input, options, env) {
|
|
if (input == null || (typeof input !== 'string' && input.type !== 'Program')) {
|
|
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;
|
|
}
|
|
if (options.compat) {
|
|
options.useDepths = true;
|
|
}
|
|
|
|
var ast = env.parse(input, options);
|
|
var environment = new env.Compiler().compile(ast, options);
|
|
return new env.JavaScriptCompiler().compile(environment, options);
|
|
}
|
|
|
|
export function compile(input, options, env) {
|
|
if (input == null || (typeof input !== 'string' && input.type !== 'Program')) {
|
|
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;
|
|
}
|
|
if (options.compat) {
|
|
options.useDepths = true;
|
|
}
|
|
|
|
var compiled;
|
|
|
|
function compileInput() {
|
|
var ast = env.parse(input, options);
|
|
var environment = new env.Compiler().compile(ast, options);
|
|
var templateSpec = new env.JavaScriptCompiler().compile(environment, options, undefined, true);
|
|
return env.template(templateSpec);
|
|
}
|
|
|
|
// Template is only compiled on first use and cached after that point.
|
|
var ret = function(context, options) {
|
|
if (!compiled) {
|
|
compiled = compileInput();
|
|
}
|
|
return compiled.call(this, context, options);
|
|
};
|
|
ret._setup = function(options) {
|
|
if (!compiled) {
|
|
compiled = compileInput();
|
|
}
|
|
return compiled._setup(options);
|
|
};
|
|
ret._child = function(i, data, depths) {
|
|
if (!compiled) {
|
|
compiled = compileInput();
|
|
}
|
|
return compiled._child(i, data, depths);
|
|
};
|
|
return ret;
|
|
}
|
|
|
|
function argEquals(a, b) {
|
|
if (a === b) {
|
|
return true;
|
|
}
|
|
|
|
if (isArray(a) && isArray(b) && a.length === b.length) {
|
|
for (var i = 0; i < a.length; i++) {
|
|
if (!argEquals(a[i], b[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|