summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--lib/handlebars/compiler/ast.js5
-rw-r--r--lib/handlebars/compiler/compiler.js23
-rw-r--r--lib/handlebars/compiler/printer.js4
-rw-r--r--spec/parser_spec.rb16
-rw-r--r--spec/qunit_spec.js57
-rw-r--r--spec/tokenizer_spec.rb14
-rw-r--r--src/handlebars.l1
-rw-r--r--src/handlebars.yy3
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