summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.markdown23
-rw-r--r--Rakefile24
-rw-r--r--lib/handlebars.js3
-rw-r--r--lib/handlebars/ast.js13
-rw-r--r--lib/handlebars/base.js26
-rw-r--r--lib/handlebars/compiler.js (renamed from lib/handlebars/vm.js)127
-rw-r--r--lib/handlebars/printer.js8
-rw-r--r--lib/handlebars/runtime.js267
-rw-r--r--lib/handlebars/utils.js11
-rw-r--r--spec/parser_spec.rb57
-rw-r--r--spec/qunit_spec.js262
-rw-r--r--spec/spec_helper.rb3
-rw-r--r--spec/tokenizer_spec.rb36
-rw-r--r--src/handlebars.l7
-rw-r--r--src/handlebars.yy4
15 files changed, 459 insertions, 412 deletions
diff --git a/README.markdown b/README.markdown
index 9501b24..3cee789 100644
--- a/README.markdown
+++ b/README.markdown
@@ -3,6 +3,9 @@ Handlebars.js
Handlebars.js is an extension to the [Mustache templating language](http://mustache.github.com/) created by Chris Wanstrath. Handlebars.js and Mustache are both logicless templating languages that keep the view and the code separated like we all know they should be.
+Checkout the official Handlebars docs site at [http://www.handlebarsjs.com](http://www.handlebarsjs.com).
+
+
Installing
----------
Installing Handlebars is easy. Simply [download the package from GitHub](https://github.com/wycats/handlebars.js/archives/master) and add it to your web pages (you should usually use the most recent version).
@@ -42,7 +45,7 @@ embedded in them, as well as the text for a link:
});
var context = { posts: [{url: "/hello-world", body: "Hello World!"}] };
- var source = "<ul>{{#posts}}<li>{{{link_to this}}}</li></ul>"
+ var source = "<ul>{{#posts}}<li>{{{link_to this}}}</li>{{/posts}}</ul>"
var template = Handlebars.compile(source);
template(context);
@@ -99,7 +102,7 @@ instance:
});
var context = { posts: [{url: "/hello-world", body: "Hello World!"}] };
- var source = '<ul>{{#posts}}<li>{{{link_to "Post" this}}}</li></ul>'
+ var source = '<ul>{{#posts}}<li>{{{link_to "Post" this}}}</li>{{/posts}}</ul>'
var template = Handlebars.compile(source);
template(context);
@@ -183,6 +186,21 @@ annotations for webkit browsers, but will slightly increase startup
time.
+Upgrading
+---------
+
+When upgrading from the Handlebars 0.9 series, be aware that the
+signature for passing custom helpers or partials to templates has
+changed.
+
+Instead of:
+
+ template(context, helpers, partials, [data])
+
+Use:
+
+ template(context, {helpers: helpers, partials: partials, data: data})
+
Known Issues
------------
* Handlebars.js can be cryptic when there's an error while rendering.
@@ -195,6 +213,7 @@ Handlebars in the Wild
-----------------
* Don Park wrote an Express.js view engine adapter for Handlebars.js called [hbs](http://github.com/donpark/hbs)
* [sammy.js](http://github.com/quirkey/sammy) by Aaron Quint, a.k.a. quirkey, supports Handlebars.js as one of its template plugins.
+* [SproutCore](http://www.sproutcore.com) uses Handlebars.js as its main templating engine, extending it with automatic data binding support.
Helping Out
-----------
diff --git a/Rakefile b/Rakefile
index ebb03bd..6b9ac11 100644
--- a/Rakefile
+++ b/Rakefile
@@ -4,7 +4,11 @@ require "bundler/setup"
file "lib/handlebars/parser.js" => ["src/handlebars.yy","src/handlebars.l"] do
if ENV['PATH'].split(':').any? {|folder| File.exists?(folder+'/jison')}
system "jison src/handlebars.yy src/handlebars.l"
- sh "mv handlebars.js lib/handlebars/parser.js"
+ File.open("lib/handlebars/parser.js", "w") do |file|
+ file.puts File.read("handlebars.js") + ";"
+ end
+
+ sh "rm handlebars.js"
else
puts "Jison is not installed. Try running `npm install jison`."
end
@@ -24,22 +28,17 @@ def remove_exports(string)
match ? match[1] : string
end
-minimal_deps = %w(parser base ast visitor utils vm).map do |file|
- "lib/handlebars/#{file}.js"
-end
-
-base_deps = %w(parser base ast visitor runtime utils vm).map do |file|
+minimal_deps = %w(parser base ast visitor utils compiler).map do |file|
"lib/handlebars/#{file}.js"
end
-debug_deps = %w(parser base ast visitor printer runtime utils vm debug).map do |file|
+debug_deps = %w(parser base ast visitor printer utils compiler debug).map do |file|
"lib/handlebars/#{file}.js"
end
directory "dist"
minimal_deps.unshift "dist"
-base_deps.unshift "dist"
debug_deps.unshift "dist"
def build_for_task(task)
@@ -62,20 +61,15 @@ file "dist/handlebars.js" => minimal_deps do |task|
build_for_task(task)
end
-file "dist/handlebars.base.js" => base_deps do |task|
- build_for_task(task)
-end
-
file "dist/handlebars.debug.js" => debug_deps do |task|
build_for_task(task)
end
task :build => [:compile, "dist/handlebars.js"]
-task :base => [:compile, "dist/handlebars.base.js"]
task :debug => [:compile, "dist/handlebars.debug.js"]
-desc "build the build, debug and base versions of handlebars"
-task :release => [:build, :debug, :base]
+desc "build the build and debug versions of handlebars"
+task :release => [:build, :debug]
directory "vendor"
diff --git a/lib/handlebars.js b/lib/handlebars.js
index 1f85892..b578775 100644
--- a/lib/handlebars.js
+++ b/lib/handlebars.js
@@ -7,8 +7,7 @@ require("handlebars/ast");
require("handlebars/printer");
require("handlebars/visitor");
-require("handlebars/runtime");
-require("handlebars/vm");
+require("handlebars/compiler");
// BEGIN(BROWSER)
diff --git a/lib/handlebars/ast.js b/lib/handlebars/ast.js
index a266fe7..d7cc150 100644
--- a/lib/handlebars/ast.js
+++ b/lib/handlebars/ast.js
@@ -60,7 +60,7 @@ var Handlebars = require("handlebars");
Handlebars.AST.IdNode = function(parts) {
this.type = "ID";
- this.original = parts.join("/");
+ this.original = parts.join(".");
var dig = [], depth = 0;
@@ -73,6 +73,7 @@ var Handlebars = require("handlebars");
}
this.parts = dig;
+ this.string = dig.join('.');
this.depth = depth;
this.isSimple = (dig.length === 1) && (depth === 0);
};
@@ -82,6 +83,16 @@ var Handlebars = require("handlebars");
this.string = string;
};
+ Handlebars.AST.IntegerNode = function(integer) {
+ this.type = "INTEGER";
+ this.integer = integer;
+ };
+
+ Handlebars.AST.BooleanNode = function(bool) {
+ this.type = "BOOLEAN";
+ this.bool = bool;
+ };
+
Handlebars.AST.CommentNode = function(comment) {
this.type = "comment";
this.comment = comment;
diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js
index 387ff76..b1cc4ba 100644
--- a/lib/handlebars/base.js
+++ b/lib/handlebars/base.js
@@ -3,6 +3,8 @@ var handlebars = require("handlebars/parser").parser;
// BEGIN(BROWSER)
var Handlebars = {};
+Handlebars.VERSION = "1.0.beta.2";
+
Handlebars.Parser = handlebars;
Handlebars.parse = function(string) {
@@ -14,22 +16,6 @@ Handlebars.print = function(ast) {
return new Handlebars.PrintVisitor().accept(ast);
};
-Handlebars.Runtime = {};
-
-Handlebars.Runtime.compile = function(string) {
- var ast = Handlebars.parse(string);
-
- return function(context, helpers, partials) {
- helpers = helpers || Handlebars.helpers;
- partials = partials || Handlebars.partials;
-
- var internalContext = new Handlebars.Context(context, helpers, partials);
- var runtime = new Handlebars.Runtime(internalContext);
- runtime.accept(ast);
- return runtime.buffer;
- };
-};
-
Handlebars.helpers = {};
Handlebars.partials = {};
@@ -42,6 +28,14 @@ Handlebars.registerPartial = function(name, str) {
this.partials[name] = str;
};
+Handlebars.registerHelper('helperMissing', function(arg) {
+ if(arguments.length === 2) {
+ return undefined;
+ } else {
+ throw new Error("Could not find property '" + arg + "'");
+ }
+});
+
Handlebars.registerHelper('blockHelperMissing', function(context, fn, inverse) {
inverse = inverse || function() {};
diff --git a/lib/handlebars/vm.js b/lib/handlebars/compiler.js
index 3dbd8f4..6f52482 100644
--- a/lib/handlebars/vm.js
+++ b/lib/handlebars/compiler.js
@@ -20,7 +20,8 @@ Handlebars.JavaScriptCompiler = function() {};
invokePartial: 12,
push: 13,
invokeInverse: 14,
- assignToHash: 15
+ assignToHash: 15,
+ pushStringParam: 16
};
Compiler.MULTI_PARAM_OPCODES = {
@@ -36,7 +37,8 @@ Handlebars.JavaScriptCompiler = function() {};
invokePartial: 1,
push: 1,
invokeInverse: 1,
- assignToHash: 1
+ assignToHash: 1,
+ pushStringParam: 1
};
Compiler.DISASSEMBLE_MAP = {};
@@ -51,6 +53,8 @@ Handlebars.JavaScriptCompiler = function() {};
};
Compiler.prototype = {
+ compiler: Compiler,
+
disassemble: function() {
var opcodes = this.opcodes, opcode, nextCode;
var out = [], str, name, value;
@@ -89,9 +93,10 @@ Handlebars.JavaScriptCompiler = function() {};
guid: 0,
- compile: function(program) {
+ compile: function(program, options) {
this.children = [];
this.depths = {list: []};
+ this.options = options || {};
return this.program(program);
},
@@ -116,7 +121,7 @@ Handlebars.JavaScriptCompiler = function() {};
},
compileProgram: function(program) {
- var result = new Compiler().compile(program);
+ var result = new this.compiler().compile(program, this.options);
var guid = this.guid++;
this.usePartial = this.usePartial || result.usePartial;
@@ -219,6 +224,14 @@ Handlebars.JavaScriptCompiler = function() {};
this.opcode('pushString', string.string);
},
+ INTEGER: function(integer) {
+ this.opcode('push', integer.integer);
+ },
+
+ BOOLEAN: function(bool) {
+ this.opcode('push', bool.bool);
+ },
+
comment: function() {},
// HELPERS
@@ -227,7 +240,17 @@ Handlebars.JavaScriptCompiler = function() {};
while(i--) {
param = params[i];
- this[param.type](param);
+
+ if(this.options.stringParams) {
+ if(param.depth) {
+ this.addDepth(param.depth);
+ }
+
+ this.opcode('getContext', param.depth || 0);
+ this.opcode('pushStringParam', param.string);
+ } else {
+ this[param.type](param);
+ }
}
},
@@ -273,8 +296,10 @@ Handlebars.JavaScriptCompiler = function() {};
// PUBLIC API: You can override these methods in a subclass to provide
// alternative compiled forms for name lookup and buffering semantics
nameLookup: function(parent, name, type) {
- if(JavaScriptCompiler.RESERVED_WORDS[name]) {
+ if(JavaScriptCompiler.RESERVED_WORDS[name] || name.indexOf('-') !== -1 || !isNaN(name)) {
return parent + "['" + name + "']";
+ } else if (/^[0-9]+$/.test(name)) {
+ return parent + "[" + name + "]";
} else {
return parent + "." + name;
}
@@ -289,9 +314,9 @@ Handlebars.JavaScriptCompiler = function() {};
},
// END PUBLIC API
- compile: function(environment, data) {
+ compile: function(environment, options) {
this.environment = environment;
- this.data = data;
+ this.options = options || {};
this.preamble();
@@ -299,7 +324,7 @@ Handlebars.JavaScriptCompiler = function() {};
this.stackVars = [];
this.registers = {list: []};
- this.compileChildren(environment, data);
+ this.compileChildren(environment, options);
Handlebars.log(Handlebars.logger.DEBUG, environment.disassemble() + "\n\n");
@@ -393,13 +418,12 @@ Handlebars.JavaScriptCompiler = function() {};
var params = ["Handlebars", "context", "helpers", "partials"];
- if(this.data) { params.push("data"); }
+ if(this.options.data) { params.push("data"); }
for(var i=0, l=this.environment.depths.list.length; i<l; i++) {
params.push("depth" + this.environment.depths.list[i]);
}
-
if(params.length === 4 && !this.environment.usePartial) { params.pop(); }
params.push(this.source.join("\n"));
@@ -413,10 +437,12 @@ Handlebars.JavaScriptCompiler = function() {};
container.children = this.environment.children;
- return function(context, helpers, partials, data, $depth) {
+ return function(context, options, $depth) {
try {
- var args = Array.prototype.slice.call(arguments);
- args.unshift(Handlebars);
+ options = options || {};
+ var args = [Handlebars, context, options.helpers, options.partials, options.data];
+ var depth = Array.prototype.slice.call(arguments, 2);
+ args = args.concat(depth);
return container.render.apply(container, args);
} catch(e) {
throw e;
@@ -477,6 +503,11 @@ Handlebars.JavaScriptCompiler = function() {};
this.source.push(topStack + " = " + this.nameLookup(topStack, name, 'context') + ";");
},
+ pushStringParam: function(string) {
+ this.pushStack("currentContext");
+ this.pushString(string);
+ },
+
pushString: function(string) {
this.pushStack(this.quotedString(string));
},
@@ -503,24 +534,32 @@ Handlebars.JavaScriptCompiler = function() {};
populateParams: function(paramSize, helperId, program, inverse, fn) {
var id = this.popStack(), nextStack;
- var params = [];
+ var params = [], param, stringParam;
var hash = this.popStack();
+ this.register('tmp1', program);
+ this.source.push('tmp1.hash = ' + hash + ';');
+
+ if(this.options.stringParams) {
+ this.source.push('tmp1.contexts = [];');
+ }
+
for(var i=0; i<paramSize; i++) {
- var param = this.popStack();
+ param = this.popStack();
params.push(param);
- }
- this.register('tmp1', program);
- this.source.push('tmp1.hash = ' + hash + ';');
+ if(this.options.stringParams) {
+ this.source.push('tmp1.contexts.push(' + this.popStack() + ');');
+ }
+ }
if(inverse) {
this.source.push('tmp1.fn = tmp1;');
this.source.push('tmp1.inverse = ' + inverse + ';');
}
- if(this.data) {
+ if(this.options.data) {
this.source.push('tmp1.data = data;');
}
@@ -566,7 +605,7 @@ Handlebars.JavaScriptCompiler = function() {};
compiler: JavaScriptCompiler,
- compileChildren: function(environment, data) {
+ compileChildren: function(environment, options) {
var children = environment.children, child, compiler;
var compiled = [];
@@ -574,7 +613,7 @@ Handlebars.JavaScriptCompiler = function() {};
child = children[i];
compiler = new this.compiler();
- compiled[i] = compiler.compile(child, data);
+ compiled[i] = compiler.compile(child, options);
}
environment.rawChildren = children;
@@ -588,7 +627,7 @@ Handlebars.JavaScriptCompiler = function() {};
var depths = this.environment.rawChildren[guid].depths.list;
- if(this.data) { programParams.push("data"); }
+ if(this.options.data) { programParams.push("data"); }
for(var i=0, l = depths.length; i<l; i++) {
depth = depths[i];
@@ -646,7 +685,7 @@ Handlebars.JavaScriptCompiler = function() {};
quotedString: function(str) {
return '"' + str
- .replace(/\\/, '\\\\')
+ .replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r') + '"';
@@ -666,34 +705,46 @@ Handlebars.JavaScriptCompiler = function() {};
})(Handlebars.Compiler, Handlebars.JavaScriptCompiler);
Handlebars.VM = {
- programWithDepth: function(fn) {
- var args = Array.prototype.slice.call(arguments, 1);
- return function(context, helpers, partials, data) {
- args[0] = helpers || args[0];
- args[1] = partials || args[1];
- args[2] = data || args[2];
- return fn.apply(this, [context].concat(args));
+ programWithDepth: function(fn, helpers, partials, data, $depth) {
+ var args = Array.prototype.slice.call(arguments, 4);
+
+ return function(context, options) {
+ options = options || {};
+
+ options = {
+ helpers: options.helpers || helpers,
+ partials: options.partials || partials,
+ data: options.data || data
+ };
+
+ return fn.apply(this, [context, options].concat(args));
};
},
program: function(fn, helpers, partials, data) {
- return function(context, h2, p2, d2) {
- return fn(context, h2 || helpers, p2 || partials, d2 || data);
+ return function(context, options) {
+ options = options || {};
+
+ return fn(context, {
+ helpers: options.helpers || helpers,
+ partials: options.partials || partials,
+ data: options.data || data
+ });
};
},
noop: function() { return ""; },
- compile: function(string, data) {
+ compile: function(string, options) {
var ast = Handlebars.parse(string);
- var environment = new Handlebars.Compiler().compile(ast);
- return new Handlebars.JavaScriptCompiler().compile(environment, data);
+ var environment = new Handlebars.Compiler().compile(ast, options);
+ return new Handlebars.JavaScriptCompiler().compile(environment, options);
},
invokePartial: function(partial, name, context, helpers, partials) {
if(partial === undefined) {
throw new Handlebars.Exception("The partial " + name + " could not be found");
} else if(partial instanceof Function) {
- return partial(context, helpers, partials);
+ return partial(context, {helpers: helpers, partials: partials});
} else {
partials[name] = Handlebars.VM.compile(partial);
- return partials[name](context, helpers, partials);
+ return partials[name](context, {helpers: helpers, partials: partials});
}
}
};
diff --git a/lib/handlebars/printer.js b/lib/handlebars/printer.js
index 2da7bcc..7be3f98 100644
--- a/lib/handlebars/printer.js
+++ b/lib/handlebars/printer.js
@@ -109,6 +109,14 @@ Handlebars.PrintVisitor.prototype.STRING = function(string) {
return '"' + string.string + '"';
};
+Handlebars.PrintVisitor.prototype.INTEGER = function(integer) {
+ return "INTEGER{" + integer.integer + "}";
+};
+
+Handlebars.PrintVisitor.prototype.BOOLEAN = function(bool) {
+ return "BOOLEAN{" + bool.bool + "}";
+};
+
Handlebars.PrintVisitor.prototype.ID = function(id) {
var path = id.parts.join("/");
if(id.parts.length > 1) {
diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js
deleted file mode 100644
index 29ace86..0000000
--- a/lib/handlebars/runtime.js
+++ /dev/null
@@ -1,267 +0,0 @@
-var inspect = function(obj) {
- require("sys").print(require("sys").inspect(obj) + "\n");
-};
-
-var Handlebars = require("handlebars");
-
-// BEGIN(BROWSER)
-// A Context wraps data, and makes it possible to extract a
-// new Context given a path. For instance, if the data
-// is { person: { name: "Alan" } }, a Context wrapping
-// "Alan" can be extracted by searching for "person/name"
-Handlebars.Context = function(data, helpers, partials) {
- this.data = data;
- this.helpers = helpers || {};
- this.partials = partials || {};
-};
-
-Handlebars.Context.prototype = {
- isContext: true,
-
- // Make a shallow copy of the Context
- clone: function() {
- return new Handlebars.Context(this.data, this.helpers, this.partials);
- },
-
- // Search for an object inside the Context's data. The
- // path parameter is an object with parts
- // ("person/name" represented as ["person", "name"]),
- // and depth (the amount of levels to go up the stack,
- // originally represented as ..). The stack parameter
- // is the objects already searched from the root of
- // the original Context in order to get to this point.
- //
- // Return a new Context wrapping the data found in
- // the search.
- evaluate: function(path, stack) {
- var context = this.clone();
- var depth = path.depth, parts = path.parts;
-
- if(depth > stack.length) { context.data = null; }
- else if(depth > 0) { context = stack[stack.length - depth].clone(); }
-
- for(var i=0,l=parts.length; i<l && context.data != null; i++) {
- context.data = context.data[parts[i]];
- }
-
- if(context.data !== undefined) { return context; }
-
- if(parts.length === 1 && context.data === undefined) {
- context.data = context.helpers[parts[0]];
- }
-
- return context;
- }
-};
-
-Handlebars.K = function() { return this; };
-
-Handlebars.proxy = function(obj) {
- var Proxy = this.K;
- Proxy.prototype = obj;
- return new Proxy();
-};
-
-Handlebars.Runtime = function(context, stack) {
- this.stack = stack || [];
- this.buffer = "";
-
- this.context = context;
-};
-
-Handlebars.Runtime.prototype = {
- accept: Handlebars.Visitor.prototype.accept,
-
- ID: function(path) {
- return this.context.evaluate(path, this.stack);
- },
-
- STRING: function(string) {
- return { data: string.string };
- },
-
- program: function(program) {
- var statements = program.statements;
-
- for(var i=0, l=statements.length; i<l; i++) {
- var statement = statements[i];
- this[statement.type](statement);
- }
-
- return this.buffer;
- },
-
- mustache: function(mustache) {
- var idObj = this.ID(mustache.id);
- var params = mustache.params.slice(0);
- var buf;
-
- for(var i=0, l=params.length; i<l; i++) {
- var param = params[i];
- params[i] = this[param.type](param).data;
- }
-
- var data = idObj.data;
-
- var type = Object.prototype.toString.call(data);
- var functionType = (type === "[object Function]");
-
- if(!functionType && params.length) {
- params = params.slice(0);
- params.unshift(data || mustache.id.original);
- data = this.context.helpers.helperMissing;
- functionType = true;
- }
-
- if(functionType) {
- buf = data.apply(this.wrapContext(), params);
- } else {
- buf = data;
- }
-
- if(buf && mustache.escaped) { buf = Handlebars.Utils.escapeExpression(buf); }
-
- this.buffer = this.buffer + ((!buf && buf !== 0) ? '' : buf);
- },
-
- block: function(block) {
- var mustache = block.mustache, data;
-
- var id = mustache.id,
- idObj = this.ID(mustache.id),
- data = idObj.data;
-
- var result;
-
- if(typeof data === "function") {
- params = this.evaluateParams(mustache.params);
- } else {
- params = [data];
- data = this.context.helpers.blockHelperMissing;
- }
-
- params.push(this.wrapProgram(block.program));
- result = data.apply(this.wrapContext(), params);
- this.buffer = this.buffer + ((result === undefined) ? "" : result);
-
- if(block.program.inverse) {
- params.pop();
- params.push(this.wrapProgram(block.program.inverse));
- result = data.not ? data.not.apply(this.wrapContext(), params) : "";
- this.buffer = this.buffer + result;
- }
- },
-
- partial: function(partial) {
- var partials = this.context.partials || {};
- var id = partial.id.original;
-
- var partialBody = partials[partial.id.original];
- var program, context;
-
- if(!partialBody) {
- throw new Handlebars.Exception("The partial " + partial.id.original + " does not exist");
- }
-
- if(typeof partialBody === "string") {
- program = Handlebars.parse(partialBody);
- partials[id] = program;
- } else {
- program = partialBody;
- }
-
- if(partial.context) {
- context = this.ID(partial.context);
- } else {
- context = this.context;
- }
- var runtime = new Handlebars.Runtime(context, this.stack);
- this.buffer = this.buffer + runtime.program(program);
- },
-
- not: function(context, fn) {
- return fn(context);
- },
-
- // TODO: Write down the actual spec for inverse sections...
- inverse: function(block) {
- var mustache = block.mustache,
- id = mustache.id,
- not;
-
- var idObj = this.ID(id),
- data = idObj.data,
- isInverse = Handlebars.Utils.isEmpty(data);
-
-
- var context = this.wrapContext();
-
- if(Object.prototype.toString.call(data) === "[object Function]") {
- params = this.evaluateParams(mustache.params);
- id = id.parts.join("/");
-
- data = data.apply(context, params);
- if(Handlebars.Utils.isEmpty(data)) { isInverse = true; }
- if(data.not) { not = data.not; } else { not = this.not; }
- } else {
- not = this.not;
- }
-
- var result = not(context, this.wrapProgram(block.program));
- if(result != null) { this.buffer = this.buffer + result; }
- return;
- },
-
- content: function(content) {
- this.buffer += content.string;
- },
-
- comment: function() {},
-
- evaluateParams: function(params) {
- var ret = [];
-
- for(var i=0, l=params.length; i<l; i++) {
- var param = params[i];
- ret[i] = this[param.type](param).data;
- }
-
- if(ret.length === 0) { ret = [this.wrapContext()]; }
- return ret;
- },
-
- wrapContext: function() {
- var data = this.context.data;
- var proxy = Handlebars.proxy(data);
- var context = proxy.__context__ = this.context;
- var stack = proxy.__stack__ = this.stack.slice(0);
-
- proxy.__get__ = function(path) {
- path = new Handlebars.AST.IdNode(path.split("/"));
- return context.evaluate(path, stack).data;
- };
-
- proxy.isWrappedContext = true;
- proxy.__data__ = data;
-
- return proxy;
- },
-
- wrapProgram: function(program) {
- var currentContext = this.context;
- var stack = this.stack.slice(0);
-
- return function(context) {
- if(context && context.isWrappedContext) { context = context.__data__; }
-
- stack.push(currentContext);
- var newContext = new Handlebars.Context(context, currentContext.helpers, currentContext.partials);
- var runtime = new Handlebars.Runtime(newContext, stack);
- runtime.program(program);
- return runtime.buffer;
- };
- }
-
-};
-// END(BROWSER)
-
diff --git a/lib/handlebars/utils.js b/lib/handlebars/utils.js
index 981bb1f..4202c77 100644
--- a/lib/handlebars/utils.js
+++ b/lib/handlebars/utils.js
@@ -16,14 +16,17 @@ Handlebars.SafeString.prototype.toString = function() {
(function() {
var escape = {
"<": "&lt;",
- ">": "&gt;"
+ ">": "&gt;",
+ '"': "&quot;",
+ "'": "&#x27;",
+ "`": "&#x60;"
};
- var badChars = /&(?!\w+;)|[<>]/g;
- var possible = /[&<>]/
+ var badChars = /&(?!\w+;)|[<>"'`]/g;
+ var possible = /[&<>"'`]/;
var escapeChar = function(chr) {
- return escape[chr] || "&amp;"
+ return escape[chr] || "&amp;";
};
Handlebars.Utils = {
diff --git a/spec/parser_spec.rb b/spec/parser_spec.rb
index 3c6250a..ead3315 100644
--- a/spec/parser_spec.rb
+++ b/spec/parser_spec.rb
@@ -80,6 +80,10 @@ describe "Parser" do
pad("{{! '#{comment}' }}")
end
+ def multiline_comment(comment)
+ pad("{{! '\n#{comment}\n' }}")
+ end
+
def content(string)
pad("CONTENT[ '#{string}' ]")
end
@@ -88,6 +92,14 @@ describe "Parser" do
string.inspect
end
+ def integer(string)
+ "INTEGER{#{string}}"
+ end
+
+ def boolean(string)
+ "BOOLEAN{#{string}}"
+ end
+
def hash(*pairs)
"HASH{" + pairs.map {|k,v| "#{k}=#{v}" }.join(", ") + "}"
end
@@ -113,13 +125,29 @@ describe "Parser" do
ast_for("{{this/foo}}").should == program { mustache id("foo") }
end
+ it "parses mustaches with - in a path" do
+ ast_for("{{foo-bar}}").should == program { mustache id("foo-bar") }
+ end
+
it "parses mustaches with parameters" do
ast_for("{{foo bar}}").should == program { mustache id("foo"), [id("bar")] }
end
it "parses mustaches with hash arguments" do
ast_for("{{foo bar=baz}}").should == program do
- mustache id("foo"), [], hash(["bar", "ID:baz"])
+ mustache id("foo"), [], hash(["bar", id("baz")])
+ end
+
+ ast_for("{{foo bar=1}}").should == program do
+ mustache id("foo"), [], hash(["bar", integer("1")])
+ end
+
+ ast_for("{{foo bar=true}}").should == program do
+ mustache id("foo"), [], hash(["bar", boolean("true")])
+ end
+
+ ast_for("{{foo bar=false}}").should == program do
+ mustache id("foo"), [], hash(["bar", boolean("false")])
end
ast_for("{{foo bar=baz bat=bam}}").should == program do
@@ -133,12 +161,33 @@ describe "Parser" do
ast_for("{{foo omg bar=baz bat=\"bam\"}}").should == program do
mustache id("foo"), [id("omg")], hash(["bar", id("baz")], ["bat", string("bam")])
end
+
+ ast_for("{{foo omg bar=baz bat=\"bam\" baz=1}}").should == program do
+ mustache id("foo"), [id("omg")], hash(["bar", id("baz")], ["bat", string("bam")], ["baz", integer("1")])
+ end
+
+ ast_for("{{foo omg bar=baz bat=\"bam\" baz=true}}").should == program do
+ mustache id("foo"), [id("omg")], hash(["bar", id("baz")], ["bat", string("bam")], ["baz", boolean("true")])
+ end
+
+ ast_for("{{foo omg bar=baz bat=\"bam\" baz=false}}").should == program do
+ mustache id("foo"), [id("omg")], hash(["bar", id("baz")], ["bat", string("bam")], ["baz", boolean("false")])
+ end
end
it "parses mustaches with string parameters" do
ast_for("{{foo bar \"baz\" }}").should == program { mustache id("foo"), [id("bar"), string("baz")] }
end
+ it "parses mustaches with INTEGER parameters" do
+ ast_for("{{foo 1}}").should == program { mustache id("foo"), [integer("1")] }
+ end
+
+ it "parses mustaches with BOOLEAN parameters" do
+ ast_for("{{foo true}}").should == program { mustache id("foo"), [boolean("true")] }
+ ast_for("{{foo false}}").should == program { mustache id("foo"), [boolean("false")] }
+ end
+
it "parses contents followed by a mustache" do
ast_for("foo bar {{baz}}").should == program do
content "foo bar "
@@ -160,6 +209,12 @@ describe "Parser" do
end
end
+ it "parses a multi-line comment" do
+ ast_for("{{!\nthis is a multi-line comment\n}}").should == program do
+ multiline_comment "this is a multi-line comment"
+ end
+ end
+
it "parses an inverse section" do
ast_for("{{#foo}} bar {{^}} baz {{/foo}}").should == program do
block do
diff --git a/spec/qunit_spec.js b/spec/qunit_spec.js
index e144ba2..56cb687 100644
--- a/spec/qunit_spec.js
+++ b/spec/qunit_spec.js
@@ -6,19 +6,25 @@ Handlebars.registerHelper('helperMissing', function(helper, context) {
}
});
-var shouldCompileTo = function(string, hash, expected, message) {
- var template = Handlebars.compile(string);
- if(Object.prototype.toString.call(hash) === "[object Array]") {
- if(hash[1]) {
+var shouldCompileTo = function(string, hashOrArray, expected, message) {
+ var template = Handlebars.compile(string), ary;
+ if(Object.prototype.toString.call(hashOrArray) === "[object Array]") {
+ helpers = hashOrArray[1];
+
+ if(helpers) {
for(var prop in Handlebars.helpers) {
- hash[1][prop] = Handlebars.helpers[prop];
+ helpers[prop] = Handlebars.helpers[prop];
}
}
+
+ ary = [];
+ ary.push(hashOrArray[0]);
+ ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] });
} else {
- hash = [hash];
+ ary = [hashOrArray];
}
- result = template.apply(this, hash)
+ result = template.apply(this, ary);
equal(result, expected, "'" + expected + "' should === '" + result + "': " + message);
};
@@ -72,7 +78,8 @@ test("newlines", function() {
test("escaping text", function() {
shouldCompileTo("Awesome's", {}, "Awesome's", "text is escaped so that it doesn't get caught on single quotes");
shouldCompileTo("Awesome\\", {}, "Awesome\\", "text is escaped so that the closing quote can't be ignored");
- shouldCompileTo("Awesome\\ foo", {}, "Awesome\\ foo", "text is escaped so that it doesn't mess up backslashes");
+ shouldCompileTo("Awesome\\\\ foo", {}, "Awesome\\\\ foo", "text is escaped so that it doesn't mess up backslashes");
+ shouldCompileTo("Awesome {{foo}}", {foo: '\\'}, "Awesome \\", "text is escaped so that it doesn't mess up backslashes");
shouldCompileTo(' " " ', {}, ' " " ', "double quotes never produce invalid javascript");
});
@@ -80,12 +87,12 @@ test("escaping expressions", function() {
shouldCompileTo("{{{awesome}}}", {awesome: "&\"\\<>"}, '&\"\\<>',
"expressions with 3 handlebars aren't escaped");
- shouldCompileTo("{{awesome}}", {awesome: "&\"\\<>"}, '&amp;\"\\&lt;&gt;',
- "by default expressions should be escaped");
-
shouldCompileTo("{{&awesome}}", {awesome: "&\"\\<>"}, '&\"\\<>',
"expressions with {{& handlebars aren't escaped");
+ shouldCompileTo("{{awesome}}", {awesome: "&\"'`\\<>"}, '&amp;&quot;&#x27;&#x60;\\&lt;&gt;',
+ "by default expressions should be escaped");
+
});
test("functions returning safestrings shouldn't be escaped", function() {
@@ -106,6 +113,10 @@ test("functions with context argument", function() {
"Frank", "functions are called with context arguments");
});
+test("paths with hyphens", function() {
+ shouldCompileTo("{{foo-bar}}", {"foo-bar": "baz"}, "baz", "Paths can contain hyphens (-)");
+});
+
test("nested paths", function() {
shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: "beautiful"}},
"Goodbye beautiful world!", "Nested paths access nested objects");
@@ -128,7 +139,7 @@ test("--- TODO --- bad idea nested paths", function() {
shouldCompileTo(string, hash, "world world world ", "Same context (.) is ignored in paths");
});
-test("that current context path ({{.}}) doesn't hit fallback", function() {
+test("that current context path ({{.}}) doesn't hit helpers", function() {
shouldCompileTo("test: {{.}}", [null, {helper: "awesome"}], "test: ");
});
@@ -220,16 +231,16 @@ test("block with complex lookup", function() {
test("helper with complex lookup", function() {
var string = "{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}"
var hash = {prefix: "/root", goodbyes: [{text: "Goodbye", url: "goodbye"}]};
- var fallback = {link: function(prefix) {
+ var helpers = {link: function(prefix) {
return "<a href='" + prefix + "/" + this.url + "'>" + this.text + "</a>"
}};
- shouldCompileTo(string, [hash, fallback], "<a href='/root/goodbye'>Goodbye</a>")
+ shouldCompileTo(string, [hash, helpers], "<a href='/root/goodbye'>Goodbye</a>")
});
test("helper block with complex lookup expression", function() {
var string = "{{#goodbyes}}{{../name}}{{/goodbyes}}"
var hash = {name: "Alan"};
- var fallback = {goodbyes: function(fn) {
+ var helpers = {goodbyes: function(fn) {
var out = "";
var byes = ["Goodbye", "goodbye", "GOODBYE"];
for (var i = 0,j = byes.length; i < j; i++) {
@@ -237,16 +248,16 @@ test("helper block with complex lookup expression", function() {
}
return out;
}};
- shouldCompileTo(string, [hash, fallback], "Goodbye Alan! goodbye Alan! GOODBYE Alan! ");
+ shouldCompileTo(string, [hash, helpers], "Goodbye Alan! goodbye Alan! GOODBYE Alan! ");
});
test("helper with complex lookup and nested template", function() {
var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}";
var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]};
- var fallback = {link: function (prefix, fn) {
+ var helpers = {link: function (prefix, fn) {
return "<a href='" + prefix + "/" + this.url + "'>" + fn(this) + "</a>";
}};
- shouldCompileTo(string, [hash, fallback], "<a href='/root/goodbye'>Goodbye</a>")
+ shouldCompileTo(string, [hash, helpers], "<a href='/root/goodbye'>Goodbye</a>")
});
test("block with deep nested complex lookup", function() {
@@ -359,24 +370,24 @@ test("block helper inverted sections", function() {
// so we should see the output of both
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");
+ shouldCompileTo(messageString, rootMessage, "<p>Nobody&#x27;s here</p>", "the context of an inverse is the parent of the block");
});
-module("fallback hash");
+module("helpers hash");
-test("providing a fallback hash", function() {
+test("providing a helpers hash", function() {
shouldCompileTo("Goodbye {{cruel}} {{world}}!", [{cruel: "cruel"}, {world: "world"}], "Goodbye cruel world!",
- "Fallback hash is available");
+ "helpers hash is available");
shouldCompileTo("Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!", [{iter: [{cruel: "cruel"}]}, {world: "world"}],
- "Goodbye cruel world!", "Fallback hash is available inside other blocks");
+ "Goodbye cruel world!", "helpers hash is available inside other blocks");
});
test("in cases of conflict, the explicit hash wins", function() {
});
-test("the fallback hash is available is nested contexts", function() {
+test("the helpers hash is available is nested contexts", function() {
});
@@ -423,10 +434,15 @@ test("GH-14: a partial preceding a selector", function() {
module("String literal parameters");
test("simple literals work", function() {
- var string = 'Message: {{hello "world"}}';
+ var string = 'Message: {{hello "world" 12 true false}}';
var hash = {};
- var fallback = {hello: function(param) { return "Hello " + param; }}
- shouldCompileTo(string, [hash, fallback], "Message: Hello world", "template with a simple String literal");
+ var helpers = {hello: function(param, times, bool1, bool2) {
+ if(typeof times !== 'number') { times = "NaN"; }
+ if(typeof bool1 !== 'boolean') { bool1 = "NaB"; }
+ if(typeof bool2 !== 'boolean') { bool2 = "NaB"; }
+ return "Hello " + param + " " + times + " times: " + bool1 + " " + bool2;
+ }}
+ shouldCompileTo(string, [hash, helpers], "Message: Hello world 12 times: true false", "template with a simple String literal");
});
test("using a quote in the middle of a parameter raises an error", function() {
@@ -437,17 +453,17 @@ test("using a quote in the middle of a parameter raises an error", function() {
});
test("escaping a String is possible", function(){
- var string = 'Message: {{hello "\\"world\\""}}';
+ 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");
+ var helpers = {hello: function(param) { return "Hello " + param; }}
+ shouldCompileTo(string, [hash, helpers], "Message: Hello \"world\"", "template with an escaped String literal");
});
test("it works with ' marks", function() {
- var string = 'Message: {{hello "Alan\'s world"}}';
+ 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");
+ var helpers = {hello: function(param) { return "Hello " + param; }}
+ shouldCompileTo(string, [hash, helpers], "Message: Hello Alan's world", "template with a ' mark");
});
module("multiple parameters");
@@ -455,17 +471,17 @@ 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");
+ var helpers = {goodbye: function(cruel, world) { return "Goodbye " + cruel + " " + world; }}
+ shouldCompileTo(string, [hash, helpers], "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) {
+ var helpers = {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");
+ shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "block helpers with multiple params");
})
module("safestring");
@@ -523,7 +539,7 @@ test("overriding property lookup", function() {
test("passing in data to a compiled function that expects data - works with helpers", function() {
- var template = Handlebars.compile("{{hello}}", true);
+ var template = Handlebars.compile("{{hello}}", {data: true});
var helpers = {
hello: function(options) {
@@ -531,12 +547,12 @@ test("passing in data to a compiled function that expects data - works with help
}
};
- var result = template({noun: "cat"}, helpers, null, {adjective: "happy"});
+ var result = template({noun: "cat"}, {helpers: helpers, data: {adjective: "happy"}});
equals("happy cat", result);
});
test("passing in data to a compiled function that expects data - works with helpers and parameters", function() {
- var template = Handlebars.compile("{{hello world}}", true);
+ var template = Handlebars.compile("{{hello world}}", {data: true});
var helpers = {
hello: function(noun, options) {
@@ -544,12 +560,12 @@ test("passing in data to a compiled function that expects data - works with help
}
};
- var result = template({exclaim: true, world: "world"}, helpers, null, {adjective: "happy"});
+ var result = template({exclaim: true, world: "world"}, {helpers: helpers, data: {adjective: "happy"}});
equals("happy world!", result);
});
test("passing in data to a compiled function that expects data - works with block helpers", function() {
- var template = Handlebars.compile("{{#hello}}{{world}}{{/hello}}", true);
+ var template = Handlebars.compile("{{#hello}}{{world}}{{/hello}}", {data: true});
var helpers = {
hello: function(fn) {
@@ -560,12 +576,12 @@ test("passing in data to a compiled function that expects data - works with bloc
}
};
- var result = template({exclaim: true}, helpers, null, {adjective: "happy"});
+ var result = template({exclaim: true}, {helpers: helpers, data: {adjective: "happy"}});
equals("happy world!", result);
});
test("passing in data to a compiled function that expects data - works with block helpers that use ..", function() {
- var template = Handlebars.compile("{{#hello}}{{world ../zomg}}{{/hello}}", true);
+ var template = Handlebars.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true});
var helpers = {
hello: function(fn) {
@@ -576,12 +592,12 @@ test("passing in data to a compiled function that expects data - works with bloc
}
};
- var result = template({exclaim: true, zomg: "world"}, helpers, null, {adjective: "happy"});
+ var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy"}});
equals("happy world?", result);
});
test("passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..", function() {
- var template = Handlebars.compile("{{#hello}}{{world ../zomg}}{{/hello}}", true);
+ var template = Handlebars.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true});
var helpers = {
hello: function(fn, inverse) {
@@ -592,40 +608,40 @@ test("passing in data to a compiled function that expects data - data is passed
}
};
- var result = template({exclaim: true, zomg: "world"}, helpers, null, {adjective: "happy", accessData: "#win"});
+ var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy", accessData: "#win"}});
equals("#win happy world?", result);
});
test("you can override inherited data when invoking a helper", function() {
- var template = Handlebars.compile("{{#hello}}{{world zomg}}{{/hello}}", true);
+ var template = Handlebars.compile("{{#hello}}{{world zomg}}{{/hello}}", {data: true});
var helpers = {
hello: function(fn) {
- return fn({exclaim: "?", zomg: "world"}, null, null, {adjective: "sad"});
+ return fn({exclaim: "?", zomg: "world"}, { data: {adjective: "sad"} });
},
world: function(thing, options) {
return options.data.adjective + " " + thing + (this.exclaim || "");
}
};
- var result = template({exclaim: true, zomg: "planet"}, helpers, null, {adjective: "happy"});
+ var result = template({exclaim: true, zomg: "planet"}, {helpers: helpers, data: {adjective: "happy"}});
equals("sad world?", result);
});
test("you can override inherited data when invoking a helper with depth", function() {
- var template = Handlebars.compile("{{#hello}}{{world ../zomg}}{{/hello}}", true);
+ var template = Handlebars.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true});
var helpers = {
hello: function(fn) {
- return fn({exclaim: "?"}, null, null, {adjective: "sad"});
+ return fn({exclaim: "?"}, { data: {adjective: "sad"} });
},
world: function(thing, options) {
return options.data.adjective + " " + thing + (this.exclaim || "");
}
};
- var result = template({exclaim: true, zomg: "world"}, helpers, null, {adjective: "happy"});
+ var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy"}});
equals("sad world?", result);
});
@@ -647,7 +663,7 @@ test("helpers take precedence over same-named context properties", function() {
world: "world"
};
- var result = template(context, helpers);
+ var result = template(context, {helpers: helpers});
equals(result, "GOODBYE cruel WORLD");
});
@@ -669,34 +685,158 @@ test("helpers take precedence over same-named context properties", function() {
world: "world"
};
- var result = template(context, helpers);
+ var result = template(context, {helpers: helpers});
equals(result, "GOODBYE cruel WORLD");
});
test("helpers can take an optional hash", function() {
- var template = Handlebars.compile('{{goodbye cruel="CRUEL" world="WORLD"}}');
+ var template = Handlebars.compile('{{goodbye cruel="CRUEL" world="WORLD" times=12}}');
var helpers = {
goodbye: function(options) {
- return "GOODBYE " + options.hash.cruel + " " + options.hash.world;
+ return "GOODBYE " + options.hash.cruel + " " + options.hash.world + " " + options.hash.times + " TIMES";
}
};
var context = {};
- var result = template(context, helpers);
+ var result = template(context, {helpers: helpers});
+ equals(result, "GOODBYE CRUEL WORLD 12 TIMES");
+});
+
+test("helpers can take an optional hash with booleans", function() {
+ var helpers = {
+ goodbye: function(options) {
+ if (options.hash.print === true) {
+ return "GOODBYE " + options.hash.cruel + " " + options.hash.world;
+ } else if (options.hash.print === false) {
+ return "NOT PRINTING";
+ } else {
+ return "THIS SHOULD NOT HAPPEN";
+ }
+ }
+ };
+
+ var context = {};
+
+ var template = Handlebars.compile('{{goodbye cruel="CRUEL" world="WORLD" print=true}}');
+ var result = template(context, {helpers: helpers});
equals(result, "GOODBYE CRUEL WORLD");
+
+ var template = Handlebars.compile('{{goodbye cruel="CRUEL" world="WORLD" print=false}}');
+ var result = template(context, {helpers: helpers});
+ equals(result, "NOT PRINTING");
});
test("block helpers can take an optional hash", function() {
- var template = Handlebars.compile('{{#goodbye cruel="CRUEL"}}world{{/goodbye}}');
+ var template = Handlebars.compile('{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}');
var helpers = {
goodbye: function(options) {
- return "GOODBYE " + options.hash.cruel + " " + options.fn(this);
+ return "GOODBYE " + options.hash.cruel + " " + options.fn(this) + " " + options.hash.times + " TIMES";
}
};
- var result = template({}, helpers);
+ var result = template({}, {helpers: helpers});
+ equals(result, "GOODBYE CRUEL world 12 TIMES");
+});
+
+test("block helpers can take an optional hash with booleans", function() {
+ var helpers = {
+ goodbye: function(options) {
+ if (options.hash.print === true) {
+ return "GOODBYE " + options.hash.cruel + " " + options.fn(this);
+ } else if (options.hash.print === false) {
+ return "NOT PRINTING";
+ } else {
+ return "THIS SHOULD NOT HAPPEN";
+ }
+ }
+ };
+
+ var template = Handlebars.compile('{{#goodbye cruel="CRUEL" print=true}}world{{/goodbye}}');
+ var result = template({}, {helpers: helpers});
equals(result, "GOODBYE CRUEL world");
+
+ var template = Handlebars.compile('{{#goodbye cruel="CRUEL" print=false}}world{{/goodbye}}');
+ var result = template({}, {helpers: helpers});
+ equals(result, "NOT PRINTING");
+});
+
+
+test("arguments to helpers can be retrieved from options hash in string form", function() {
+ var template = Handlebars.compile('{{wycats is.a slave.driver}}', {stringParams: true});
+
+ var helpers = {
+ wycats: function(passiveVoice, noun, options) {
+ return "HELP ME MY BOSS " + passiveVoice + ' ' + noun;
+ }
+ };
+
+ var result = template({}, {helpers: helpers});
+
+ equals(result, "HELP ME MY BOSS is.a slave.driver");
+});
+
+test("when using block form, arguments to helpers can be retrieved from options hash in string form", function() {
+ var template = Handlebars.compile('{{#wycats is.a slave.driver}}help :({{/wycats}}', {stringParams: true});
+
+ var helpers = {
+ wycats: function(passiveVoice, noun, options) {
+ return "HELP ME MY BOSS " + passiveVoice + ' ' +
+ noun + ': ' + options.fn(this);
+ }
+ };
+
+ var result = template({}, {helpers: helpers});
+
+ equals(result, "HELP ME MY BOSS is.a slave.driver: help :(");
});
+
+test("when inside a block in String mode, .. passes the appropriate context in the options hash", function() {
+ var template = Handlebars.compile('{{#with dale}}{{tomdale ../need dad.joke}}{{/with}}', {stringParams: true});
+
+ var helpers = {
+ tomdale: function(desire, noun, options) {
+ return "STOP ME FROM READING HACKER NEWS I " +
+ options.contexts[0][desire] + " " + noun;
+ },
+
+ "with": function(context, options) {
+ return options.fn(options.contexts[0][context]);
+ }
+ };
+
+ var result = template({
+ dale: {},
+
+ need: 'need-a'
+ }, {helpers: helpers});
+
+ equals(result, "STOP ME FROM READING HACKER NEWS I need-a dad.joke");
+});
+
+test("when inside a block in String mode, .. passes the appropriate context in the options hash to a block helper", function() {
+ var template = Handlebars.compile('{{#with dale}}{{#tomdale ../need dad.joke}}wot{{/tomdale}}{{/with}}', {stringParams: true});
+
+ var helpers = {
+ tomdale: function(desire, noun, options) {
+ return "STOP ME FROM READING HACKER NEWS I " +
+ options.contexts[0][desire] + " " + noun + " " +
+ options.fn(this);
+ },
+
+ "with": function(context, options) {
+ return options.fn(options.contexts[0][context]);
+ }
+ };
+
+ var result = template({
+ dale: {},
+
+ need: 'need-a'
+ }, {helpers: helpers});
+
+ equals(result, "STOP ME FROM READING HACKER NEWS I need-a dad.joke wot");
+});
+
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 95496cb..c3c89ef 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -77,9 +77,8 @@ module Handlebars
Handlebars::Spec.js_load('lib/handlebars/ast.js');
Handlebars::Spec.js_load('lib/handlebars/visitor.js');
Handlebars::Spec.js_load('lib/handlebars/printer.js')
- Handlebars::Spec.js_load('lib/handlebars/runtime.js')
Handlebars::Spec.js_load('lib/handlebars/utils.js')
- Handlebars::Spec.js_load('lib/Handlebars/vm.js')
+ Handlebars::Spec.js_load('lib/handlebars/compiler.js')
Handlebars::Spec.js_load('lib/handlebars.js')
context["Handlebars"]["logger"]["level"] = ENV["DEBUG_JS"] ? context["Handlebars"]["logger"][ENV["DEBUG_JS"]] : 4
diff --git a/spec/tokenizer_spec.rb b/spec/tokenizer_spec.rb
index d12566d..9b61fe2 100644
--- a/spec/tokenizer_spec.rb
+++ b/spec/tokenizer_spec.rb
@@ -77,12 +77,18 @@ describe "Tokenizer" do
result[3].should be_token("ID", "foo")
end
- it "tokenizes a simple mustahe with spaces as 'OPEN ID CLOSE'" do
+ it "tokenizes a simple mustache with spaces as 'OPEN ID CLOSE'" do
result = tokenize("{{ foo }}")
result.should match_tokens(%w(OPEN ID CLOSE))
result[1].should be_token("ID", "foo")
end
+ it "tokenizes a simple mustache with line breaks as 'OPEN ID ID CLOSE'" do
+ result = tokenize("{{ foo \n bar }}")
+ result.should match_tokens(%w(OPEN ID ID CLOSE))
+ result[1].should be_token("ID", "foo")
+ end
+
it "tokenizes raw content as 'CONTENT'" do
result = tokenize("foo {{ bar }} baz")
result.should match_tokens(%w(CONTENT OPEN ID CLOSE CONTENT))
@@ -165,6 +171,22 @@ describe "Tokenizer" do
result[2].should be_token("STRING", %{bar"baz})
end
+ it "tokenizes numbers" do
+ result = tokenize(%|{{ foo 1 }}|)
+ result.should match_tokens(%w(OPEN ID INTEGER CLOSE))
+ result[2].should be_token("INTEGER", "1")
+ end
+
+ it "tokenizes booleans" do
+ result = tokenize(%|{{ foo true }}|)
+ result.should match_tokens(%w(OPEN ID BOOLEAN CLOSE))
+ result[2].should be_token("BOOLEAN", "true")
+
+ result = tokenize(%|{{ foo false }}|)
+ result.should match_tokens(%w(OPEN ID BOOLEAN CLOSE))
+ result[2].should be_token("BOOLEAN", "false")
+ end
+
it "tokenizes hash arguments" do
result = tokenize("{{ foo bar=baz }}")
result.should match_tokens %w(OPEN ID ID EQUALS ID CLOSE)
@@ -172,6 +194,18 @@ describe "Tokenizer" do
result = tokenize("{{ foo bar baz=bat }}")
result.should match_tokens %w(OPEN ID ID ID EQUALS ID CLOSE)
+ result = tokenize("{{ foo bar baz=1 }}")
+ result.should match_tokens %w(OPEN ID ID ID EQUALS INTEGER CLOSE)
+
+ result = tokenize("{{ foo bar baz=true }}")
+ result.should match_tokens %w(OPEN ID ID ID EQUALS BOOLEAN CLOSE)
+
+ result = tokenize("{{ foo bar baz=false }}")
+ result.should match_tokens %w(OPEN ID ID ID EQUALS BOOLEAN CLOSE)
+
+ result = tokenize("{{ foo bar\n baz=bat }}")
+ result.should match_tokens %w(OPEN ID ID ID EQUALS ID CLOSE)
+
result = tokenize("{{ foo bar baz=\"bat\" }}")
result.should match_tokens %w(OPEN ID ID ID EQUALS STRING CLOSE)
diff --git a/src/handlebars.l b/src/handlebars.l
index e24e871..0d3cdf0 100644
--- a/src/handlebars.l
+++ b/src/handlebars.l
@@ -13,7 +13,7 @@
<mu>"{{"\s*"else" { return 'OPEN_INVERSE'; }
<mu>"{{{" { return 'OPEN_UNESCAPED'; }
<mu>"{{&" { return 'OPEN_UNESCAPED'; }
-<mu>"{{!".*?"}}" { yytext = yytext.substr(3,yyleng-5); this.begin("INITIAL"); return 'COMMENT'; }
+<mu>"{{!"[\s\S]*?"}}" { yytext = yytext.substr(3,yyleng-5); this.begin("INITIAL"); return 'COMMENT'; }
<mu>"{{" { return 'OPEN'; }
<mu>"=" { return 'EQUALS'; }
@@ -24,7 +24,10 @@
<mu>"}}}" { this.begin("INITIAL"); return 'CLOSE'; }
<mu>"}}" { this.begin("INITIAL"); return 'CLOSE'; }
<mu>'"'("\\"["]|[^"])*'"' { yytext = yytext.substr(1,yyleng-2).replace(/\\"/g,'"'); return 'STRING'; }
-<mu>[a-zA-Z0-9_]+/[=} /.] { return 'ID'; }
+<mu>"true"/[}\s] { return 'BOOLEAN'; }
+<mu>"false"/[}\s] { return 'BOOLEAN'; }
+<mu>[0-9]+/[}\s] { return 'INTEGER'; }
+<mu>[a-zA-Z0-9_$-]+/[=}\s/.] { return 'ID'; }
<mu>. { return 'INVALID'; }
<INITIAL,mu><<EOF>> { return 'EOF'; }
diff --git a/src/handlebars.yy b/src/handlebars.yy
index f119df2..d3d41df 100644
--- a/src/handlebars.yy
+++ b/src/handlebars.yy
@@ -68,6 +68,8 @@ params
param
: path { $$ = $1 }
| STRING { $$ = new yy.StringNode($1) }
+ | INTEGER { $$ = new yy.IntegerNode($1) }
+ | BOOLEAN { $$ = new yy.BooleanNode($1) }
;
hash
@@ -82,6 +84,8 @@ hashSegments
hashSegment
: ID EQUALS path { $$ = [$1, $3] }
| ID EQUALS STRING { $$ = [$1, new yy.StringNode($3)] }
+ | ID EQUALS INTEGER { $$ = [$1, new yy.IntegerNode($3)] }
+ | ID EQUALS BOOLEAN { $$ = [$1, new yy.BooleanNode($3)] }
;
path