Fix a number of outstanding issues:

* {{}} escape their contents, {{{}}} and {{& }} do not
* Add support in the parser, tokenizer and AST for partials
  with context (support is still not there in the runtime)
* Fix some inconsistencies with the old behavior involving
  the correct printing of null and undefined
* Add Handlebars.Exception
* Fixed an issue involving ./foo and this/foo
* Fleshed out helperMissing in the specs (this will be
  moved out into handlebars proper once registerHelper
  and registerPartial are added)
This commit is contained in:
wycats
2010-12-02 01:13:24 -05:00
parent 9846fb8a28
commit 762329913d
11 changed files with 118 additions and 14 deletions
+3
View File
@@ -6,6 +6,9 @@ if(exports) {
Handlebars.PrintVisitor = require("handlebars/printer").PrintVisitor; Handlebars.PrintVisitor = require("handlebars/printer").PrintVisitor;
Handlebars.Parser = require("handlebars/parser").parser; Handlebars.Parser = require("handlebars/parser").parser;
Handlebars.Runtime = require("handlebars/runtime").Runtime; Handlebars.Runtime = require("handlebars/runtime").Runtime;
Handlebars.Utils = require("handlebars/utils").Utils;
Handlebars.SafeString = require("handlebars/utils").SafeString;
Handlebars.Exception = require("handlebars/utils").Exception;
} else { } else {
Handlebars.Parser = handlebars; Handlebars.Parser = handlebars;
} }
+7 -5
View File
@@ -8,15 +8,17 @@ Handlebars.AST.ProgramNode = function(statements, inverse) {
this.inverse = inverse; this.inverse = inverse;
}; };
Handlebars.AST.MustacheNode = function(params) { Handlebars.AST.MustacheNode = function(params, unescaped) {
this.type = "mustache"; this.type = "mustache";
this.id = params[0]; this.id = params[0];
this.params = params.slice(1); this.params = params.slice(1);
this.escaped = !unescaped;
}; };
Handlebars.AST.PartialNode = function(id) { Handlebars.AST.PartialNode = function(id, context) {
this.type = "partial"; this.type = "partial";
this.id = id; this.id = id;
this.context = context;
}; };
Handlebars.AST.BlockNode = function(mustache, program) { Handlebars.AST.BlockNode = function(mustache, program) {
@@ -39,7 +41,7 @@ Handlebars.AST.IdNode = function(parts) {
var part = parts[i]; var part = parts[i];
if(part === "..") { depth++; } if(part === "..") { depth++; }
else if(part === "." || part === "this") { break; } else if(part === "." || part === "this") { continue; }
else { dig.push(part) } else { dig.push(part) }
} }
+4 -1
View File
@@ -96,7 +96,10 @@ Handlebars.HandlebarsLexer.prototype.lex = function() {
return "OPEN_ENDBLOCK"; return "OPEN_ENDBLOCK";
} else if(peek === "^") { } else if(peek === "^") {
this.getchar(); this.getchar();
return "OPEN_INVERSE" return "OPEN_INVERSE";
} else if(peek === "{" || peek === "&") {
this.getchar();
return "OPEN_UNESCAPED";
} else if(peek === "!") { } else if(peek === "!") {
this.readchar(); this.readchar();
this.setupLex(); // reset the lexer state so the yytext is the comment only this.setupLex(); // reset the lexer state so the yytext is the comment only
+3 -1
View File
@@ -71,7 +71,9 @@ Handlebars.PrintVisitor.prototype.mustache = function(mustache) {
}; };
Handlebars.PrintVisitor.prototype.partial = function(partial) { Handlebars.PrintVisitor.prototype.partial = function(partial) {
return this.pad("{{> " + this.accept(partial.id) + " }}"); var content = this.accept(partial.id);
if(partial.context) { content = content + " " + this.accept(partial.context); }
return this.pad("{{> " + content + " }}");
}; };
Handlebars.PrintVisitor.prototype.STRING = function(string) { Handlebars.PrintVisitor.prototype.STRING = function(string) {
+8 -3
View File
@@ -70,7 +70,7 @@ Handlebars.Runtime = function(context, fallback, stack) {
var newContext = this.context = new Handlebars.Context; var newContext = this.context = new Handlebars.Context;
if(context.isContext) { if(context && context.isContext) {
newContext.data = context.data; newContext.data = context.data;
newContext.fallback = context.fallback; newContext.fallback = context.fallback;
} else { } else {
@@ -103,17 +103,22 @@ Handlebars.Runtime.prototype = {
mustache: function(mustache) { mustache: function(mustache) {
var idObj = this.accept(mustache.id); var idObj = this.accept(mustache.id);
var params = mustache.params; var params = mustache.params;
var buf;
for(var i=0, l=params.length; i<l; i++) { for(var i=0, l=params.length; i<l; i++) {
params[i] = this.accept(params[i]).data; params[i] = this.accept(params[i]).data;
} }
if(toString.call(idObj.data) === "[object Function]") { if(toString.call(idObj.data) === "[object Function]") {
this.buffer = this.buffer + idObj.data.apply(this.wrapContext(), params); buf = idObj.data.apply(this.wrapContext(), params);
} else { } else {
if(params.length) { throw new Error(mustache.id.parts.join("/") + " is not a Function, so you cannot have Function parameters"); } if(params.length) { throw new Error(mustache.id.parts.join("/") + " is not a Function, so you cannot have Function parameters"); }
this.buffer = this.buffer + idObj.data; buf = idObj.data;
} }
if(buf && mustache.escaped) { buf = Handlebars.Utils.escapeExpression(buf); }
this.buffer = this.buffer + (buf == null ? '' : buf);
}, },
block: function(block) { block: function(block) {
+54
View File
@@ -0,0 +1,54 @@
if(exports) {
var Handlebars = {};
}
Handlebars.Exception = function(message) {
this.message = message;
};
// Build out our basic SafeString type
Handlebars.SafeString = function(string) {
this.string = string;
}
Handlebars.SafeString.prototype.toString = function() {
return this.string.toString();
}
Handlebars.Utils = {
escapeExpression: function(string) {
// don't escape SafeStrings, since they're already safe
if (string instanceof Handlebars.SafeString) {
return string.toString();
}
else if (string === null) {
string = "";
}
return string.toString().replace(/&(?!\w+;)|["\\<>]/g, function(str) {
switch(str) {
case "&":
return "&amp;";
break;
case '"':
return "\"";
case "\\":
return "\\\\";
break;
case "<":
return "&lt;";
break;
case ">":
return "&gt;";
break;
default:
return str;
}
});
}
}
if(exports) {
exports.Utils = Handlebars.Utils;
exports.SafeString = Handlebars.SafeString;
exports.Exception = Handlebars.Exception;
}
+12 -2
View File
@@ -64,8 +64,10 @@ describe "Parser" do
pad("{{ #{id} [#{params.join(", ")}] }}") pad("{{ #{id} [#{params.join(", ")}] }}")
end end
def partial(id) def partial(id, context = nil)
pad("{{> #{id} }}") content = id.dup
content << " #{context}" if context
pad("{{> #{content} }}")
end end
def comment(comment) def comment(comment)
@@ -97,6 +99,10 @@ describe "Parser" do
ast_for("{{foo/bar}}").should == program { mustache path("foo", "bar") } ast_for("{{foo/bar}}").should == program { mustache path("foo", "bar") }
end end
it "parses mustaches with this/foo" do
ast_for("{{this/foo}}").should == program { mustache id("foo") }
end
it "parses mustaches with parameters" do it "parses mustaches with parameters" do
ast_for("{{foo bar}}").should == program { mustache id("foo"), id("bar") } ast_for("{{foo bar}}").should == program { mustache id("foo"), id("bar") }
end end
@@ -116,6 +122,10 @@ describe "Parser" do
ast_for("{{> foo }}").should == program { partial id("foo") } ast_for("{{> foo }}").should == program { partial id("foo") }
end end
it "parses a partial with context" do
ast_for("{{> foo bar}}").should == program { partial id("foo"), id("bar") }
end
it "parses a comment" do it "parses a comment" do
ast_for("{{! this is a comment }}").should == program do ast_for("{{! this is a comment }}").should == program do
comment " this is a comment " comment " this is a comment "
+10 -1
View File
@@ -1,11 +1,20 @@
module("basic context"); module("basic context");
var helperMissing = function(context, fn) { var helperMissing = function(context, fn) {
var ret = "";
if(context === true) { if(context === true) {
return fn(this); return fn(this);
} else if(context === false) { } else if(context === false) {
return ""; return "";
} } else if(Object.prototype.toString.call(context) === "[object Array]") {
for(var i=0, j=context.length; i<j; i++) {
ret = ret + fn(context[i]);
}
return ret;
} else {
return fn(context);
}
} }
var shouldCompileTo = function(string, hash, expected, message) { var shouldCompileTo = function(string, hash, expected, message) {
+1
View File
@@ -40,6 +40,7 @@ module Handlebars
context.load('lib/handlebars/printer.js') context.load('lib/handlebars/printer.js')
context.load('lib/handlebars/parser.js') context.load('lib/handlebars/parser.js')
context.load('lib/handlebars/runtime.js') context.load('lib/handlebars/runtime.js')
context.load('lib/handlebars/utils.js')
context.load('lib/handlebars.js') context.load('lib/handlebars.js')
end end
end end
+13 -1
View File
@@ -36,12 +36,19 @@ describe "Tokenizer" do
result[1].should be_token("ID", "foo") result[1].should be_token("ID", "foo")
end end
it "tokenizes a path as 'OPEN ID CLOSE'" do it "tokenizes a path as 'OPEN (ID SEP)* ID CLOSE'" do
result = tokenize("{{../foo/bar}}") result = tokenize("{{../foo/bar}}")
result.should match_tokens(%w(OPEN ID SEP ID SEP ID CLOSE)) result.should match_tokens(%w(OPEN ID SEP ID SEP ID CLOSE))
result[1].should be_token("ID", "..") result[1].should be_token("ID", "..")
end end
it "tokenizes a path with this/foo as OPEN ID SEP ID CLOSE" do
result = tokenize("{{this/foo}}")
result.should match_tokens(%w(OPEN ID SEP ID CLOSE))
result[1].should be_token("ID", "this")
result[3].should be_token("ID", "foo")
end
it "tokenizes a simple mustahe with spaces as 'OPEN ID CLOSE'" do it "tokenizes a simple mustahe with spaces as 'OPEN ID CLOSE'" do
result = tokenize("{{ foo }}") result = tokenize("{{ foo }}")
result.should match_tokens(%w(OPEN ID CLOSE)) result.should match_tokens(%w(OPEN ID CLOSE))
@@ -60,6 +67,11 @@ describe "Tokenizer" do
result.should match_tokens(%w(OPEN_PARTIAL ID CLOSE)) result.should match_tokens(%w(OPEN_PARTIAL ID CLOSE))
end end
it "tokenizes a partial with context as 'OPEN_PARTIAL ID ID CLOSE'" do
result = tokenize("{{> foo bar }}")
result.should match_tokens(%w(OPEN_PARTIAL ID ID CLOSE))
end
it "tokenizes a partial without spaces as 'OPEN_PARTIAL ID CLOSE'" do it "tokenizes a partial without spaces as 'OPEN_PARTIAL ID CLOSE'" do
result = tokenize("{{>foo}}") result = tokenize("{{>foo}}")
result.should match_tokens(%w(OPEN_PARTIAL ID CLOSE)) result.should match_tokens(%w(OPEN_PARTIAL ID CLOSE))
+3
View File
@@ -34,10 +34,13 @@ closeBlock
mustache mustache
: OPEN inMustache CLOSE { $$ = new yy.MustacheNode($2) } : OPEN inMustache CLOSE { $$ = new yy.MustacheNode($2) }
| OPEN_UNESCAPED inMustache CLOSE { $$ = new yy.MustacheNode($2, true) }
; ;
partial partial
: OPEN_PARTIAL path CLOSE { $$ = new yy.PartialNode($2) } : OPEN_PARTIAL path CLOSE { $$ = new yy.PartialNode($2) }
| OPEN_PARTIAL path path CLOSE { $$ = new yy.PartialNode($2, $3) }
; ;
simpleInverse simpleInverse