diff options
author | Yehuda Katz <wycats@gmail.com> | 2012-07-05 22:43:05 -0700 |
---|---|---|
committer | Yehuda Katz <wycats@gmail.com> | 2012-07-05 22:43:05 -0700 |
commit | 72e05d623c07cc3a812528d52fb6d325134efee4 (patch) | |
tree | 8eec0d2b12cb9faaeea22a76fd0c045457669f7c | |
parent | ff1547ea049e5b4b499dfa8d4d450567a91f5505 (diff) | |
download | handlebars.js-72e05d623c07cc3a812528d52fb6d325134efee4.zip handlebars.js-72e05d623c07cc3a812528d52fb6d325134efee4.tar.gz handlebars.js-72e05d623c07cc3a812528d52fb6d325134efee4.tar.bz2 |
Add support for @data variables
-rw-r--r-- | lib/handlebars/compiler/ast.js | 5 | ||||
-rw-r--r-- | lib/handlebars/compiler/compiler.js | 23 | ||||
-rw-r--r-- | lib/handlebars/compiler/printer.js | 4 | ||||
-rw-r--r-- | spec/parser_spec.rb | 16 | ||||
-rw-r--r-- | spec/qunit_spec.js | 57 | ||||
-rw-r--r-- | spec/tokenizer_spec.rb | 14 | ||||
-rw-r--r-- | src/handlebars.l | 1 | ||||
-rw-r--r-- | src/handlebars.yy | 3 |
8 files changed, 117 insertions, 6 deletions
diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index d61377e..25abe0a 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -93,6 +93,11 @@ var Handlebars = require('./base'); this.isSimple = parts.length === 1 && !this.isScoped && depth === 0; }; + Handlebars.AST.DataNode = function(id) { + this.type = "DATA"; + this.id = id; + }; + Handlebars.AST.StringNode = function(string) { this.type = "STRING"; this.string = string; diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 6807494..ae48c69 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -96,7 +96,7 @@ Handlebars.JavaScriptCompiler = function() {}; this.children[guid] = result; for(var i=0, l=result.depths.list.length; i<l; i++) { - var depth = result.depths.list[i]; + depth = result.depths.list[i]; if(depth < 2) { continue; } else { this.addDepth(depth - 1); } @@ -210,10 +210,14 @@ Handlebars.JavaScriptCompiler = function() {}; simpleMustache: function(mustache, program, inverse) { var id = mustache.id; - this.addDepth(id.depth); - this.opcode('getContext', id.depth); + if (id.type === 'ID') { + this.addDepth(id.depth); + this.opcode('getContext', id.depth); + } - if (id.parts.length) { + if (id.type === 'DATA') { + this.opcode('lookupData', id.id); + } else if (id.parts.length) { this.opcode('lookupOnContext', id.parts[0]); for(var i=1, l=id.parts.length; i<l; i++) { this.opcode('lookup', id.parts[i]); @@ -247,6 +251,10 @@ Handlebars.JavaScriptCompiler = function() {}; } }, + DATA: function(data) { + this.opcode('lookupData', data.id); + }, + STRING: function(string) { this.opcode('pushString', string.string); }, @@ -271,6 +279,7 @@ Handlebars.JavaScriptCompiler = function() {}; }, addDepth: function(depth) { + if(isNaN(depth)) { throw new Error("EWOT"); } if(depth === 0) { return; } if(!this.depths[depth]) { @@ -646,6 +655,10 @@ Handlebars.JavaScriptCompiler = function() {}; }); }, + lookupData: function(id) { + this.pushStack(this.nameLookup('data', id, 'data')); + }, + // [pushStringParam] // // On stack, before: ... @@ -831,7 +844,7 @@ Handlebars.JavaScriptCompiler = function() {}; var programParams = [child.index, child.name, "data"]; for(var i=0, l = depths.length; i<l; i++) { - var depth = depths[i]; + depth = depths[i]; if(depth === 1) { programParams.push("depth0"); } else { programParams.push("depth" + (depth - 1)); } diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js index 8157190..7a42a66 100644 --- a/lib/handlebars/compiler/printer.js +++ b/lib/handlebars/compiler/printer.js @@ -111,6 +111,10 @@ Handlebars.PrintVisitor.prototype.ID = function(id) { } }; +Handlebars.PrintVisitor.prototype.DATA = function(data) { + return "@" + data.id; +}; + Handlebars.PrintVisitor.prototype.content = function(content) { return this.pad("CONTENT[ '" + content.string + "' ]"); }; diff --git a/spec/parser_spec.rb b/spec/parser_spec.rb index 8dd13a4..3fe9029 100644 --- a/spec/parser_spec.rb +++ b/spec/parser_spec.rb @@ -110,6 +110,10 @@ describe "Parser" do "ID:#{id}" end + def data(id) + "@#{id}" + end + def path(*parts) "PATH:#{parts.join("/")}" end @@ -119,6 +123,10 @@ describe "Parser" do ast_for("{{foo}}").should == root { mustache id("foo") } end + it "parses simple mustaches with data" do + ast_for("{{@foo}}").should == root { mustache data("foo") } + end + it "parses mustaches with paths" do ast_for("{{foo/bar}}").should == root { mustache path("foo", "bar") } end @@ -152,6 +160,10 @@ describe "Parser" do mustache id("foo"), [], hash(["bar", boolean("false")]) end + ast_for("{{foo bar=@baz}}").should == root do + mustache id("foo"), [], hash(["bar", data("baz")]) + end + ast_for("{{foo bar=baz bat=bam}}").should == root do mustache id("foo"), [], hash(["bar", "ID:baz"], ["bat", "ID:bam"]) end @@ -190,6 +202,10 @@ describe "Parser" do ast_for("{{foo false}}").should == root { mustache id("foo"), [boolean("false")] } end + it "parses mutaches with DATA parameters" do + ast_for("{{foo @bar}}").should == root { mustache id("foo"), [data("bar")] } + end + it "parses contents followed by a mustache" do ast_for("foo bar {{baz}}").should == root do content "foo bar " diff --git a/spec/qunit_spec.js b/spec/qunit_spec.js index 5c53e97..1bf831b 100644 --- a/spec/qunit_spec.js +++ b/spec/qunit_spec.js @@ -616,7 +616,7 @@ test("if with function argument", function() { }); test("each", function() { - var string = "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!" + var string = "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!"; var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!", "each with array argument iterates over the contents when not empty"); @@ -624,6 +624,16 @@ test("each", function() { "each with array argument ignores the contents when empty"); }); +test("each with @index", function() { + var string = "{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!"; + var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; + + var template = CompilerContext.compile(string, {data: true}); + var result = template(hash, { data: {} }); + + equal(result, "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", "The @index variable is used"); +}); + test("log", function() { var string = "{{log blah}}" var hash = { blah: "whee" }; @@ -655,6 +665,51 @@ test("passing in data to a compiled function that expects data - works with help equals("happy cat", result, "Data output by helper"); }); +test("data can be looked up via @foo", function() { + var template = CompilerContext.compile("{{@hello}}", { data: true }); + var result = template({}, { data: { hello: "hello" } }); + equals("hello", result, "@foo retrieves template data"); +}); + +test("parameter data can be looked up via @foo", function() { + var template = CompilerContext.compile("{{hello @world}}", { data: true }); + var helpers = { + hello: function(noun) { + return "Hello " + noun; + } + }; + + var result = template({}, { helpers: helpers, data: { world: "world" } }); + equals("Hello world", result, "@foo as a parameter retrieves template data"); +}); + +test("hash values can be looked up via @foo", function() { + var template = CompilerContext.compile("{{hello noun=@world}}", { data: true }); + var helpers = { + hello: function(options) { + return "Hello " + options.hash.noun; + } + }; + + var result = template({}, { helpers: helpers, data: { world: "world" } }); + equals("Hello world", result, "@foo as a parameter retrieves template data"); +}); + +test("data is inherited downstream", function() { + var template = CompilerContext.compile("{{#let foo=bar.baz}}{{@foo}}{{/let}}", { data: true }); + var helpers = { + let: function(options) { + for (var prop in options.hash) { + options.data[prop] = options.hash[prop]; + } + return options.fn(this); + } + }; + + var result = template({ bar: { baz: "hello world" } }, { helpers: helpers, data: {} }); + equals("hello world", result, "data variables are inherited downstream"); +}); + test("passing in data to a compiled function that expects data - works with helpers in partials", function() { var template = CompilerContext.compile("{{>my_partial}}", {data: true}); diff --git a/spec/tokenizer_spec.rb b/spec/tokenizer_spec.rb index a8bb94d..2517ade 100644 --- a/spec/tokenizer_spec.rb +++ b/spec/tokenizer_spec.rb @@ -244,6 +244,20 @@ describe "Tokenizer" do result[2].should be_token("ID", "omg") end + it "tokenizes special @ identifiers" do + result = tokenize("{{ @foo }}") + result.should match_tokens %w( OPEN DATA CLOSE ) + result[1].should be_token("DATA", "foo") + + result = tokenize("{{ foo @bar }}") + result.should match_tokens %w( OPEN ID DATA CLOSE ) + result[2].should be_token("DATA", "bar") + + result = tokenize("{{ foo bar=@baz }}") + result.should match_tokens %w( OPEN ID ID EQUALS DATA CLOSE ) + result[4].should be_token("DATA", "baz") + end + it "does not time out in a mustache with a single } followed by EOF" do Timeout.timeout(1) { tokenize("{{foo}").should match_tokens(%w(OPEN ID)) } end diff --git a/src/handlebars.l b/src/handlebars.l index 592fd5c..f81fcdb 100644 --- a/src/handlebars.l +++ b/src/handlebars.l @@ -31,6 +31,7 @@ <mu>"}}}" { this.popState(); return 'CLOSE'; } <mu>"}}" { this.popState(); return 'CLOSE'; } <mu>'"'("\\"["]|[^"])*'"' { yytext = yytext.substr(1,yyleng-2).replace(/\\"/g,'"'); return 'STRING'; } +<mu>"@"[a-zA-Z]+ { yytext = yytext.substr(1); return 'DATA'; } <mu>"true"/[}\s] { return 'BOOLEAN'; } <mu>"false"/[}\s] { return 'BOOLEAN'; } <mu>[0-9]+/[}\s] { return 'INTEGER'; } diff --git a/src/handlebars.yy b/src/handlebars.yy index ec4fbe1..70b7777 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -58,6 +58,7 @@ inMustache | path params { $$ = [[$1].concat($2), null]; } | path hash { $$ = [[$1], $2]; } | path { $$ = [[$1], null]; } + | DATA { $$ = [[new yy.DataNode($1)], null]; } ; params @@ -70,6 +71,7 @@ param | STRING { $$ = new yy.StringNode($1); } | INTEGER { $$ = new yy.IntegerNode($1); } | BOOLEAN { $$ = new yy.BooleanNode($1); } + | DATA { $$ = new yy.DataNode($1); } ; hash @@ -86,6 +88,7 @@ hashSegment | ID EQUALS STRING { $$ = [$1, new yy.StringNode($3)]; } | ID EQUALS INTEGER { $$ = [$1, new yy.IntegerNode($3)]; } | ID EQUALS BOOLEAN { $$ = [$1, new yy.BooleanNode($3)]; } + | ID EQUALS DATA { $$ = [$1, new yy.DataNode($3)]; } ; path |