diff options
-rw-r--r-- | README.markdown | 23 | ||||
-rw-r--r-- | Rakefile | 24 | ||||
-rw-r--r-- | lib/handlebars.js | 3 | ||||
-rw-r--r-- | lib/handlebars/ast.js | 13 | ||||
-rw-r--r-- | lib/handlebars/base.js | 26 | ||||
-rw-r--r-- | lib/handlebars/compiler.js (renamed from lib/handlebars/vm.js) | 127 | ||||
-rw-r--r-- | lib/handlebars/printer.js | 8 | ||||
-rw-r--r-- | lib/handlebars/runtime.js | 267 | ||||
-rw-r--r-- | lib/handlebars/utils.js | 11 | ||||
-rw-r--r-- | spec/parser_spec.rb | 57 | ||||
-rw-r--r-- | spec/qunit_spec.js | 262 | ||||
-rw-r--r-- | spec/spec_helper.rb | 3 | ||||
-rw-r--r-- | spec/tokenizer_spec.rb | 36 | ||||
-rw-r--r-- | src/handlebars.l | 7 | ||||
-rw-r--r-- | src/handlebars.yy | 4 |
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 ----------- @@ -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 = { "<": "<", - ">": ">" + ">": ">", + '"': """, + "'": "'", + "`": "`" }; - var badChars = /&(?!\w+;)|[<>]/g; - var possible = /[&<>]/ + var badChars = /&(?!\w+;)|[<>"'`]/g; + var possible = /[&<>"'`]/; var escapeChar = function(chr) { - return escape[chr] || "&" + return escape[chr] || "&"; }; 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: "&\"\\<>"}, '&\"\\<>', - "by default expressions should be escaped"); - shouldCompileTo("{{&awesome}}", {awesome: "&\"\\<>"}, '&\"\\<>', "expressions with {{& handlebars aren't escaped"); + shouldCompileTo("{{awesome}}", {awesome: "&\"'`\\<>"}, '&"'`\\<>', + "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'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 |