diff --git a/.jshintrc b/.jshintrc index ecd80acfc..bb29c76e8 100644 --- a/.jshintrc +++ b/.jshintrc @@ -35,7 +35,7 @@ "evil": true, "forin": false, "immed": false, - "laxbreak": false, + "laxbreak": true, "newcap": true, "noarg": true, "noempty": false, diff --git a/bench/templates/subexpression.js b/bench/templates/subexpression.js new file mode 100644 index 000000000..261c22d01 --- /dev/null +++ b/bench/templates/subexpression.js @@ -0,0 +1,14 @@ +module.exports = { + helpers: { + echo: function(value) { + return 'foo ' + value; + }, + header: function() { + return "Colors"; + } + }, + handlebars: "{{echo (header)}}", + eco: "<%= @echo(@header()) %>" +}; + +module.exports.context = module.exports.helpers; diff --git a/components/bower.json b/components/bower.json index b55ebc502..25923a475 100644 --- a/components/bower.json +++ b/components/bower.json @@ -1,6 +1,6 @@ { "name": "handlebars", - "version": "1.2.1", + "version": "1.3.0", "main": "handlebars.js", "dependencies": {} } diff --git a/components/handlebars.js.nuspec b/components/handlebars.js.nuspec index 610ca2f31..e4a169103 100644 --- a/components/handlebars.js.nuspec +++ b/components/handlebars.js.nuspec @@ -2,7 +2,7 @@ handlebars.js - 1.2.1 + 1.3.0 handlebars.js Authors https://github.com/wycats/handlebars.js/blob/master/LICENSE https://github.com/wycats/handlebars.js/ diff --git a/lib/handlebars.js b/lib/handlebars.js index c8956b401..ffa9c7ab2 100644 --- a/lib/handlebars.js +++ b/lib/handlebars.js @@ -14,7 +14,9 @@ var create = function() { hb.compile = function(input, options) { return compile(input, options, hb); }; - hb.precompile = precompile; + hb.precompile = function (input, options) { + return precompile(input, options, hb); + }; hb.AST = AST; hb.Compiler = Compiler; diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js index 027f05254..00c8bf1ff 100644 --- a/lib/handlebars/base.js +++ b/lib/handlebars/base.js @@ -1,7 +1,7 @@ module Utils from "./utils"; import Exception from "./exception"; -export var VERSION = "1.2.1"; +export var VERSION = "1.3.0"; export var COMPILER_REVISION = 4; export var REVISION_CHANGES = { @@ -53,7 +53,7 @@ function registerDefaultHelpers(instance) { if(arguments.length === 2) { return undefined; } else { - throw new Error("Missing helper: '" + arg + "'"); + throw new Exception("Missing helper: '" + arg + "'"); } }); diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 64cd3b7d2..002fd3be6 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -1,22 +1,51 @@ import Exception from "../exception"; +function LocationInfo(locInfo){ + locInfo = locInfo || {}; + this.firstLine = locInfo.first_line; + this.firstColumn = locInfo.first_column; + this.lastColumn = locInfo.last_column; + this.lastLine = locInfo.last_line; +} + var AST = { - ProgramNode: function(statements, inverseStrip, inverse) { + ProgramNode: function(statements, inverseStrip, inverse, locInfo) { + var inverseLocationInfo, firstInverseNode; + if (arguments.length === 3) { + locInfo = inverse; + inverse = null; + } else if (arguments.length === 2) { + locInfo = inverseStrip; + inverseStrip = null; + } + + LocationInfo.call(this, locInfo); this.type = "program"; this.statements = statements; this.strip = {}; if(inverse) { - this.inverse = new AST.ProgramNode(inverse, inverseStrip); + firstInverseNode = inverse[0]; + if (firstInverseNode) { + inverseLocationInfo = { + first_line: firstInverseNode.firstLine, + last_line: firstInverseNode.lastLine, + last_column: firstInverseNode.lastColumn, + first_column: firstInverseNode.firstColumn + }; + this.inverse = new AST.ProgramNode(inverse, inverseStrip, inverseLocationInfo); + } else { + this.inverse = new AST.ProgramNode(inverse, inverseStrip); + } this.strip.right = inverseStrip.left; } else if (inverseStrip) { this.strip.left = inverseStrip.right; } }, - MustacheNode: function(rawParams, hash, open, strip) { + MustacheNode: function(rawParams, hash, open, strip, locInfo) { + LocationInfo.call(this, locInfo); this.type = "mustache"; - this.hash = hash; this.strip = strip; // Open may be a string parsed from the parser or a passed boolean flag @@ -28,6 +57,29 @@ var AST = { this.escaped = !!open; } + if (rawParams instanceof AST.SexprNode) { + this.sexpr = rawParams; + } else { + // Support old AST API + this.sexpr = new AST.SexprNode(rawParams, hash); + } + + this.sexpr.isRoot = true; + + // Support old AST API that stored this info in MustacheNode + this.id = this.sexpr.id; + this.params = this.sexpr.params; + this.hash = this.sexpr.hash; + this.eligibleHelper = this.sexpr.eligibleHelper; + this.isHelper = this.sexpr.isHelper; + }, + + SexprNode: function(rawParams, hash, locInfo) { + LocationInfo.call(this, locInfo); + + this.type = "sexpr"; + this.hash = hash; + var id = this.id = rawParams[0]; var params = this.params = rawParams.slice(1); @@ -45,19 +97,22 @@ var AST = { // pass or at runtime. }, - PartialNode: function(partialName, context, strip) { + PartialNode: function(partialName, context, strip, locInfo) { + LocationInfo.call(this, locInfo); this.type = "partial"; this.partialName = partialName; this.context = context; this.strip = strip; }, - BlockNode: function(mustache, program, inverse, close) { - if(mustache.id.original !== close.path.original) { - throw new Exception(mustache.id.original + " doesn't match " + close.path.original); + BlockNode: function(mustache, program, inverse, close, locInfo) { + LocationInfo.call(this, locInfo); + + if(mustache.sexpr.id.original !== close.path.original) { + throw new Exception(mustache.sexpr.id.original + " doesn't match " + close.path.original, this); } - this.type = "block"; + this.type = 'block'; this.mustache = mustache; this.program = program; this.inverse = inverse; @@ -75,17 +130,20 @@ var AST = { } }, - ContentNode: function(string) { + ContentNode: function(string, locInfo) { + LocationInfo.call(this, locInfo); this.type = "content"; this.string = string; }, - HashNode: function(pairs) { + HashNode: function(pairs, locInfo) { + LocationInfo.call(this, locInfo); this.type = "hash"; this.pairs = pairs; }, - IdNode: function(parts) { + IdNode: function(parts, locInfo) { + LocationInfo.call(this, locInfo); this.type = "ID"; var original = "", @@ -97,11 +155,16 @@ var AST = { original += (parts[i].separator || '') + part; if (part === ".." || part === "." || part === "this") { - if (dig.length > 0) { throw new Exception("Invalid path: " + original); } - else if (part === "..") { depth++; } - else { this.isScoped = true; } + if (dig.length > 0) { + throw new Exception("Invalid path: " + original, this); + } else if (part === "..") { + depth++; + } else { + this.isScoped = true; + } + } else { + dig.push(part); } - else { dig.push(part); } } this.original = original; @@ -116,37 +179,43 @@ var AST = { this.stringModeValue = this.string; }, - PartialNameNode: function(name) { + PartialNameNode: function(name, locInfo) { + LocationInfo.call(this, locInfo); this.type = "PARTIAL_NAME"; this.name = name.original; }, - DataNode: function(id) { + DataNode: function(id, locInfo) { + LocationInfo.call(this, locInfo); this.type = "DATA"; this.id = id; }, - StringNode: function(string) { + StringNode: function(string, locInfo) { + LocationInfo.call(this, locInfo); this.type = "STRING"; this.original = this.string = this.stringModeValue = string; }, - IntegerNode: function(integer) { + IntegerNode: function(integer, locInfo) { + LocationInfo.call(this, locInfo); this.type = "INTEGER"; this.original = this.integer = integer; this.stringModeValue = Number(integer); }, - BooleanNode: function(bool) { + BooleanNode: function(bool, locInfo) { + LocationInfo.call(this, locInfo); this.type = "BOOLEAN"; this.bool = bool; this.stringModeValue = bool === "true"; }, - CommentNode: function(comment) { + CommentNode: function(comment, locInfo) { + LocationInfo.call(this, locInfo); this.type = "comment"; this.comment = comment; } diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index daea3072c..461f98b65 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -1,7 +1,4 @@ import Exception from "../exception"; -import { parse } from "./base"; -import JavaScriptCompiler from "./javascript-compiler"; -import AST from "./ast"; export function Compiler() {} @@ -159,12 +156,13 @@ Compiler.prototype = { inverse = this.compileProgram(inverse); } - var type = this.classifyMustache(mustache); + var sexpr = mustache.sexpr; + var type = this.classifySexpr(sexpr); if (type === "helper") { - this.helperMustache(mustache, program, inverse); + this.helperSexpr(sexpr, program, inverse); } else if (type === "simple") { - this.simpleMustache(mustache); + this.simpleSexpr(sexpr); // now that the simple mustache is resolved, we need to // evaluate it by executing `blockHelperMissing` @@ -173,7 +171,7 @@ Compiler.prototype = { this.opcode('emptyHash'); this.opcode('blockValue'); } else { - this.ambiguousMustache(mustache, program, inverse); + this.ambiguousSexpr(sexpr, program, inverse); // now that the simple mustache is resolved, we need to // evaluate it by executing `blockHelperMissing` @@ -201,6 +199,12 @@ Compiler.prototype = { } this.opcode('getContext', val.depth || 0); this.opcode('pushStringParam', val.stringModeValue, val.type); + + if (val.type === 'sexpr') { + // Subexpressions get evaluated and passed in + // in string params mode. + this.sexpr(val); + } } else { this.accept(val); } @@ -229,26 +233,17 @@ Compiler.prototype = { }, 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); - } + this.sexpr(mustache.sexpr); - if(mustache.escaped && !options.noEscape) { + if(mustache.escaped && !this.options.noEscape) { this.opcode('appendEscaped'); } else { this.opcode('append'); } }, - ambiguousMustache: function(mustache, program, inverse) { - var id = mustache.id, + ambiguousSexpr: function(sexpr, program, inverse) { + var id = sexpr.id, name = id.parts[0], isBlock = program != null || inverse != null; @@ -260,8 +255,8 @@ Compiler.prototype = { this.opcode('invokeAmbiguous', name, isBlock); }, - simpleMustache: function(mustache) { - var id = mustache.id; + simpleSexpr: function(sexpr) { + var id = sexpr.id; if (id.type === 'DATA') { this.DATA(id); @@ -277,16 +272,28 @@ Compiler.prototype = { this.opcode('resolvePossibleLambda'); }, - helperMustache: function(mustache, program, inverse) { - var params = this.setupFullMustacheParams(mustache, program, inverse), - name = mustache.id.parts[0]; + helperSexpr: function(sexpr, program, inverse) { + var params = this.setupFullMustacheParams(sexpr, program, inverse), + name = sexpr.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); + throw new Exception("You specified knownHelpersOnly, but used the unknown helper " + name, sexpr); } else { - this.opcode('invokeHelper', params.length, name); + this.opcode('invokeHelper', params.length, name, sexpr.isRoot); + } + }, + + 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); } }, @@ -309,7 +316,7 @@ Compiler.prototype = { 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); + throw new Exception('Scoped data references are not supported: ' + data.original, data); } this.opcode('lookupData'); @@ -343,7 +350,6 @@ Compiler.prototype = { }, addDepth: function(depth) { - if(isNaN(depth)) { throw new Error("EWOT"); } if(depth === 0) { return; } if(!this.depths[depth]) { @@ -352,14 +358,14 @@ Compiler.prototype = { } }, - classifyMustache: function(mustache) { - var isHelper = mustache.isHelper; - var isEligible = mustache.eligibleHelper; + classifySexpr: function(sexpr) { + var isHelper = sexpr.isHelper; + var isEligible = sexpr.eligibleHelper; var options = this.options; // if ambiguous, we can possibly resolve the ambiguity now if (isEligible && !isHelper) { - var name = mustache.id.parts[0]; + var name = sexpr.id.parts[0]; if (options.knownHelpers[name]) { isHelper = true; @@ -386,35 +392,27 @@ Compiler.prototype = { this.opcode('getContext', param.depth || 0); this.opcode('pushStringParam', param.stringModeValue, param.type); + + if (param.type === 'sexpr') { + // Subexpressions get evaluated and passed in + // in string params mode. + this.sexpr(param); + } } 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; + setupFullMustacheParams: function(sexpr, program, inverse) { + var params = sexpr.params; this.pushParams(params); this.opcode('pushProgram', program); this.opcode('pushProgram', inverse); - if(mustache.hash) { - this.hash(mustache.hash); + if (sexpr.hash) { + this.hash(sexpr.hash); } else { this.opcode('emptyHash'); } @@ -423,8 +421,8 @@ Compiler.prototype = { } }; -export function precompile(input, options) { - if (input == null || (typeof input !== 'string' && input.constructor !== AST.ProgramNode)) { +export function precompile(input, options, env) { + if (input == null || (typeof input !== 'string' && input.constructor !== env.AST.ProgramNode)) { throw new Exception("You must pass a string or Handlebars AST to Handlebars.precompile. You passed " + input); } @@ -433,13 +431,13 @@ export function precompile(input, options) { options.data = true; } - var ast = parse(input); - var environment = new Compiler().compile(ast, options); - return new JavaScriptCompiler().compile(environment, options); + var ast = env.parse(input); + 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 !== AST.ProgramNode)) { + if (input == null || (typeof input !== 'string' && input.constructor !== env.AST.ProgramNode)) { throw new Exception("You must pass a string or Handlebars AST to Handlebars.compile. You passed " + input); } @@ -452,9 +450,9 @@ export function compile(input, options, env) { var compiled; function compileInput() { - var ast = parse(input); - var environment = new Compiler().compile(ast, options); - var templateSpec = new JavaScriptCompiler().compile(environment, options, undefined, true); + var ast = env.parse(input); + var environment = new env.Compiler().compile(ast, options); + var templateSpec = new env.JavaScriptCompiler().compile(environment, options, undefined, true); return env.template(templateSpec); } diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 159a38bfc..75390685f 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -1,4 +1,5 @@ import { COMPILER_REVISION, REVISION_CHANGES, log } from "../base"; +import Exception from "../exception"; function Literal(value) { this.value = value; @@ -76,6 +77,7 @@ JavaScriptCompiler.prototype = { this.stackSlot = 0; this.stackVars = []; this.registers = { list: [] }; + this.hashes = []; this.compileStack = []; this.inlineStack = []; @@ -103,6 +105,10 @@ JavaScriptCompiler.prototype = { // Flush any trailing content that might be pending. this.pushSource(''); + if (this.stackSlot || this.inlineStack.length || this.compileStack.length) { + throw new Exception('Compile completed with content left on stack'); + } + return this.createFunctionContext(asObject); }, @@ -244,9 +250,6 @@ JavaScriptCompiler.prototype = { var current = this.topStack(); params.splice(1, 0, current); - // Use the options value generated from the invocation - params[params.length-1] = 'options'; - this.pushSource("if (!" + this.lastHelper + ") { " + current + " = blockHelperMissing.call(" + params.join(", ") + "); }"); }, @@ -382,7 +385,7 @@ JavaScriptCompiler.prototype = { // // Push the data lookup operator lookupData: function() { - this.push('data'); + this.pushStackLiteral('data'); }, // [pushStringParam] @@ -398,10 +401,14 @@ JavaScriptCompiler.prototype = { this.pushString(type); - if (typeof string === 'string') { - this.pushString(string); - } else { - this.pushStackLiteral(string); + // If it's a subexpression, the string result + // will be pushed after this opcode. + if (type !== 'sexpr') { + if (typeof string === 'string') { + this.pushString(string); + } else { + this.pushStackLiteral(string); + } } }, @@ -409,21 +416,25 @@ JavaScriptCompiler.prototype = { this.pushStackLiteral('{}'); if (this.options.stringParams) { - this.register('hashTypes', '{}'); - this.register('hashContexts', '{}'); + this.push('{}'); // hashContexts + this.push('{}'); // hashTypes } }, pushHash: function() { + if (this.hash) { + this.hashes.push(this.hash); + } this.hash = {values: [], types: [], contexts: []}; }, popHash: function() { var hash = this.hash; - this.hash = undefined; + this.hash = this.hashes.pop(); if (this.options.stringParams) { - this.register('hashContexts', '{' + hash.contexts.join(',') + '}'); - this.register('hashTypes', '{' + hash.types.join(',') + '}'); + this.push('{' + hash.contexts.join(',') + '}'); + this.push('{' + hash.types.join(',') + '}'); } + this.push('{\n ' + hash.values.join(',\n ') + '\n }'); }, @@ -485,18 +496,31 @@ JavaScriptCompiler.prototype = { // and pushes the helper's return value onto the stack. // // If the helper is not found, `helperMissing` is called. - invokeHelper: function(paramSize, name) { + invokeHelper: function(paramSize, name, isRoot) { this.context.aliases.helperMissing = 'helpers.helperMissing'; + this.useRegister('helper'); var helper = this.lastHelper = this.setupHelper(paramSize, name, true); var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context'); - this.push(helper.name + ' || ' + nonHelper); - this.replaceStack(function(name) { - return name + ' ? ' + name + '.call(' + - helper.callParams + ") " + ": helperMissing.call(" + - helper.helperMissingParams + ")"; - }); + var lookup = 'helper = ' + helper.name + ' || ' + nonHelper; + if (helper.paramsInit) { + lookup += ',' + helper.paramsInit; + } + + this.push( + '(' + + lookup + + ',helper ' + + '? helper.call(' + helper.callParams + ') ' + + ': helperMissing.call(' + helper.helperMissingParams + '))'); + + // Always flush subexpressions. This is both to prevent the compounding size issue that + // occurs when the code has to be duplicated for inlining and also to prevent errors + // due to the incorrect options object being passed due to the shared register. + if (!isRoot) { + this.flushInline(); + } }, // [invokeKnownHelper] @@ -525,8 +549,9 @@ JavaScriptCompiler.prototype = { // `knownHelpersOnly` flags at compile-time. invokeAmbiguous: function(name, helperCall) { this.context.aliases.functionType = '"function"'; + this.useRegister('helper'); - this.pushStackLiteral('{}'); // Hash value + this.emptyHash(); var helper = this.setupHelper(0, name, helperCall); var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper'); @@ -534,8 +559,11 @@ JavaScriptCompiler.prototype = { var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context'); var nextStack = this.nextStack(); - this.pushSource('if (' + nextStack + ' = ' + helperName + ') { ' + nextStack + ' = ' + nextStack + '.call(' + helper.callParams + '); }'); - this.pushSource('else { ' + nextStack + ' = ' + nonHelper + '; ' + nextStack + ' = typeof ' + nextStack + ' === functionType ? ' + nextStack + '.call(' + helper.callParams + ') : ' + nextStack + '; }'); + if (helper.paramsInit) { + this.pushSource(helper.paramsInit); + } + this.pushSource('if (helper = ' + helperName + ') { ' + nextStack + ' = helper.call(' + helper.callParams + '); }'); + this.pushSource('else { helper = ' + nonHelper + '; ' + nextStack + ' = typeof helper === functionType ? helper.call(' + helper.callParams + ') : helper; }'); }, // [invokePartial] @@ -681,7 +709,9 @@ JavaScriptCompiler.prototype = { replaceStack: function(callback) { var prefix = '', inline = this.isInline(), - stack; + stack, + createdStack, + usedLiteral; // If we are currently inline then we want to merge the inline statement into the // replacement statement via ',' @@ -691,9 +721,11 @@ JavaScriptCompiler.prototype = { if (top instanceof Literal) { // Literals do not need to be inlined stack = top.value; + usedLiteral = true; } else { // Get or create the current stack name for use by the inline - var name = this.stackSlot ? this.topStackName() : this.incrStack(); + createdStack = !this.stackSlot; + var name = !createdStack ? this.topStackName() : this.incrStack(); prefix = '(' + this.push(name) + ' = ' + top + '),'; stack = this.topStack(); @@ -705,9 +737,12 @@ JavaScriptCompiler.prototype = { var item = callback.call(this, stack); if (inline) { - if (this.inlineStack.length || this.compileStack.length) { + if (!usedLiteral) { this.popStack(); } + if (createdStack) { + this.stackSlot--; + } this.push('(' + prefix + item + ')'); } else { // Prevent modification of the context depth variable. Through replaceStack @@ -758,6 +793,9 @@ JavaScriptCompiler.prototype = { return item.value; } else { if (!inline) { + if (!this.stackSlot) { + throw new Exception('Invalid stack pop'); + } this.stackSlot--; } return item; @@ -786,25 +824,29 @@ JavaScriptCompiler.prototype = { }, setupHelper: function(paramSize, name, missingParams) { - var params = []; - this.setupParams(paramSize, params, missingParams); + var params = [], + paramsInit = this.setupParams(paramSize, params, missingParams); var foundHelper = this.nameLookup('helpers', name, 'helper'); return { params: params, + paramsInit: paramsInit, name: foundHelper, callParams: ["depth0"].concat(params).join(", "), helperMissingParams: missingParams && ["depth0", this.quotedString(name)].concat(params).join(", ") }; }, - // the params and contexts arguments are passed in arrays - // to fill in - setupParams: function(paramSize, params, useRegister) { + setupOptions: function(paramSize, params) { var options = [], contexts = [], types = [], param, inverse, program; options.push("hash:" + this.popStack()); + if (this.options.stringParams) { + options.push("hashTypes:" + this.popStack()); + options.push("hashContexts:" + this.popStack()); + } + inverse = this.popStack(); program = this.popStack(); @@ -817,7 +859,7 @@ JavaScriptCompiler.prototype = { } if (!inverse) { - this.context.aliases.self = "this"; + this.context.aliases.self = "this"; inverse = "self.noop"; } @@ -838,22 +880,28 @@ JavaScriptCompiler.prototype = { if (this.options.stringParams) { options.push("contexts:[" + contexts.join(",") + "]"); options.push("types:[" + types.join(",") + "]"); - options.push("hashContexts:hashContexts"); - options.push("hashTypes:hashTypes"); } if(this.options.data) { options.push("data:data"); } - options = "{" + options.join(",") + "}"; + return options; + }, + + // the params and contexts arguments are passed in arrays + // to fill in + setupParams: function(paramSize, params, useRegister) { + var options = '{' + this.setupOptions(paramSize, params).join(',') + '}'; + if (useRegister) { - this.register('options', options); + this.useRegister('options'); params.push('options'); + return 'options=' + options; } else { params.push(options); + return ''; } - return params.join(", "); } }; diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js index f91ff0216..ad55c7d4a 100644 --- a/lib/handlebars/compiler/printer.js +++ b/lib/handlebars/compiler/printer.js @@ -62,8 +62,8 @@ PrintVisitor.prototype.block = function(block) { return out; }; -PrintVisitor.prototype.mustache = function(mustache) { - var params = mustache.params, paramStrings = [], hash; +PrintVisitor.prototype.sexpr = function(sexpr) { + var params = sexpr.params, paramStrings = [], hash; for(var i=0, l=params.length; i@\[-\^`\{-~]+/{LOOKAHEAD} [\s\S]*?"--}}" strip(0,4); this.popState(); return 'COMMENT'; +"(" return 'OPEN_SEXPR'; +")" return 'CLOSE_SEXPR'; + "{{"{LEFT_STRIP}?">" return 'OPEN_PARTIAL'; "{{"{LEFT_STRIP}?"#" return 'OPEN_BLOCK'; "{{"{LEFT_STRIP}?"/" return 'OPEN_ENDBLOCK'; diff --git a/src/handlebars.yy b/src/handlebars.yy index 93797f5ae..7bff5125a 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -16,17 +16,17 @@ function stripFlags(open, close) { %% root - : statements EOF { return new yy.ProgramNode($1); } - | EOF { return new yy.ProgramNode([]); } + : statements EOF { return new yy.ProgramNode($1, @$); } + | EOF { return new yy.ProgramNode([], @$); } ; program - : simpleInverse statements -> new yy.ProgramNode([], $1, $2) - | statements simpleInverse statements -> new yy.ProgramNode($1, $2, $3) - | statements simpleInverse -> new yy.ProgramNode($1, $2, []) - | statements -> new yy.ProgramNode($1) - | simpleInverse -> new yy.ProgramNode([]) - | "" -> new yy.ProgramNode([]) + : simpleInverse statements -> new yy.ProgramNode([], $1, $2, @$) + | statements simpleInverse statements -> new yy.ProgramNode($1, $2, $3, @$) + | statements simpleInverse -> new yy.ProgramNode($1, $2, [], @$) + | statements -> new yy.ProgramNode($1, @$) + | simpleInverse -> new yy.ProgramNode([], @$) + | "" -> new yy.ProgramNode([], @$) ; statements @@ -35,20 +35,20 @@ statements ; statement - : openInverse program closeBlock -> new yy.BlockNode($1, $2.inverse, $2, $3) - | openBlock program closeBlock -> new yy.BlockNode($1, $2, $2.inverse, $3) + : openInverse program closeBlock -> new yy.BlockNode($1, $2.inverse, $2, $3, @$) + | openBlock program closeBlock -> new yy.BlockNode($1, $2, $2.inverse, $3, @$) | mustache -> $1 | partial -> $1 - | CONTENT -> new yy.ContentNode($1) - | COMMENT -> new yy.CommentNode($1) + | CONTENT -> new yy.ContentNode($1, @$) + | COMMENT -> new yy.CommentNode($1, @$) ; openBlock - : OPEN_BLOCK inMustache CLOSE -> new yy.MustacheNode($2[0], $2[1], $1, stripFlags($1, $3)) + : OPEN_BLOCK sexpr CLOSE -> new yy.MustacheNode($2, null, $1, stripFlags($1, $3), @$) ; openInverse - : OPEN_INVERSE inMustache CLOSE -> new yy.MustacheNode($2[0], $2[1], $1, stripFlags($1, $3)) + : OPEN_INVERSE sexpr CLOSE -> new yy.MustacheNode($2, null, $1, stripFlags($1, $3), @$) ; closeBlock @@ -58,34 +58,34 @@ closeBlock mustache // Parsing out the '&' escape token at AST level saves ~500 bytes after min due to the removal of one parser node. // This also allows for handler unification as all mustache node instances can utilize the same handler - : OPEN inMustache CLOSE -> new yy.MustacheNode($2[0], $2[1], $1, stripFlags($1, $3)) - | OPEN_UNESCAPED inMustache CLOSE_UNESCAPED -> new yy.MustacheNode($2[0], $2[1], $1, stripFlags($1, $3)) + : OPEN sexpr CLOSE -> new yy.MustacheNode($2, null, $1, stripFlags($1, $3), @$) + | OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> new yy.MustacheNode($2, null, $1, stripFlags($1, $3), @$) ; - partial - : OPEN_PARTIAL partialName path? CLOSE -> new yy.PartialNode($2, $3, stripFlags($1, $4)) + : OPEN_PARTIAL partialName path? CLOSE -> new yy.PartialNode($2, $3, stripFlags($1, $4), @$) ; simpleInverse : OPEN_INVERSE CLOSE -> stripFlags($1, $2) ; -inMustache - : path param* hash? -> [[$1].concat($2), $3] - | dataName -> [[$1], null] +sexpr + : path param* hash? -> new yy.SexprNode([$1].concat($2), $3, @$) + | dataName -> new yy.SexprNode([$1], null, @$) ; param : path -> $1 - | STRING -> new yy.StringNode($1) - | INTEGER -> new yy.IntegerNode($1) - | BOOLEAN -> new yy.BooleanNode($1) + | STRING -> new yy.StringNode($1, @$) + | INTEGER -> new yy.IntegerNode($1, @$) + | BOOLEAN -> new yy.BooleanNode($1, @$) | dataName -> $1 + | OPEN_SEXPR sexpr CLOSE_SEXPR {$2.isHelper = true; $$ = $2;} ; hash - : hashSegment+ -> new yy.HashNode($1) + : hashSegment+ -> new yy.HashNode($1, @$) ; hashSegment @@ -93,17 +93,17 @@ hashSegment ; partialName - : path -> new yy.PartialNameNode($1) - | STRING -> new yy.PartialNameNode(new yy.StringNode($1)) - | INTEGER -> new yy.PartialNameNode(new yy.IntegerNode($1)) + : path -> new yy.PartialNameNode($1, @$) + | STRING -> new yy.PartialNameNode(new yy.StringNode($1, @$), @$) + | INTEGER -> new yy.PartialNameNode(new yy.IntegerNode($1, @$)) ; dataName - : DATA path -> new yy.DataNode($2) + : DATA path -> new yy.DataNode($2, @$) ; path - : pathSegments -> new yy.IdNode($1) + : pathSegments -> new yy.IdNode($1, @$) ; pathSegments