diff options
-rw-r--r-- | lib/handlebars/compiler/ast.js | 24 | ||||
-rw-r--r-- | lib/handlebars/compiler/compiler.js | 90 | ||||
-rw-r--r-- | lib/handlebars/compiler/javascript-compiler.js | 42 | ||||
-rw-r--r-- | lib/handlebars/compiler/printer.js | 12 | ||||
-rw-r--r-- | spec/ast.js | 14 | ||||
-rw-r--r-- | spec/string-params.js | 16 | ||||
-rw-r--r-- | spec/subexpressions.js | 166 | ||||
-rw-r--r-- | spec/tokenizer.js | 27 | ||||
-rw-r--r-- | src/handlebars.l | 7 | ||||
-rw-r--r-- | src/handlebars.yy | 16 |
10 files changed, 329 insertions, 85 deletions
diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index b6c3a03..ce5ee11 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -46,7 +46,6 @@ var AST = { 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 @@ -58,6 +57,25 @@ 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); + } + + // 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) { + this.type = "sexpr"; + this.hash = hash; + var id = this.id = rawParams[0]; var params = this.params = rawParams.slice(1); @@ -84,8 +102,8 @@ var AST = { }, BlockNode: function(mustache, program, inverse, close, locInfo) { - if(mustache.id.original !== close.path.original) { - throw new Exception(mustache.id.original + " doesn't match " + close.path.original); + if(mustache.sexpr.id.original !== close.path.original) { + throw new Exception(mustache.sexpr.id.original + " doesn't match " + close.path.original); } LocationInfo.call(this, locInfo); diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 259c543..00353a8 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -156,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` @@ -170,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` @@ -198,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); } @@ -226,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; @@ -257,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); @@ -274,9 +272,9 @@ 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); @@ -287,6 +285,18 @@ Compiler.prototype = { } }, + 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); + } + }, + ID: function(id) { this.addDepth(id.depth); this.opcode('getContext', id.depth); @@ -349,14 +359,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; @@ -383,35 +393,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'); } diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 159a38b..d920d52 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -244,9 +244,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(", ") + "); }"); }, @@ -398,10 +395,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,8 +410,8 @@ JavaScriptCompiler.prototype = { this.pushStackLiteral('{}'); if (this.options.stringParams) { - this.register('hashTypes', '{}'); - this.register('hashContexts', '{}'); + this.push('{}'); // hashContexts + this.push('{}'); // hashTypes } }, pushHash: function() { @@ -421,9 +422,10 @@ JavaScriptCompiler.prototype = { this.hash = undefined; 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 }'); }, @@ -526,7 +528,7 @@ JavaScriptCompiler.prototype = { invokeAmbiguous: function(name, helperCall) { this.context.aliases.functionType = '"function"'; - this.pushStackLiteral('{}'); // Hash value + this.emptyHash(); var helper = this.setupHelper(0, name, helperCall); var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper'); @@ -805,6 +807,11 @@ JavaScriptCompiler.prototype = { 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(); @@ -838,22 +845,13 @@ 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(",") + "}"; - if (useRegister) { - this.register('options', options); - params.push('options'); - } else { - params.push(options); - } - return params.join(", "); + params.push("{" + options.join(",") + "}"); } }; diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js index f91ff02..ad55c7d 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<l; i++) { paramStrings.push(this.accept(params[i])); @@ -71,9 +71,13 @@ PrintVisitor.prototype.mustache = function(mustache) { params = "[" + paramStrings.join(", ") + "]"; - hash = mustache.hash ? " " + this.accept(mustache.hash) : ""; + hash = sexpr.hash ? " " + this.accept(sexpr.hash) : ""; + + return this.accept(sexpr.id) + " " + params + hash; +}; - return this.pad("{{ " + this.accept(mustache.id) + " " + params + hash + " }}"); +PrintVisitor.prototype.mustache = function(mustache) { + return this.pad("{{ " + this.accept(mustache.sexpr) + " }}"); }; PrintVisitor.prototype.partial = function(partial) { diff --git a/spec/ast.js b/spec/ast.js index 0feb3f5..0cb1103 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -68,14 +68,24 @@ describe('ast', function() { }); }); describe('BlockNode', function() { + it('should throw on mustache mismatch (old sexpr-less version)', function() { + shouldThrow(function() { + var mustacheNode = new handlebarsEnv.AST.MustacheNode([{ original: 'foo'}], null, '{{', {}); + new handlebarsEnv.AST.BlockNode(mustacheNode, {}, {}, {path: {original: 'bar'}}); + }, Handlebars.Exception, "foo doesn't match bar"); + }); it('should throw on mustache mismatch', function() { shouldThrow(function() { - new handlebarsEnv.AST.BlockNode({id: {original: 'foo'}}, {}, {}, {path: {original: 'bar'}}); + var sexprNode = new handlebarsEnv.AST.SexprNode([{ original: 'foo'}], null); + var mustacheNode = new handlebarsEnv.AST.MustacheNode(sexprNode, null, '{{', {}); + new handlebarsEnv.AST.BlockNode(mustacheNode, {}, {}, {path: {original: 'bar'}}); }, Handlebars.Exception, "foo doesn't match bar"); }); it('stores location info', function(){ - var block = new handlebarsEnv.AST.BlockNode({strip: {}, id: {original: 'foo'}}, + var sexprNode = new handlebarsEnv.AST.SexprNode([{ original: 'foo'}], null); + var mustacheNode = new handlebarsEnv.AST.MustacheNode(sexprNode, null, '{{', {}); + var block = new handlebarsEnv.AST.BlockNode(mustacheNode, {strip: {}}, {strip: {}}, { strip: {}, diff --git a/spec/string-params.js b/spec/string-params.js index 1ebb583..920b855 100644 --- a/spec/string-params.js +++ b/spec/string-params.js @@ -142,4 +142,20 @@ describe('string params mode', function() { equals(result, "STOP ME FROM READING HACKER NEWS I need-a dad.joke wot", "Proper context variable output"); }); + + it("with nested block ambiguous", function() { + var template = CompilerContext.compile('{{#with content}}{{#view}}{{firstName}} {{lastName}}{{/view}}{{/with}}', {stringParams: true}); + + var helpers = { + with: function(options) { + return "WITH"; + }, + view: function() { + return "VIEW"; + } + }; + + var result = template({}, {helpers: helpers}); + equals(result, "WITH"); + }); }); diff --git a/spec/subexpressions.js b/spec/subexpressions.js new file mode 100644 index 0000000..9c06f94 --- /dev/null +++ b/spec/subexpressions.js @@ -0,0 +1,166 @@ +/*global CompilerContext, shouldCompileTo */ +describe('subexpressions', function() { + it("arg-less helper", function() { + var string = "{{foo (bar)}}!"; + var context = {}; + var helpers = { + foo: function(val) { + return val+val; + }, + bar: function() { + return "LOL"; + } + }; + shouldCompileTo(string, [context, helpers], "LOLLOL!"); + }); + + it("helper w args", function() { + var string = '{{blog (equal a b)}}'; + + var context = { bar: "LOL" }; + var helpers = { + blog: function(val) { + return "val is " + val; + }, + equal: function(x, y) { + return x === y; + } + }; + shouldCompileTo(string, [context, helpers], "val is true"); + }); + + it("supports much nesting", function() { + var string = '{{blog (equal (equal true true) true)}}'; + + var context = { bar: "LOL" }; + var helpers = { + blog: function(val) { + return "val is " + val; + }, + equal: function(x, y) { + return x === y; + } + }; + shouldCompileTo(string, [context, helpers], "val is true"); + }); + + it("provides each nested helper invocation its own options hash", function() { + var string = '{{equal (equal true true) true}}'; + + var lastOptions = null; + var helpers = { + equal: function(x, y, options) { + if (!options || options === lastOptions) { + throw new Error("options hash was reused"); + } + lastOptions = options; + return x === y; + } + }; + shouldCompileTo(string, [{}, helpers], "true"); + }); + + it("with hashes", function() { + var string = '{{blog (equal (equal true true) true fun="yes")}}'; + + var context = { bar: "LOL" }; + var helpers = { + blog: function(val) { + return "val is " + val; + }, + equal: function(x, y) { + return x === y; + } + }; + shouldCompileTo(string, [context, helpers], "val is true"); + }); + + it("as hashes", function() { + var string = '{{blog fun=(equal true true)}}'; + + var helpers = { + blog: function(options) { + return "val is " + options.hash.fun; + }, + equal: function(x, y) { + return x === y; + } + }; + shouldCompileTo(string, [{}, helpers], "val is true"); + }); + + it("in string params mode,", function() { + var template = CompilerContext.compile('{{snog (blorg foo x=y) yeah a=b}}', {stringParams: true}); + + var helpers = { + snog: function(a, b, options) { + equals(a, 'foo'); + equals(options.types.length, 2, "string params for outer helper processed correctly"); + equals(options.types[0], 'sexpr', "string params for outer helper processed correctly"); + equals(options.types[1], 'ID', "string params for outer helper processed correctly"); + return a + b; + }, + + blorg: function(a, options) { + equals(options.types.length, 1, "string params for inner helper processed correctly"); + equals(options.types[0], 'ID', "string params for inner helper processed correctly"); + return a; + } + }; + + var result = template({ + foo: {}, + yeah: {} + }, {helpers: helpers}); + + equals(result, "fooyeah"); + }); + + it("as hashes in string params mode", function() { + + var template = CompilerContext.compile('{{blog fun=(bork)}}', {stringParams: true}); + + var helpers = { + blog: function(options) { + equals(options.hashTypes.fun, 'sexpr'); + return "val is " + options.hash.fun; + }, + bork: function() { + return "BORK"; + } + }; + + var result = template({}, {helpers: helpers}); + equals(result, "val is BORK"); + }); + + it("subexpression functions on the context", function() { + var string = "{{foo (bar)}}!"; + var context = { + bar: function() { + return "LOL"; + } + }; + var helpers = { + foo: function(val) { + return val+val; + } + }; + shouldCompileTo(string, [context, helpers], "LOLLOL!"); + }); + + it("subexpressions can't just be property lookups", function() { + var string = "{{foo (bar)}}!"; + var context = { + bar: "LOL" + }; + var helpers = { + foo: function(val) { + return val+val; + } + }; + shouldThrow(function() { + shouldCompileTo(string, [context, helpers], "LOLLOL!"); + }); + }); +}); diff --git a/spec/tokenizer.js b/spec/tokenizer.js index 80f28ab..841a5ab 100644 --- a/spec/tokenizer.js +++ b/spec/tokenizer.js @@ -364,4 +364,31 @@ describe('Tokenizer', function() { it('does not time out in a mustache when invalid ID characters are used', function() { shouldMatchTokens(tokenize("{{foo & }}"), ['OPEN', 'ID']); }); + + it('tokenizes subexpressions', function() { + var result = tokenize("{{foo (bar)}}"); + shouldMatchTokens(result, ['OPEN', 'ID', 'OPEN_SEXPR', 'ID', 'CLOSE_SEXPR', 'CLOSE']); + shouldBeToken(result[1], "ID", "foo"); + shouldBeToken(result[3], "ID", "bar"); + + result = tokenize("{{foo (a-x b-y)}}"); + shouldMatchTokens(result, ['OPEN', 'ID', 'OPEN_SEXPR', 'ID', 'ID', 'CLOSE_SEXPR', 'CLOSE']); + shouldBeToken(result[1], "ID", "foo"); + shouldBeToken(result[3], "ID", "a-x"); + shouldBeToken(result[4], "ID", "b-y"); + }); + + it('tokenizes nested subexpressions', function() { + var result = tokenize("{{foo (bar (lol rofl)) (baz)}}"); + shouldMatchTokens(result, ['OPEN', 'ID', 'OPEN_SEXPR', 'ID', 'OPEN_SEXPR', 'ID', 'ID', 'CLOSE_SEXPR', 'CLOSE_SEXPR', 'OPEN_SEXPR', 'ID', 'CLOSE_SEXPR', 'CLOSE']); + shouldBeToken(result[3], "ID", "bar"); + shouldBeToken(result[5], "ID", "lol"); + shouldBeToken(result[6], "ID", "rofl"); + shouldBeToken(result[10], "ID", "baz"); + }); + + it('tokenizes nested subexpressions: literals', function() { + var result = tokenize("{{foo (bar (lol true) false) (baz 1) (blah 'b') (blorg \"c\")}}"); + shouldMatchTokens(result, ['OPEN', 'ID', 'OPEN_SEXPR', 'ID', 'OPEN_SEXPR', 'ID', 'BOOLEAN', 'CLOSE_SEXPR', 'BOOLEAN', 'CLOSE_SEXPR', 'OPEN_SEXPR', 'ID', 'INTEGER', 'CLOSE_SEXPR', 'OPEN_SEXPR', 'ID', 'STRING', 'CLOSE_SEXPR', 'OPEN_SEXPR', 'ID', 'STRING', 'CLOSE_SEXPR', 'CLOSE']); + }); }); diff --git a/src/handlebars.l b/src/handlebars.l index 913121c..996badb 100644 --- a/src/handlebars.l +++ b/src/handlebars.l @@ -12,8 +12,8 @@ function strip(start, end) { LEFT_STRIP "~" RIGHT_STRIP "~" -LOOKAHEAD [=~}\s\/.] -LITERAL_LOOKAHEAD [~}\s] +LOOKAHEAD [=~}\s\/.)] +LITERAL_LOOKAHEAD [~}\s)] /* ID is the inverse of control characters. @@ -51,6 +51,9 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD} <com>[\s\S]*?"--}}" strip(0,4); this.popState(); return 'COMMENT'; +<mu>"(" return 'OPEN_SEXPR'; +<mu>")" return 'CLOSE_SEXPR'; + <mu>"{{"{LEFT_STRIP}?">" return 'OPEN_PARTIAL'; <mu>"{{"{LEFT_STRIP}?"#" return 'OPEN_BLOCK'; <mu>"{{"{LEFT_STRIP}?"/" return 'OPEN_ENDBLOCK'; diff --git a/src/handlebars.yy b/src/handlebars.yy index 63de17b..319b8ef 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -44,11 +44,11 @@ statement ; 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,11 +58,10 @@ 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), @$) ; @@ -71,9 +70,9 @@ 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 @@ -82,6 +81,7 @@ param | INTEGER -> new yy.IntegerNode($1, @$) | BOOLEAN -> new yy.BooleanNode($1, @$) | dataName -> $1 + | OPEN_SEXPR sexpr CLOSE_SEXPR {$2.isHelper = true; $$ = $2;} ; hash |