This commit is contained in:
Jason Davies
2010-09-27 17:38:41 +01:00
5 changed files with 112 additions and 72 deletions
+6 -2
View File
@@ -75,7 +75,7 @@ Handlebars.js also adds the ability to define block helpers. Block helpers are f
// <li><a href="/people/2">Yehuda</a></li>
// </ul>
Whenever the block helper is called it is given a single parameter, the compiled contents of the block. Inside of the block helper the value of `this` is the current context. The context includes a method `__get__` that helps to translate paths into values for use within the helper.
Whenever the block helper is called it is given two parameters, the argument that is passed to the helper, or the current context if no argument is passed and the compiled contents of the block. Inside of the block helper the value of `this` is the current context, wrapped to include a method named `__get__` that helps translate paths into values within the helpers.
### Partials
@@ -106,12 +106,16 @@ A node.js compatible command-line tool is included in the lib folder. compiler.j
Performance
-----------
In a rough performance test, precompiled Handlebars.js templates rendered in about half the time of Mustache templates. It would be a shame if it were any other way, since they were precompiled, but the difference in architecture does have some big performance advantages.
In a rough performance test, precompiled Handlebars.js templates rendered in about half the time of Mustache templates. It would be a shame if it were any other way, since they were precompiled, but the difference in architecture does have some big performance advantages. Justin Marney, a.k.a. [gotascii](http://github.com/gotascii), confirmed that with an [independent test](http://sorescode.com/2010/09/12/benchmarks.html).
Known Issues
------------
* Handlebars.js can be a bit cryptic when there's an error during compilation, and it can be even more cryptic when there's an error while rendering.
Handlebars in the Wild
-----------------
* Don Park wrote an Express.js view engine adapter for Handlebars called [hbs](http://github.com/donpark/hbs)
Helping Out
-----------
If you notice any problems, please report them to the GitHub issue tracker at [http://github.com/wycats/handlebars.js/issues](http://github.com/wycats/handlebars.js/issues). Feel free to contact commondream or wycats through GitHub with any other questions or feature requests. To submit changes fork the project and send a pull request.
-8
View File
@@ -1,8 +0,0 @@
* README
* Rationale (how it's different from mustache, and why)
* Refactor blocks/inverted sections so shared code isn't copied and pasted
* Add support for {{^}} when helperMissing is in play.
* Figure out how to allow detection of first/last/iteration when enumerating over arrays.
* Easier way to push context within a block helper, so that it can pass a built context to its function.
+77 -58
View File
@@ -1,20 +1,27 @@
var Handlebars = {
compilerCache: {},
compile: function(string) {
var fnBody = Handlebars.compileFunctionBody(string);
var fn = new Function("context", "fallback", "Handlebars", fnBody);
return function(context, fallback) { return fn(context, fallback, Handlebars); };
if (Handlebars.compilerCache[string] == null) {
var fnBody = Handlebars.compileFunctionBody(string);
var fn = new Function("context", "fallback", "Handlebars", fnBody);
Handlebars.compilerCache[string] =
function(context, fallback) { return fn(context, fallback, Handlebars); };
}
return Handlebars.compilerCache[string];
},
compileToString: function(string) {
var fnBody = Handlebars.compileFunctionBody(string);
return "function(context, fallback) { " + fnBody + "}";
},
compileFunctionBody: function(string) {
var compiler = new Handlebars.Compiler(string);
compiler.compile();
var compiler = new Handlebars.Compiler(string);
compiler.compile();
return "fallback = fallback || {}; var stack = [];" + compiler.fn;
return "fallback = fallback || {}; var stack = [];" + compiler.fn;
},
isFunction: function(fn) {
@@ -62,53 +69,57 @@ var Handlebars = {
});
},
compiledPartials: {},
compilePartial: function(partial) {
var compiled = Handlebars.compiledPartials[partial];
if (compiled == null) {
if (Handlebars.isFunction(partial)) {
compiled = partial;
} else {
compiled = Handlebars.compile(partial);
}
Handlebars.compiledPartials[partial] = compiled;
if (Handlebars.isFunction(partial)) {
compiled = partial;
} else {
compiled = Handlebars.compile(partial);
}
return compiled;
},
evalExpression: function(path, context, stack) {
var parsedPath = Handlebars.parsePath(path);
var depth = parsedPath[0];
var parts = parsedPath[1];
if (depth > stack.length) {
context = null;
} else if (depth > 0) {
context = stack[stack.length - depth];
}
for (var i = 0; i < parts.length && context !== undefined; i++) {
context = context[parts[i]];
}
return context;
},
buildContext: function(context, stack) {
var contextWrapper = function(stack) {
this.__stack__ = stack;
var ContextWrapper = function(stack) {
this.__stack__ = stack.slice(0);
this.__get__ = function(path) {
var context = this;
var parsedPath = Handlebars.parsePath(path);
var depth = parsedPath[0];
var parts = parsedPath[1];
if (depth > 0) {
context = this.__stack__[stack.length - depth];
}
for (var i = 0; i < parts.length; i++) {
context = context[parts[i]];
}
return context;
return Handlebars.evalExpression(path, this, this.__stack__);
};
};
contextWrapper.prototype = context;
return new contextWrapper(stack);
ContextWrapper.prototype = context;
return new ContextWrapper(stack);
},
// Returns a two element array containing the numbers of contexts to back up the stack and
// spot to memoize paths to speed up loops and subsequent parses
pathPatterns: {},
// returns a two element array containing the numbers of contexts to back up the stack and
// the properties to dig into on the current context
//
// For example, if the path is "../../alan/name", the result will be [2, ["alan", "name"]].
// for example, if the path is "../../alan/name", the result will be [2, ["alan", "name"]].
parsePath: function(path) {
if (path == null) {
return [0, []];
} else if (Handlebars.pathPatterns[path] != null) {
return Handlebars.pathPatterns[path];
}
var parts = path.split("/");
@@ -134,8 +145,10 @@ var Handlebars = {
dig.push(parts[i]);
}
}
return [depth, dig];
var ret = [depth, dig];
Handlebars.pathPatterns[path] = ret;
return ret;
},
isEmpty: function(value) {
@@ -166,20 +179,26 @@ var Handlebars = {
var out = "";
if (Handlebars.isFunction(lookup)) {
out = out + lookup.call(context, arg, fn);
if (notFn != null && Handlebars.isFunction(lookup.not)) {
out = out + lookup.not.call(context, arg, notFn);
}
}
else if (typeof lookup != 'undefined') {
out = out + Handlebars.helperMissing.call(arg, lookup, fn);
else {
if (!Handlebars.isEmpty(lookup)) {
out = out + Handlebars.helperMissing.call(arg, lookup, fn);
}
if (notFn != null) {
out = out + Handlebars.helperMissing.not.call(arg, lookup, notFn);
}
}
if (notFn != null && Handlebars.isFunction(lookup.not)) {
out = out + lookup.not.call(context, arg, notFn);
}
return out;
},
handleExpression: function(lookup, context, arg, isEscaped) {
var out = "";
if (Handlebars.isFunction(lookup)) {
out = out + Handlebars.filterOutput(lookup.call(context, arg), isEscaped);
} else if(!Handlebars.isEmpty(lookup)) {
@@ -243,6 +262,9 @@ Handlebars.helperMissing = function(object, fn) {
return fn(object);
}
};
Handlebars.helperMissing.not = function(context, fn) {
return fn(context);
}
Handlebars.Compiler.prototype = {
getChar: function(n) {
@@ -330,15 +352,12 @@ Handlebars.Compiler.prototype = {
var depth = parsed[0];
var parts = parsed[1];
var paramExpr = "";
for(var i = 0; i < parts.length; i++) {
paramExpr += "['" + parts[i] + "']";
}
if (depth > 0) {
return "( stack[stack.length - " + depth + "]" + paramExpr + ")";
if (depth > 0 || parts.length > 1) {
return "(Handlebars.evalExpression('" + param + "', context, stack))";
} else if (parts.length == 1) {
return "(context['" + parts[0] + "'] || fallback['" + parts[0] + "'])";
} else {
return "( context" + paramExpr + " != null ? context" + paramExpr + " : fallback" + paramExpr + " )";
return "(context || fallback)";
}
},
@@ -364,16 +383,14 @@ Handlebars.Compiler.prototype = {
},
addBlock: function(mustache, param, parts) {
// set up the stack before the new compiler starts
this.fn += "stack.push(context);";
var compiler = this.compileToEndOfBlock(mustache);
var result = compiler.fn;
// each function made internally needs a unique IDs. These are locals, so they
// don't need to be globally unique, just per compiler
var fnId = "fn" + this.pointer.toString();
this.fn += "var proxy = Handlebars.buildContext(" + param + ", stack);";
this.fn += "var wrappedContext = Handlebars.buildContext(context);";
this.fn += "var wrappedContext = Handlebars.buildContext(context, stack);";
this.fn += "stack.push(context);";
this.fn += "var " + fnId + " = function(context) {" + result + "}; ";
this.fn += "lookup = " + this.lookupFor(mustache) + "; ";
@@ -384,7 +401,7 @@ Handlebars.Compiler.prototype = {
else {
this.fn += " var " + fnId + "Not = null;";
}
this.fn += "out = out + Handlebars.handleBlock(lookup, wrappedContext, proxy, " + fnId + ", " + fnId + "Not);"
this.fn += "out = out + Handlebars.handleBlock(lookup, wrappedContext, " + param + ", " + fnId + ", " + fnId + "Not);"
this.fn += "stack.pop();";
this.openBlock = false;
@@ -443,9 +460,11 @@ Handlebars.Compiler.prototype = {
return;
} else if (this.partial) {
this.addPartial(mustache, param)
this.partial = false;
return;
} else if (this.inverted) {
this.addInvertedSection(mustache);
this.inverted = false;
return;
} else if(this.openBlock) {
this.addBlock(mustache, param, parts)
+28 -2
View File
@@ -103,6 +103,11 @@ test("bad idea nested paths", function() {
shouldCompileTo(string, hash, "world world world ", "Same context (.) is ignored in paths");
});
test("complex but empty paths", function() {
shouldCompileTo("{{person/name}}", {person: {name: null}}, "");
shouldCompileTo("{{person/name}}", {person: {}}, "");
});
test("this keyword in paths", function() {
var string = "{{#goodbyes}}{{this}}{{/goodbyes}}";
var hash = {goodbyes: ["goodbye", "Goodbye", "GOODBYE"]};
@@ -192,6 +197,15 @@ test("helper with complex lookup", function() {
shouldCompileTo(string, [hash, fallback], "<a href='/root/goodbye'>Goodbye</a>")
});
test("helper with complex lookup and nested template", function() {
var string = "{{#goodbyes}}{{#link}}{{text}}{{/link}}{{/goodbyes}}";
var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]};
var fallback = {link: function (context, fn) {
return "<a href='" + this.__get__("../prefix") + "/" + this.url + "'>" + fn(this) + "</a>";
}};
shouldCompileTo(string, [hash, fallback], "<a href='/root/goodbye'>Goodbye</a>")
});
test("block with deep nested complex lookup", function() {
var string = "{{#outer}}Goodbye {{#inner}}cruel {{../../omg}}{{/inner}}{{/outer}}";
var hash = {omg: "OMG!", outer: [{ inner: [{ text: "goodbye" }] }] };
@@ -253,6 +267,11 @@ test("nested block helpers", function() {
});
test("block inverted sections", function() {
shouldCompileTo("{{#people}}{{name}}{{^}}{{../none}}{{/people}}", {none: "No people"},
"No people");
});
test("block helper inverted sections", function() {
var string = "{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}"
var list = function(context, fn) {
if (context.length > 0) {
@@ -306,7 +325,7 @@ test("basic partials", function() {
});
test("partials with context", function() {
var string = "Dudes: {{> dude dudes}}";
var string = "Dudes: {{>dude dudes}}";
var partial = "{{#this}}{{name}} ({{url}}) {{/this}}";
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
shouldCompileTo(string, [hash, {partials: {dude: partial}}], "Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
@@ -314,7 +333,7 @@ test("partials with context", function() {
});
test("partial in a partial", function() {
var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}";
var string = "Dudes: {{#dudes}}{{>dude}}{{/dudes}}";
var dude = "{{name}} {{> url}} ";
var url = "<a href='{{url}}'>{{url}}</a>";
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
@@ -328,6 +347,13 @@ test("rendering undefined partial throws an exception", function() {
}, Handlebars.Exception, "Should throw exception");
});
test("GH-14: a partial preceding a selector", function() {
var string = "Dudes: {{>dude}} {{another_dude}}";
var dude = "{{name}}";
var hash = {name:"Jeepers", another_dude:"Creepers"};
shouldCompileTo(string, [hash, {partials: {dude:dude}}], "Dudes: Jeepers Creepers", "Regular selectors can follow a partial");
});
module("safestring");
test("constructing a safestring from a string and checking its type", function() {
+1 -2
View File
@@ -79,9 +79,8 @@ var data = {
]
}
var fn = Handlebars.compile(tmpl);
var test1 = function() {
fn(data, {partials: partials});
Handlebars.compile(tmpl)(data, {partials: partials});
}
var test2 = function() {
Mustache.to_html(tmpl, data, partials);