diff options
-rw-r--r-- | Gemfile.lock | 20 | ||||
-rw-r--r-- | lib/handlebars/base.js | 23 | ||||
-rw-r--r-- | lib/handlebars/compiler/ast.js | 16 | ||||
-rw-r--r-- | lib/handlebars/compiler/base.js | 10 | ||||
-rw-r--r-- | lib/handlebars/compiler/compiler.js | 20 | ||||
-rw-r--r-- | lib/handlebars/compiler/printer.js | 6 | ||||
-rw-r--r-- | spec/parser_spec.rb | 136 | ||||
-rw-r--r-- | spec/qunit_spec.js | 48 | ||||
-rw-r--r-- | spec/spec_helper.rb | 27 | ||||
-rw-r--r-- | spec/tokenizer_spec.rb | 16 | ||||
-rw-r--r-- | src/handlebars.l | 6 | ||||
-rw-r--r-- | src/handlebars.yy | 13 |
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); } ; |