This commit is contained in:
Jason Davies
2010-11-29 15:05:11 +00:00
2 changed files with 189 additions and 81 deletions
+139 -80
View File
@@ -5,7 +5,7 @@ var Handlebars = {
if (Handlebars.compilerCache[string] == null) {
var fnBody = Handlebars.compileFunctionBody(string);
var fn = new Function("context", "fallback", "stack", "Handlebars", fnBody);
Handlebars.compilerCache[string] =
Handlebars.compilerCache[string] =
function(context, fallback, stack) { return fn(context, fallback, stack, Handlebars); };
}
@@ -16,12 +16,12 @@ var Handlebars = {
var fnBody = Handlebars.compileFunctionBody(string);
return "function(context, fallback, stack) { " + fnBody + "}";
},
compileFunctionBody: function(string) {
var compiler = new Handlebars.Compiler(string);
compiler.compile();
return "fallback = fallback || {}; var stack = stack || [];" + compiler.fn;
return "var stack = stack || [];" + compiler.fn;
},
isFunction: function(fn) {
@@ -29,7 +29,7 @@ var Handlebars = {
},
trim: function(str) {
return str.replace(/^\s+|\s+$/g, '');
return str.replace(/^\s+|\s+$/g, '');
},
escapeText: function(string) {
@@ -69,42 +69,20 @@ var Handlebars = {
} else {
compiled = Handlebars.compile(partial);
}
return compiled;
},
evalExpression: function(path, context, stack, fallback) {
fallback = fallback || {};
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 && typeof context !== "undefined" && context != null ; i++) {
context = context[parts[i]];
}
if (parts.length == 1 && typeof context === "undefined") {
return fallback[parts[0]];
}
return context;
},
buildContext: function(context, stack) {
var ContextWrapper = function(stack) {
var ContextWrapper = function(stack) {
this.__context__ = context;
this.__stack__ = stack.slice(0);
this.__get__ = function(path) {
return Handlebars.evalExpression(path, this, this.__stack__);
return this.__context__.evalExpression(path, this.__stack__).data;
};
};
ContextWrapper.prototype = context;
ContextWrapper.prototype = context.data;
return new ContextWrapper(stack);
},
@@ -130,7 +108,7 @@ var Handlebars = {
switch(parts[i]) {
case "..":
if (readDepth) {
throw new Handlebars.Exception("Cannot jump out of context after moving into a context.");
throw new Handlebars.Exception("Cannot jump out of context after moving into a context.");
} else {
depth += 1;
}
@@ -145,7 +123,7 @@ var Handlebars = {
dig.push(parts[i]);
}
}
var ret = [depth, dig];
Handlebars.pathPatterns["hbs" + path] = ret;
return ret;
@@ -167,66 +145,117 @@ var Handlebars = {
// Escapes output and converts empty values to empty strings
filterOutput: function(value, escape) {
if (Handlebars.isEmpty(value)) {
return "";
} else if (escape) {
return Handlebars.escapeExpression(value);
return Handlebars.escapeExpression(value);
} else {
return value;
}
},
handleBlock: function(lookup, context, arg, fn, notFn) {
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);
}
var out = "", args;
originalArgs = arg.length ? arg : [null]
if (Handlebars.isFunction(lookup.data)) {
args = originalArgs.concat(fn);
out = out + lookup.data.apply(context, args);
if (notFn != null && Handlebars.isFunction(lookup.data.not)) {
args = originalArgs.concat(notFn);
out = out + lookup.data.not.apply(context, args);
}
}
else {
if (!Handlebars.isEmpty(lookup)) {
out = out + Handlebars.helperMissing.call(arg, lookup, fn);
if (!Handlebars.isEmpty(lookup.data)) {
// TODO: which case is this, and what does it mean for multiple args
out = out + Handlebars.helperMissing.call(arg[0], lookup, fn);
}
if (notFn != null) {
out = out + Handlebars.helperMissing.not.call(arg, lookup, notFn);
out = out + Handlebars.helperMissing.not.call(arg[0], lookup, notFn);
}
}
return out;
},
handleExpression: function(lookup, context, arg, isEscaped) {
// lookup: The evaluated mustache, either function or value. A Handlebars.Context
// context: The wrapped context object
// args: The evaluated arguments to the mustache
// isEscaped: Is it escaped?
handleExpression: function(lookup, context, args, isEscaped) {
var out = "";
if (Handlebars.isFunction(lookup)) {
out = out + Handlebars.filterOutput(lookup.call(context, arg), isEscaped);
} else if(!Handlebars.isEmpty(lookup)) {
out = out + Handlebars.filterOutput(lookup, isEscaped);
if (Handlebars.isFunction(lookup.data)) {
out = out + Handlebars.filterOutput(lookup.data.apply(context, args), isEscaped);
} else if(!Handlebars.isEmpty(lookup.data)) {
out = out + Handlebars.filterOutput(lookup.data, isEscaped);
}
return out;
},
// lookup: The evaluated mustache, either function or value. A Handlebars.Context
// context: The context where this item occurred
// fn: The compiled body of this inverted section
handleInvertedSection: function(lookup, context, fn) {
var out = "";
if(Handlebars.isFunction(lookup) && Handlebars.isEmpty(lookup())) {
if(Handlebars.isFunction(lookup.data) && Handlebars.isEmpty(lookup.data())) {
out = out + fn(context);
} else if (Handlebars.isEmpty(lookup)) {
} else if (Handlebars.isEmpty(lookup.data)) {
out = out + fn(context);
}
return out;
}
}
Handlebars.Context = function(context, fallback, path) {
if (context instanceof Handlebars.Context) {
this.data = context.data;
this.fallback = context.fallback;
this.path = context.path;
} else {
this.data = context;
this.fallback = fallback || {};
this.path = path || "";
}
};
Handlebars.Context.prototype = {
// path: The path to evaluate
// stack: The stack to work with
evalExpression: function(path, stack) {
var newContext = new Handlebars.Context(this);
var parsedPath = Handlebars.parsePath(path);
var depth = parsedPath[0];
var parts = parsedPath[1];
if (depth > stack.length) {
newContext.data = null;
} else if (depth > 0) {
newContext = new Handlebars.Context(stack[stack.length - depth]);
}
for (var i = 0, j = parts.length; i < j && typeof newContext.data !== "undefined" && newContext.data !== null ; i++) {
newContext.data = newContext.data[parts[i]];
}
if (parts.length == 1 && typeof newContext.data === "undefined") {
newContext.data = newContext.fallback[parts[0]];
}
return newContext;
}
};
Handlebars.Compiler = function(string) {
this.string = string;
this.pointer = -1;
this.mustache = false;
this.text = "";
this.fn = "var out = ''; var lookup; ";
this.fn = "context = new Handlebars.Context(context, fallback); var out = ''; var lookup; ";
this.newlines = "";
this.comment = false;
this.escaped = true;
@@ -251,17 +280,17 @@ Handlebars.SafeString.prototype.toString = function() {
Handlebars.helperMissing = function(object, fn) {
var ret = "";
if(object === true) {
if(object.data === true) {
return fn(this);
} else if(object === false) {
} else if(object.data === false) {
return "";
} else if(Object.prototype.toString.call(object) === "[object Array]") {
for(var i=0, j=object.length; i<j; i++) {
ret = ret + fn(object[i]);
} else if(Object.prototype.toString.call(object.data) === "[object Array]") {
for(var i=0, j=object.data.length; i<j; i++) {
ret = ret + fn(object.data[i]);
}
return ret;
} else {
return fn(object);
return fn(object.data);
}
};
Handlebars.helperMissing.not = function(context, fn) {
@@ -327,17 +356,19 @@ Handlebars.Compiler.prototype = {
}
},
addExpression: function(mustache, param) {
param = param || null;
addExpression: function(mustache, params) {
if(!params[0]) params = ["null"]
params = params.join(", ")
var expr = this.lookupFor(mustache);
this.fn += "var proxy = Handlebars.buildContext(context, stack);"
this.fn += "out = out + Handlebars.handleExpression(" + expr + ", proxy, " + param + ", " + this.escaped + ");";
this.fn += "out = out + Handlebars.handleExpression(" + expr + ", proxy, [" + params + "], " + this.escaped + ");";
},
addInvertedSection: function(mustache) {
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();
@@ -354,7 +385,7 @@ Handlebars.Compiler.prototype = {
return "context";
}
else {
return "(Handlebars.evalExpression('" + param + "', context, stack, fallback))";
return "(context.evalExpression('" + param + "', stack))";
}
},
@@ -379,7 +410,7 @@ Handlebars.Compiler.prototype = {
return compiler;
},
addBlock: function(mustache, param, parts) {
addBlock: function(mustache, params) {
var compiler = this.compileToEndOfBlock(mustache);
var result = compiler.fn;
@@ -389,7 +420,7 @@ Handlebars.Compiler.prototype = {
this.fn += "var wrappedContext = Handlebars.buildContext(context, stack);";
this.fn += "var " + fnId + " = function(context) {" + result + "}; ";
this.fn += "lookup = " + this.lookupFor(mustache) + "; ";
this.fn += "arg = " + param + ";";
this.fn += "arg = [" + params.join(", ") + "] ;";
this.fn += "stack.push(context);";
if (compiler.continueInverted) {
@@ -407,8 +438,8 @@ Handlebars.Compiler.prototype = {
addPartial: function(mustache, param) {
// either used a cached copy of the partial or compile a new one
this.fn += "if (typeof fallback['partials'] === 'undefined' || typeof fallback['partials']['" + mustache + "'] === 'undefined') throw new Handlebars.Exception('Attempted to render undefined partial: " + mustache + "');";
this.fn += "out = out + Handlebars.compilePartial(fallback['partials']['" + mustache + "'])(" + param + ", fallback, stack);";
this.fn += "if (typeof context.fallback['partials'] === 'undefined' || typeof context.fallback['partials']['" + mustache + "'] === 'undefined') throw new Handlebars.Exception('Attempted to render undefined partial: " + mustache + "');";
this.fn += "out = out + Handlebars.compilePartial(context.fallback['partials']['" + mustache + "'])(" + param + ", null, stack);";
},
parseMustache: function() {
@@ -435,19 +466,47 @@ Handlebars.Compiler.prototype = {
}
this.addText();
this.mustache = " ";
var params = [""], currentParam = 0, literals = [];
while(chr = this.getChar()) {
if(this.mustache && chr === "}" && this.peek() === "}") {
var parts = Handlebars.trim(this.mustache).split(/\s+/);
mustache = parts[0];
param = this.lookupFor(parts[1]);
if(this.stringLiteral) {
params[currentParam] += chr;
if(chr === "\\" && this.peek() === '"') {
params[currentParam] += '"';
this.getChar();
} else if(chr === '"') {
params[++currentParam] = ""
this.stringLiteral = false;
}
} else if(chr === '"') {
if(params[currentParam] !== "") {
throw new Handlebars.Exception("You are already in the middle of" +
"the " + params[currentParam] + " param. " +
"You cannot start a String param")
}
this.stringLiteral = true;
params[currentParam] = chr;
literals[currentParam] = true;
} else if(chr === " ") {
if(params[currentParam] !== "") params[++currentParam] = ""
} else if(chr === "}" && this.peek() === "}") {
mustache = params[0];
arguments = [];
if(!params[1]) params[1] = undefined;
for(var i=1,l=params.length; i<l; i++) {
var argument = params[i];
arguments.push( literals[i] ? argument : "(" + this.lookupFor(argument) + ".data)");
}
this.mustache = false;
// finish reading off the close of the handlebars
this.getChar();
// {{{expression}} is techically valid, but if we started with {{{ we'll try to read
// {{{expression}} is techically valid, but if we started with {{{ we'll try to read
// }}} off of the close of the handlebars
if (!this.escaped && this.peek() === "}") {
this.getChar();
@@ -457,7 +516,7 @@ Handlebars.Compiler.prototype = {
this.comment = false;
return;
} else if (this.partial) {
this.addPartial(mustache, param)
this.addPartial(mustache, "(" + this.lookupFor(argument) + ")");
this.partial = false;
return;
} else if (this.inverted) {
@@ -465,17 +524,17 @@ Handlebars.Compiler.prototype = {
this.inverted = false;
return;
} else if(this.openBlock) {
this.addBlock(mustache, param, parts)
this.addBlock(mustache, arguments)
return;
} else {
return this.addExpression(mustache, param);
return this.addExpression(mustache, arguments);
}
this.escaped = true;
} else if(this.comment) {
;
} else {
this.mustache = this.mustache + chr;
params[currentParam] += chr;
}
}
}
+50 -1
View File
@@ -293,13 +293,14 @@ 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 helper inverted sections", function() {
var string = "{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}"
var list = function(context, fn) {
console.log(context);
if (context.length > 0) {
var out = "<ul>";
for(var i = 0,j=context.length; i < j; i++) {
@@ -387,6 +388,54 @@ test("Partial containing complex expression", function() {
shouldCompileTo(template, [hash, {partials: {dude: dude}}], "Dudes: Mr. Yehuda Mr. Alan ");
});
module("String literal parameters");
test("simple literals work", function() {
var string = 'Message: {{hello "world"}}';
var hash = {}
var fallback = {hello: function(param) { return "Hello " + param; }}
shouldCompileTo(string, [hash, fallback], "Message: Hello world", "template with a simple String literal");
});
test("using a quote in the middle of a parameter raises an error", function() {
shouldThrow(function() {
var string = 'Message: {{hello wo"rld"}}';
Handlebars.compile(string);
}, Handlebars.Exception, "should throw exception");
});
test("escaping a String is possible", function(){
var string = 'Message: {{hello "\\"world\\""}}';
var hash = {}
var fallback = {hello: function(param) { return "Hello " + param; }}
shouldCompileTo(string, [hash, fallback], "Message: Hello \"world\"", "template with an escaped String literal");
});
test("it works with ' marks", function() {
var string = 'Message: {{hello "Alan\'s world"}}';
var hash = {}
var fallback = {hello: function(param) { return "Hello " + param; }}
shouldCompileTo(string, [hash, fallback], "Message: Hello Alan's world", "template with a ' mark");
});
module("multiple parameters");
test("simple multi-params work", function() {
var string = 'Message: {{goodbye cruel world}}';
var hash = {cruel: "cruel", world: "world"}
var fallback = {goodbye: function(cruel, world) { return "Goodbye " + cruel + " " + world; }}
shouldCompileTo(string, [hash, fallback], "Message: Goodbye cruel world", "regular helpers with multiple params");
});
test("block multi-params work", function() {
var string = 'Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}';
var hash = {cruel: "cruel", world: "world"}
var fallback = {goodbye: function(cruel, world, fn) {
return fn({greeting: "Goodbye", adj: "cruel", noun: "world"});
}}
shouldCompileTo(string, [hash, fallback], "Message: Goodbye cruel world", "block helpers with multiple params");
})
module("safestring");
test("constructing a safestring from a string and checking its type", function() {