diff options
Diffstat (limited to 'lib/handlebars/compiler/compiler.js')
-rw-r--r-- | lib/handlebars/compiler/compiler.js | 271 |
1 files changed, 145 insertions, 126 deletions
diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 1e5d07a..5ba0916 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -3,6 +3,26 @@ 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 @@ -74,12 +94,12 @@ Compiler.prototype = { return this[node.type](node); }, - program: function(program) { - var statements = program.statements; - - for(var i=0, l=statements.length; i<l; i++) { - this.accept(statements[i]); + 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) { @@ -107,24 +127,19 @@ Compiler.prototype = { return guid; }, - block: function(block) { + BlockStatement: function(block) { var sexpr = block.sexpr, program = block.program, inverse = block.inverse; - if (program) { - program = this.compileProgram(program); - } - - if (inverse) { - inverse = this.compileProgram(inverse); - } + program = program && this.compileProgram(program); + inverse = inverse && this.compileProgram(inverse); var type = this.classifySexpr(sexpr); - if (type === "helper") { + if (type === 'helper') { this.helperSexpr(sexpr, program, inverse); - } else if (type === "simple") { + } else if (type === 'simple') { this.simpleSexpr(sexpr); // now that the simple mustache is resolved, we need to @@ -132,7 +147,7 @@ Compiler.prototype = { this.opcode('pushProgram', block, program); this.opcode('pushProgram', block, inverse); this.opcode('emptyHash', block); - this.opcode('blockValue', block, sexpr.id.original); + this.opcode('blockValue', block, sexpr.path.original); } else { this.ambiguousSexpr(sexpr, program, inverse); @@ -147,54 +162,30 @@ Compiler.prototype = { this.opcode('append', block); }, - hash: function(hash) { - var pairs = hash.pairs, i, l; - - this.opcode('pushHash', hash); - - for(i=0, l=pairs.length; i<l; i++) { - this.pushParam(pairs[i][1]); - } - while(i--) { - this.opcode('assignToHash', hash, pairs[i][0]); - } - this.opcode('popHash', hash); - }, - - partial: function(partial) { - var partialName = partial.partialName; + PartialStatement: function(partial) { + var partialName = partial.sexpr.path.original; this.usePartial = true; - if (partial.hash) { - this.accept(partial.hash); - } else { - this.opcode('pushLiteral', partial, 'undefined'); + 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}); } - if (partial.context) { - this.accept(partial.context); - } else { - this.opcode('getContext', partial, 0); - this.opcode('pushContext', partial); - } + this.setupFullMustacheParams(partial.sexpr, undefined, undefined, true); var indent = partial.indent || ''; if (this.options.preventIndent && indent) { this.opcode('appendContent', partial, indent); indent = ''; } - this.opcode('invokePartial', partial, partialName.name, indent); + this.opcode('invokePartial', partial, partialName, indent); this.opcode('append', partial); }, - content: function(content) { - if (content.string) { - this.opcode('appendContent', content, content.string); - } - }, - - mustache: function(mustache) { - this.sexpr(mustache.sexpr); + MustacheStatement: function(mustache) { + this.accept(mustache.sexpr); if(mustache.escaped && !this.options.noEscape) { this.opcode('appendEscaped', mustache); @@ -203,106 +194,107 @@ Compiler.prototype = { } }, + ContentStatement: function(content) { + if (content.value) { + this.opcode('appendContent', content, 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 id = sexpr.id, - name = id.parts[0], + var path = sexpr.path, + name = path.parts[0], isBlock = program != null || inverse != null; - this.opcode('getContext', sexpr, id.depth); + this.opcode('getContext', sexpr, path.depth); this.opcode('pushProgram', sexpr, program); this.opcode('pushProgram', sexpr, inverse); - this.ID(id); + this.accept(path); this.opcode('invokeAmbiguous', sexpr, name, isBlock); }, simpleSexpr: function(sexpr) { - var id = sexpr.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', sexpr, id.depth); - this.opcode('pushContext', sexpr); - } - + this.accept(sexpr.path); this.opcode('resolvePossibleLambda', sexpr); }, helperSexpr: function(sexpr, program, inverse) { var params = this.setupFullMustacheParams(sexpr, program, inverse), - id = sexpr.id, - name = id.parts[0]; + path = sexpr.path, + name = path.parts[0]; if (this.options.knownHelpers[name]) { this.opcode('invokeKnownHelper', sexpr, params.length, name); } else if (this.options.knownHelpersOnly) { throw new Exception("You specified knownHelpersOnly, but used the unknown helper " + name, sexpr); } else { - id.falsy = true; + path.falsy = true; - this.ID(id); - this.opcode('invokeHelper', sexpr, params.length, id.original, id.isSimple); - } - }, - - sexpr: function(sexpr) { - var type = this.classifySexpr(sexpr); - - if (type === "simple") { - this.simpleSexpr(sexpr); - } else if (type === "helper") { - this.helperSexpr(sexpr); - } else { - this.ambiguousSexpr(sexpr); + this.accept(path); + this.opcode('invokeHelper', sexpr, params.length, path.original, simpleId(path)); } }, - ID: function(id) { - this.addDepth(id.depth); - this.opcode('getContext', id, id.depth); + PathExpression: function(path) { + this.addDepth(path.depth); + this.opcode('getContext', path, path.depth); - var name = id.parts[0]; + var name = path.parts[0]; if (!name) { // Context reference, i.e. `{{foo .}}` or `{{foo ..}}` - this.opcode('pushContext', id); + this.opcode('pushContext', path); + } else if (path.data) { + this.options.data = true; + this.opcode('lookupData', path, path.depth, path.parts); } else { - this.opcode('lookupOnContext', id, id.parts, id.falsy, id.isScoped); + this.opcode('lookupOnContext', path, path.parts, path.falsy, scopedId(path)); } }, - DATA: function(data) { - this.options.data = true; - this.opcode('lookupData', data, data.id.depth, data.id.parts); + StringLiteral: function(string) { + this.opcode('pushString', string, string.value); }, - STRING: function(string) { - this.opcode('pushString', string, string.string); + NumberLiteral: function(number) { + this.opcode('pushLiteral', number, number.value); }, - NUMBER: function(number) { - this.opcode('pushLiteral', number, number.number); + BooleanLiteral: function(bool) { + this.opcode('pushLiteral', bool, bool.value); }, - BOOLEAN: function(bool) { - this.opcode('pushLiteral', bool, bool.bool); - }, + Hash: function(hash) { + var pairs = hash.pairs, i, l; + + this.opcode('pushHash', hash); - comment: function() {}, + for (i=0, l=pairs.length; i<l; i++) { + this.pushParam(pairs[i].value); + } + while (i--) { + this.opcode('assignToHash', hash, pairs[i].key); + } + this.opcode('popHash', hash); + }, // HELPERS opcode: function(name, node) { - var loc = { - firstLine: node.firstLine, firstColumn: node.firstColumn, - lastLine: node.lastLine, lastColumn: node.lastColumn - }; - this.opcodes.push({ opcode: name, args: slice.call(arguments, 2), loc: loc }); + this.opcodes.push({ opcode: name, args: slice.call(arguments, 2), loc: node.loc }); }, addDepth: function(depth) { @@ -315,14 +307,21 @@ Compiler.prototype = { }, classifySexpr: function(sexpr) { - var isHelper = sexpr.isHelper; - var isEligible = sexpr.eligibleHelper; - var options = this.options; + // 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.id.parts[0]; + var name = sexpr.path.parts[0]; if (options.knownHelpers[name]) { isHelper = true; @@ -331,9 +330,9 @@ Compiler.prototype = { } } - if (isHelper) { return "helper"; } - else if (isEligible) { return "ambiguous"; } - else { return "simple"; } + if (isHelper) { return 'helper'; } + else if (isEligible) { return 'ambiguous'; } + else { return 'simple'; } }, pushParams: function(params) { @@ -343,27 +342,47 @@ Compiler.prototype = { }, 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, val.depth || 0); - this.opcode('pushStringParam', val, val.stringModeValue, val.type); + this.opcode('pushStringParam', val, value, val.type); - if (val.type === 'sexpr') { - // Subexpressions get evaluated and passed in + if (val.type === 'SubExpression') { + // SubExpressions get evaluated and passed in // in string params mode. - this.sexpr(val); + this.accept(val); } } else { if (this.trackIds) { - this.opcode('pushId', val, val.type, val.idName || val.stringModeValue); + value = val.original || value; + if (value.replace) { + value = value + .replace(/^\.\//g, '') + .replace(/^\.$/g, ''); + } + + this.opcode('pushId', val, val.type, value); } this.accept(val); } }, - setupFullMustacheParams: function(sexpr, program, inverse) { + setupFullMustacheParams: function(sexpr, program, inverse, omitEmpty) { var params = sexpr.params; this.pushParams(params); @@ -371,9 +390,9 @@ Compiler.prototype = { this.opcode('pushProgram', sexpr, inverse); if (sexpr.hash) { - this.hash(sexpr.hash); + this.accept(sexpr.hash); } else { - this.opcode('emptyHash', sexpr); + this.opcode('emptyHash', sexpr, omitEmpty); } return params; @@ -381,7 +400,7 @@ Compiler.prototype = { }; export function precompile(input, options, env) { - if (input == null || (typeof input !== 'string' && input.constructor !== env.AST.ProgramNode)) { + 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); } @@ -393,13 +412,13 @@ export function precompile(input, options, env) { options.useDepths = true; } - var ast = env.parse(input); + 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.constructor !== env.AST.ProgramNode)) { + 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); } @@ -415,7 +434,7 @@ export function compile(input, options, env) { var compiled; function compileInput() { - var ast = env.parse(input); + 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); |