summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Gemfile.lock20
-rw-r--r--lib/handlebars/base.js23
-rw-r--r--lib/handlebars/compiler/ast.js16
-rw-r--r--lib/handlebars/compiler/base.js10
-rw-r--r--lib/handlebars/compiler/compiler.js20
-rw-r--r--lib/handlebars/compiler/printer.js6
-rw-r--r--spec/parser_spec.rb136
-rw-r--r--spec/qunit_spec.js48
-rw-r--r--spec/spec_helper.rb27
-rw-r--r--spec/tokenizer_spec.rb16
-rw-r--r--src/handlebars.l6
-rw-r--r--src/handlebars.yy13
12 files changed, 259 insertions, 82 deletions
diff --git a/Gemfile.lock b/Gemfile.lock
index 67a89da..91aef93 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -2,18 +2,18 @@ GEM
remote: http://rubygems.org/
specs:
diff-lcs (1.1.3)
- libv8 (3.3.10.4)
rake (0.9.2.2)
- rspec (2.11.0)
- rspec-core (~> 2.11.0)
- rspec-expectations (~> 2.11.0)
- rspec-mocks (~> 2.11.0)
- rspec-core (2.11.0)
- rspec-expectations (2.11.1)
+ ref (1.0.2)
+ rspec (2.12.0)
+ rspec-core (~> 2.12.0)
+ rspec-expectations (~> 2.12.0)
+ rspec-mocks (~> 2.12.0)
+ rspec-core (2.12.2)
+ rspec-expectations (2.12.1)
diff-lcs (~> 1.1.3)
- rspec-mocks (2.11.1)
- therubyracer (0.10.1)
- libv8 (~> 3.3.10)
+ rspec-mocks (2.12.1)
+ therubyracer (0.11.0)
+ ref
PLATFORMS
ruby
diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js
index 03f41ef..d658b3d 100644
--- a/lib/handlebars/base.js
+++ b/lib/handlebars/base.js
@@ -62,6 +62,24 @@ Handlebars.createFrame = Object.create || function(object) {
return obj;
};
+Handlebars.logger = {
+ DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, level: 3,
+
+ methodMap: {0: 'debug', 1: 'info', 2: 'warn', 3: 'error'},
+
+ // can be overridden in the host environment
+ log: function(level, obj) {
+ if (Handlebars.logger.level <= level) {
+ var method = Handlebars.logger.methodMap[level];
+ if (typeof console !== 'undefined' && console[method]) {
+ console[method].call(console, obj);
+ }
+ }
+ }
+};
+
+Handlebars.log = function(level, obj) { Handlebars.logger.log(level, obj); };
+
Handlebars.registerHelper('each', function(context, options) {
var fn = options.fn, inverse = options.inverse;
var i = 0, ret = "", data;
@@ -117,8 +135,9 @@ Handlebars.registerHelper('with', function(context, options) {
return options.fn(context);
});
-Handlebars.registerHelper('log', function(context) {
- Handlebars.log(context);
+Handlebars.registerHelper('log', function(context, options) {
+ var level = options.data && options.data.level != null ? parseInt(options.data.level, 10) : 1;
+ Handlebars.log(level, context);
});
}(this.Handlebars));
diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js
index 25abe0a..459b863 100644
--- a/lib/handlebars/compiler/ast.js
+++ b/lib/handlebars/compiler/ast.js
@@ -33,13 +33,10 @@ var Handlebars = require('./base');
// pass or at runtime.
};
- Handlebars.AST.PartialNode = function(id, context) {
- this.type = "partial";
-
- // TODO: disallow complex IDs
-
- this.id = id;
- this.context = context;
+ Handlebars.AST.PartialNode = function(partialName, context) {
+ this.type = "partial";
+ this.partialName = partialName;
+ this.context = context;
};
var verifyMatch = function(open, close) {
@@ -93,6 +90,11 @@ var Handlebars = require('./base');
this.isSimple = parts.length === 1 && !this.isScoped && depth === 0;
};
+ Handlebars.AST.PartialNameNode = function(name) {
+ this.type = "PARTIAL_NAME";
+ this.name = name;
+ };
+
Handlebars.AST.DataNode = function(id) {
this.type = "DATA";
this.id = id;
diff --git a/lib/handlebars/compiler/base.js b/lib/handlebars/compiler/base.js
index 4bb8735..5ce4222 100644
--- a/lib/handlebars/compiler/base.js
+++ b/lib/handlebars/compiler/base.js
@@ -12,16 +12,6 @@ Handlebars.parse = function(string) {
Handlebars.print = function(ast) {
return new Handlebars.PrintVisitor().accept(ast);
};
-
-Handlebars.logger = {
- DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, level: 3,
-
- // override in the host environment
- log: function(level, str) {}
-};
-
-Handlebars.log = function(level, str) { Handlebars.logger.log(level, str); };
-
// END(BROWSER)
module.exports = Handlebars;
diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js
index 7578dd2..297a553 100644
--- a/lib/handlebars/compiler/compiler.js
+++ b/lib/handlebars/compiler/compiler.js
@@ -160,7 +160,7 @@ Handlebars.JavaScriptCompiler = function() {};
},
partial: function(partial) {
- var id = partial.id;
+ var partialName = partial.partialName;
this.usePartial = true;
if(partial.context) {
@@ -169,7 +169,7 @@ Handlebars.JavaScriptCompiler = function() {};
this.opcode('push', 'depth0');
}
- this.opcode('invokePartial', id.original);
+ this.opcode('invokePartial', partialName.name);
this.opcode('append');
},
@@ -1035,16 +1035,28 @@ Handlebars.JavaScriptCompiler = function() {};
})(Handlebars.Compiler, Handlebars.JavaScriptCompiler);
Handlebars.precompile = function(string, options) {
- options = options || {};
+ if (typeof string !== 'string') {
+ throw new Handlebars.Exception("You must pass a string to Handlebars.compile. You passed " + string);
+ }
+ options = options || {};
+ if (!('data' in options)) {
+ options.data = true;
+ }
var ast = Handlebars.parse(string);
var environment = new Handlebars.Compiler().compile(ast, options);
return new Handlebars.JavaScriptCompiler().compile(environment, options);
};
Handlebars.compile = function(string, options) {
- options = options || {};
+ if (typeof string !== 'string') {
+ throw new Handlebars.Exception("You must pass a string to Handlebars.compile. You passed " + string);
+ }
+ options = options || {};
+ if (!('data' in options)) {
+ options.data = true;
+ }
var compiled;
function compile() {
var ast = Handlebars.parse(string);
diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js
index 7a42a66..853a4ca 100644
--- a/lib/handlebars/compiler/printer.js
+++ b/lib/handlebars/compiler/printer.js
@@ -72,7 +72,7 @@ Handlebars.PrintVisitor.prototype.mustache = function(mustache) {
};
Handlebars.PrintVisitor.prototype.partial = function(partial) {
- var content = this.accept(partial.id);
+ var content = this.accept(partial.partialName);
if(partial.context) { content = content + " " + this.accept(partial.context); }
return this.pad("{{> " + content + " }}");
};
@@ -111,6 +111,10 @@ Handlebars.PrintVisitor.prototype.ID = function(id) {
}
};
+Handlebars.PrintVisitor.prototype.PARTIAL_NAME = function(partialName) {
+ return "PARTIAL:" + partialName.name;
+};
+
Handlebars.PrintVisitor.prototype.DATA = function(data) {
return "@" + data.id;
};
diff --git a/spec/parser_spec.rb b/spec/parser_spec.rb
index 2b234d3..d9187ce 100644
--- a/spec/parser_spec.rb
+++ b/spec/parser_spec.rb
@@ -114,6 +114,10 @@ describe "Parser" do
"@#{id}"
end
+ def partial_name(name)
+ "PARTIAL:#{name}"
+ end
+
def path(*parts)
"PATH:#{parts.join("/")}"
end
@@ -218,11 +222,15 @@ describe "Parser" do
end
it "parses a partial" do
- ast_for("{{> foo }}").should == root { partial id("foo") }
+ ast_for("{{> foo }}").should == root { partial partial_name("foo") }
end
it "parses a partial with context" do
- ast_for("{{> foo bar}}").should == root { partial id("foo"), id("bar") }
+ ast_for("{{> foo bar}}").should == root { partial partial_name("foo"), id("bar") }
+ end
+
+ it "parses a partial with a complex name" do
+ ast_for("{{> shared/partial}}").should == root { partial partial_name("shared/partial") }
end
it "parses a comment" do
@@ -253,6 +261,130 @@ describe "Parser" do
end
end
+ it "parses an inverse ('else'-style) section" do
+ ast_for("{{#foo}} bar {{else}} baz {{/foo}}").should == root do
+ block do
+ mustache id("foo")
+
+ program do
+ content " bar "
+ end
+
+ inverse do
+ content " baz "
+ end
+ end
+ end
+ end
+
+ it "parses empty blocks" do
+ ast_for("{{#foo}}{{/foo}}").should == root do
+ block do
+ mustache id("foo")
+
+ program do
+ # empty program
+ end
+ end
+ end
+ end
+
+ it "parses empty blocks with empty inverse section" do
+ ast_for("{{#foo}}{{^}}{{/foo}}").should == root do
+ block do
+ mustache id("foo")
+
+ program do
+ # empty program
+ end
+
+ inverse do
+ # empty inverse
+ end
+ end
+ end
+ end
+
+ it "parses empty blocks with empty inverse ('else'-style) section" do
+ ast_for("{{#foo}}{{else}}{{/foo}}").should == root do
+ block do
+ mustache id("foo")
+
+ program do
+ # empty program
+ end
+
+ inverse do
+ # empty inverse
+ end
+ end
+ end
+ end
+
+ it "parses non-empty blocks with empty inverse section" do
+ ast_for("{{#foo}} bar {{^}}{{/foo}}").should == root do
+ block do
+ mustache id("foo")
+
+ program do
+ content " bar "
+ end
+
+ inverse do
+ # empty inverse
+ end
+ end
+ end
+ end
+
+ it "parses non-empty blocks with empty inverse ('else'-style) section" do
+ ast_for("{{#foo}} bar {{else}}{{/foo}}").should == root do
+ block do
+ mustache id("foo")
+
+ program do
+ content " bar "
+ end
+
+ inverse do
+ # empty inverse
+ end
+ end
+ end
+ end
+
+ it "parses empty blocks with non-empty inverse section" do
+ ast_for("{{#foo}}{{^}} bar {{/foo}}").should == root do
+ block do
+ mustache id("foo")
+
+ program do
+ # empty program
+ end
+
+ inverse do
+ content " bar "
+ end
+ end
+ end
+ end
+
+ it "parses empty blocks with non-empty inverse ('else'-style) section" do
+ ast_for("{{#foo}}{{else}} bar {{/foo}}").should == root do
+ block do
+ mustache id("foo")
+
+ program do
+ # empty program
+ end
+
+ inverse do
+ content " bar "
+ end
+ end
+ end
+ end
+
it "parses a standalone inverse section" do
ast_for("{{^foo}}bar{{/foo}}").should == root do
block do
diff --git a/spec/qunit_spec.js b/spec/qunit_spec.js
index 5c94253..5203cfc 100644
--- a/spec/qunit_spec.js
+++ b/spec/qunit_spec.js
@@ -456,12 +456,17 @@ test("providing a helpers hash", function() {
"Goodbye cruel world!", "helpers hash is available inside other blocks");
});
-test("in cases of conflict, the explicit hash wins", function() {
-
+test("in cases of conflict, helpers win", function() {
+ shouldCompileTo("{{{lookup}}}", [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], "helpers",
+ "helpers hash has precedence escaped expansion");
+ shouldCompileTo("{{lookup}}", [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], "helpers",
+ "helpers hash has precedence simple expansion");
});
test("the helpers hash is available is nested contexts", function() {
-
+ shouldCompileTo("{{#outer}}{{#inner}}{{helper}}{{/inner}}{{/outer}}",
+ [{'outer': {'inner': {'unused':[]}}}, {'helper': function() { return 'helper'; }}], "helper",
+ "helpers hash is available in nested contexts.");
});
suite("partials");
@@ -521,13 +526,21 @@ test("GH-14: a partial preceding a selector", function() {
shouldCompileToWithPartials(string, [hash, {}, {dude:dude}], true, "Dudes: Jeepers Creepers", "Regular selectors can follow a partial");
});
-test("Partials with literal paths", function() {
- var string = "Dudes: {{> [dude]}}";
+test("Partials with slash paths", function() {
+ var string = "Dudes: {{> shared/dude}}";
var dude = "{{name}}";
var hash = {name:"Jeepers", another_dude:"Creepers"};
- shouldCompileToWithPartials(string, [hash, {}, {dude:dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
+ shouldCompileToWithPartials(string, [hash, {}, {'shared/dude':dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
});
+test("Partials with integer path", function() {
+ var string = "Dudes: {{> 404}}";
+ var dude = "{{name}}";
+ var hash = {name:"Jeepers", another_dude:"Creepers"};
+ shouldCompileToWithPartials(string, [hash, {}, {404:dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
+});
+
+
suite("String literal parameters");
test("simple literals work", function() {
@@ -730,6 +743,23 @@ test("each with @index", function() {
equal(result, "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", "The @index variable is used");
});
+test("data passed to helpers", function() {
+ var string = "{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}";
+ var hash = {letters: ['a', 'b', 'c']};
+
+ var template = CompilerContext.compile(string);
+ var result = template(hash, {
+ data: {
+ exclaim: '!'
+ }
+ });
+ equal(result, 'a!b!c!');
+});
+
+Handlebars.registerHelper('detectDataInsideEach', function(options) {
+ return options.data && options.data.exclaim;
+});
+
test("log", function() {
var string = "{{log blah}}";
var hash = { blah: "whee" };
@@ -1245,3 +1275,9 @@ test("bug reported by @fat where lambdas weren't being properly resolved", funct
var output = "<strong>This is a slightly more complicated blah.</strong>.\n\nCheck this out:\n\n<ul>\n\n<li class=one>@fat</li>\n\n<li class=two>@dhg</li>\n\n<li class=three>@sayrer</li>\n</ul>.\n\n";
shouldCompileTo(string, data, output);
});
+
+test("Passing falsy values to Handlebars.compile throws an error", function() {
+ shouldThrow(function() {
+ CompilerContext.compile(null);
+ }, "You must pass a string to Handlebars.compile. You passed null");
+});
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index cf73801..605433c 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,32 +1,5 @@
require "v8"
-# Monkey patches due to bugs in RubyRacer
-class V8::JSError
- def initialize(try, to)
- @to = to
- begin
- super(initialize_unsafe(try))
- rescue Exception => e
- # Original code does not make an Array here
- @boundaries = [Boundary.new(:rbframes => e.backtrace)]
- @value = e
- super("BUG! please report. JSError#initialize failed!: #{e.message}")
- end
- end
-
- def parse_js_frames(try)
- raw = @to.rb(try.StackTrace())
- if raw && !raw.empty?
- raw.split("\n")[1..-1].tap do |frames|
- # Original code uses strip!, and the frames are not guaranteed to be strippable
- frames.each {|frame| frame.strip.chomp!(",")}
- end
- else
- []
- end
- end
-end
-
module Handlebars
module Spec
def self.js_backtrace(context)
diff --git a/spec/tokenizer_spec.rb b/spec/tokenizer_spec.rb
index 7a771ba..cb7f6eb 100644
--- a/spec/tokenizer_spec.rb
+++ b/spec/tokenizer_spec.rb
@@ -132,24 +132,24 @@ describe "Tokenizer" do
result[4].should be_token("CONTENT", " baz")
end
- it "tokenizes a partial as 'OPEN_PARTIAL ID CLOSE'" do
+ it "tokenizes a partial as 'OPEN_PARTIAL PARTIAL_NAME CLOSE'" do
result = tokenize("{{> foo}}")
- result.should match_tokens(%w(OPEN_PARTIAL ID CLOSE))
+ result.should match_tokens(%w(OPEN_PARTIAL PARTIAL_NAME CLOSE))
end
- it "tokenizes a partial with context as 'OPEN_PARTIAL ID ID CLOSE'" do
+ it "tokenizes a partial with context as 'OPEN_PARTIAL PARTIAL_NAME ID CLOSE'" do
result = tokenize("{{> foo bar }}")
- result.should match_tokens(%w(OPEN_PARTIAL ID ID CLOSE))
+ result.should match_tokens(%w(OPEN_PARTIAL PARTIAL_NAME ID CLOSE))
end
- it "tokenizes a partial without spaces as 'OPEN_PARTIAL ID CLOSE'" do
+ it "tokenizes a partial without spaces as 'OPEN_PARTIAL PARTIAL_NAME CLOSE'" do
result = tokenize("{{>foo}}")
- result.should match_tokens(%w(OPEN_PARTIAL ID CLOSE))
+ result.should match_tokens(%w(OPEN_PARTIAL PARTIAL_NAME CLOSE))
end
- it "tokenizes a partial space at the end as 'OPEN_PARTIAL ID CLOSE'" do
+ it "tokenizes a partial space at the end as 'OPEN_PARTIAL PARTIAL_NAME CLOSE'" do
result = tokenize("{{>foo }}")
- result.should match_tokens(%w(OPEN_PARTIAL ID CLOSE))
+ result.should match_tokens(%w(OPEN_PARTIAL PARTIAL_NAME CLOSE))
end
it "tokenizes a comment as 'COMMENT'" do
diff --git a/src/handlebars.l b/src/handlebars.l
index 87dce26..04c7c4c 100644
--- a/src/handlebars.l
+++ b/src/handlebars.l
@@ -1,5 +1,5 @@
-%x mu emu com
+%x mu emu com par
%%
@@ -19,7 +19,7 @@
<com>[\s\S]*?"--}}" { yytext = yytext.substr(0, yyleng-4); this.popState(); return 'COMMENT'; }
-<mu>"{{>" { return 'OPEN_PARTIAL'; }
+<mu>"{{>" { this.begin("par"); return 'OPEN_PARTIAL'; }
<mu>"{{#" { return 'OPEN_BLOCK'; }
<mu>"{{/" { return 'OPEN_ENDBLOCK'; }
<mu>"{{^" { return 'OPEN_INVERSE'; }
@@ -46,6 +46,8 @@
<mu>[a-zA-Z0-9_$-]+/[=}\s\/.] { return 'ID'; }
<mu>'['[^\]]*']' { yytext = yytext.substr(1, yyleng-2); return 'ID'; }
<mu>. { return 'INVALID'; }
+<par>\s+ { /*ignore whitespace*/ }
+<par>[a-zA-Z0-9_$-/]+ { this.popState(); return 'PARTIAL_NAME'; }
<INITIAL,mu><<EOF>> { return 'EOF'; }
diff --git a/src/handlebars.yy b/src/handlebars.yy
index 70b7777..7ab0153 100644
--- a/src/handlebars.yy
+++ b/src/handlebars.yy
@@ -7,8 +7,11 @@ root
;
program
- : statements simpleInverse statements { $$ = new yy.ProgramNode($1, $3); }
+ : simpleInverse statements { $$ = new yy.ProgramNode([], $2); }
+ | statements simpleInverse statements { $$ = new yy.ProgramNode($1, $3); }
+ | statements simpleInverse { $$ = new yy.ProgramNode($1, []); }
| statements { $$ = new yy.ProgramNode($1); }
+ | simpleInverse { $$ = new yy.ProgramNode([], []); }
| "" { $$ = new yy.ProgramNode([]); }
;
@@ -45,8 +48,8 @@ mustache
partial
- : OPEN_PARTIAL path CLOSE { $$ = new yy.PartialNode($2); }
- | OPEN_PARTIAL path path CLOSE { $$ = new yy.PartialNode($2, $3); }
+ : OPEN_PARTIAL partialName CLOSE { $$ = new yy.PartialNode($2); }
+ | OPEN_PARTIAL partialName path CLOSE { $$ = new yy.PartialNode($2, $3); }
;
simpleInverse
@@ -91,6 +94,10 @@ hashSegment
| ID EQUALS DATA { $$ = [$1, new yy.DataNode($3)]; }
;
+partialName
+ : PARTIAL_NAME { $$ = new yy.PartialNameNode($1); }
+ ;
+
path
: pathSegments { $$ = new yy.IdNode($1); }
;