Add optimized compiled version of handlebars, which should be significantly faster. Use Handlebars.VM.compile instead of Handlebars.compile to use the optimized version.

Major TODOS:

* clean up a bunch of code duplication in the compiler
* reorganize the compiler
* add support for debug symbols which would make it possible
  to provide information about what part of the source caused
  a runtime error.
This commit is contained in:
wycats
2010-12-18 23:27:34 -08:00
parent e15d675a64
commit b418ef6eb2
5 changed files with 632 additions and 33 deletions
+40 -26
View File
@@ -43,7 +43,7 @@ test("compiling with a basic context", function() {
});
test("comments", function() {
shouldCompileTo("{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!",
shouldCompileTo("{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!",
{cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!",
"comments are ignored");
});
@@ -90,7 +90,7 @@ test("escaping expressions", function() {
test("functions returning safestrings shouldn't be escaped", function() {
var hash = {awesome: function() { return new Handlebars.SafeString("&\"\\<>"); }};
shouldCompileTo("{{awesome}}", hash, '&\"\\<>',
shouldCompileTo("{{awesome}}", hash, '&\"\\<>',
"functions returning safestrings aren't escaped");
});
@@ -100,7 +100,7 @@ test("functions", function() {
});
test("functions with context argument", function() {
shouldCompileTo("{{awesome frank}}",
shouldCompileTo("{{awesome frank}}",
{awesome: function(context) { return context; },
frank: "Frank"},
"Frank", "functions are called with context arguments");
@@ -121,7 +121,7 @@ test("--- TODO --- bad idea nested paths", function() {
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
shouldThrow(function() {
Handlebars.compile("{{#goodbyes}}{{../name/../name}}{{/goodbyes}}")(hash);
}, Handlebars.Exception,
}, Handlebars.Exception,
"Cannot jump (..) into previous context after moving into a context.");
var string = "{{#goodbyes}}{{.././world}} {{/goodbyes}}";
@@ -140,8 +140,8 @@ test("complex but empty paths", function() {
test("this keyword in paths", function() {
var string = "{{#goodbyes}}{{this}}{{/goodbyes}}";
var hash = {goodbyes: ["goodbye", "Goodbye", "GOODBYE"]};
shouldCompileTo(string, hash, "goodbyeGoodbyeGOODBYE",
"This keyword in paths evaluates to current context");
shouldCompileTo(string, hash, "goodbyeGoodbyeGOODBYE",
"This keyword in paths evaluates to current context");
string = "{{#hellos}}{{this/text}}{{/hellos}}"
hash = {hellos: [{text: "hello"}, {text: "Hello"}, {text: "HELLO"}]};
@@ -161,7 +161,7 @@ test("inverted section with false value", function() {
var hash = {goodbyes: false};
shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value is false.");
});
test("inverted section with empty set", function() {
var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}";
var hash = {goodbyes: []};
@@ -218,10 +218,10 @@ test("block with complex lookup", function() {
});
test("helper with complex lookup", function() {
var string = "{{#goodbyes}}{{{link}}}{{/goodbyes}}"
var string = "{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}"
var hash = {prefix: "/root", goodbyes: [{text: "Goodbye", url: "goodbye"}]};
var fallback = {link: function() {
return "<a href='" + this.__get__("../prefix") + "/" + this.url + "'>" + this.text + "</a>"
var fallback = {link: function(prefix) {
return "<a href='" + prefix + "/" + this.url + "'>" + this.text + "</a>"
}};
shouldCompileTo(string, [hash, fallback], "<a href='/root/goodbye'>Goodbye</a>")
});
@@ -229,11 +229,11 @@ test("helper with complex lookup", function() {
test("helper block with complex lookup expression", function() {
var string = "{{#goodbyes}}{{../name}}{{/goodbyes}}"
var hash = {name: "Alan"};
var fallback = {goodbyes: function(context, fn) {
var fallback = {goodbyes: function(fn) {
var out = "";
var byes = ["Goodbye", "goodbye", "GOODBYE"];
for (var i = 0,j = byes.length; i < j; i++) {
out += byes[i] + " " + fn(context) + "! ";
out += byes[i] + " " + fn(this) + "! ";
}
return out;
}};
@@ -241,10 +241,10 @@ test("helper block with complex lookup expression", function() {
});
test("helper with complex lookup and nested template", function() {
var string = "{{#goodbyes}}{{#link}}{{text}}{{/link}}{{/goodbyes}}";
var string = "{{#goodbyes}}{{#link ../prefix}}{{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>";
var fallback = {link: function (prefix, fn) {
return "<a href='" + prefix + "/" + this.url + "'>" + fn(this) + "</a>";
}};
shouldCompileTo(string, [hash, fallback], "<a href='/root/goodbye'>Goodbye</a>")
});
@@ -272,10 +272,10 @@ test("block helper staying in the same context", function() {
equal(result, "<form><p>Yehuda</p></form>");
});
test("block helper should have wrapped context in this", function() {
test("block helper should have context in this", function() {
var source = "<ul>{{#people}}<li>{{#link}}{{name}}{{/link}}</li>{{/people}}</ul>";
var link = function(context, fn) {
return '<a href="/people/' + this.__get__("id") + '">' + fn(this) + '</a>';
var link = function(fn) {
return '<a href="/people/' + this.id + '">' + fn(this) + '</a>';
};
var data = { "people": [
{ "name": "Alan", "id": 1 },
@@ -315,33 +315,47 @@ test("nested block helpers", function() {
});
test("block inverted sections", function() {
shouldCompileTo("{{#people}}{{name}}{{^}}{{../none}}{{/people}}", {none: "No people"},
shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people"},
"No people");
});
test("block inverted sections with empty arrays", function() {
shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people", 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) {
var list = function(context, fn, inverse) {
if (context.length > 0) {
var out = "<ul>";
for(var i = 0,j=context.length; i < j; i++) {
out += "<li>";
out += "<li>";
out += fn(context[i]);
out += "</li>";
}
out += "</ul>";
return out;
} else {
return "<p>" + inverse(this) + "</p>";
}
};
list.not = function(context, fn) {
return "<p>" + fn(context, this) + "</p>";
};
var hash = {list: list, people: [{name: "Alan"}, {name: "Yehuda"}]};
var empty = {list: list, people: []};
var rootMessage = {
list: function(context, fn, inverse) { if(context.length === 0) { return "<p>" + inverse(this) + "</p>"; } },
people: [],
message: "Nobody's here"
}
var messageString = "{{#list people}}Hello{{^}}{{message}}{{/list}}";
// the meaning here may be kind of hard to catch, but list.not is always called,
// so we should see the output of both
shouldCompileTo(string, hash, "<ul><li>Alan</li><li>Yehuda</li></ul><p><em>Nobody's here</em></p>", "Not is called when block inverted section is encountered.");
shouldCompileTo(string, hash, "<ul><li>Alan</li><li>Yehuda</li></ul>", "an inverse wrapper is passed in as a new context");
shouldCompileTo(string, empty, "<p><em>Nobody's here</em></p>", "an inverse wrapper can be optionally called");
shouldCompileTo(messageString, rootMessage, "<p>Nobody's here</p>", "the context of an inverse is the parent of the block");
});
module("fallback hash");
@@ -390,7 +404,7 @@ test("partial in a partial", function() {
test("rendering undefined partial throws an exception", function() {
shouldThrow(function() {
var template = Handlebars.compile("{{> whatever}}");
var template = Handlebars.compile("{{> whatever}}");
template();
}, Handlebars.Exception, "Should throw exception");
});