diff options
35 files changed, 1055 insertions, 290 deletions
@@ -4,6 +4,7 @@ vendor lib/handlebars/compiler/parser.js /dist/ /tmp/ +/coverage/ node_modules *.sublime-project *.sublime-workspace diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..1739275 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "spec/mustache"] + path = spec/mustache + url = git://github.com/mustache/spec.git diff --git a/.istanbul.yml b/.istanbul.yml new file mode 100644 index 0000000..e6911f1 --- /dev/null +++ b/.istanbul.yml @@ -0,0 +1,2 @@ +instrumentation: + excludes: ['**/spec/**'] @@ -35,3 +35,6 @@ On the client side. Should these match, please file an issue with us, per our [issue filing guidelines](https://github.com/wycats/handlebars.js/blob/master/README.markdown#reporting-issues). + +1. Why doesn't IE like the `default` name in the AMD module? + Some browsers such as particular versions of IE treat `default` as a reserved word in JavaScript source files. To safely use this you need to reference this via the `Handlebars['default']` lookup method. This is an unfortunate side effect of the shims necessary to backport the handlebars ES6 code to all current browsers. diff --git a/Gruntfile.js b/Gruntfile.js index 4e12ef7..6cbef6e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -138,13 +138,11 @@ module.exports = function(grunt) { browsers: [ {browserName: 'chrome'}, {browserName: 'firefox'}, - {browserName: 'firefox', version: '3.6'}, {browserName: 'safari', version: 7, platform: 'OS X 10.9'}, {browserName: 'safari', version: 6, platform: 'OS X 10.8'}, {browserName: 'internet explorer', version: 11, platform: 'Windows 8.1'}, {browserName: 'internet explorer', version: 10, platform: 'Windows 8'}, - {browserName: 'internet explorer', version: 9, platform: 'Windows 7'}, - {browserName: 'internet explorer', version: 6, platform: 'XP'} + {browserName: 'internet explorer', version: 9, platform: 'Windows 7'} ] } } diff --git a/README.markdown b/README.markdown index 87b17aa..866661e 100644 --- a/README.markdown +++ b/README.markdown @@ -1,4 +1,4 @@ -[](https://travis-ci.org/wycats/handlebars.js) +[](https://travis-ci.org/wycats/handlebars.js) [](https://saucelabs.com/u/handlebars) Handlebars.js @@ -291,9 +291,8 @@ name sans the extension. These templates may be executed in the same manner as templates. If using the simple mode the precompiler will generate a single -javascript method. To execute this method it must be passed to the using -the `Handlebars.template` method and the resulting object may be as -normal. +javascript method. To execute this method it must be passed to +the `Handlebars.template` method and the resulting object may be used as normal. ### Optimizations @@ -339,6 +338,14 @@ rewritten Handlebars (current version) is faster than the old version, and we will have some benchmarks in the near future. +Mustache Compatibilty +--------------------- + +Handlebars deviates from the Mustache spec in a few key ways: +- Alternative delimeters are not supported +- Recrusive value lookup is not enabled by default. The compile time `compat` flag must be set to enable this functionality. Users should note that there is a performance cost for enabling this flag. The exact cost varies by template, but it's recommended that performance sensitive operations should avoid this mode and instead opt for explicit path references. +- The optional Mustache-style lambdas are not supported. Instead Handlebars provides it's own lambda resolution that follows the behaviors of helpers. + Building -------- diff --git a/bench/throughput.js b/bench/throughput.js index 308446a..d27a94d 100644 --- a/bench/throughput.js +++ b/bench/throughput.js @@ -28,11 +28,13 @@ function makeSuite(bench, name, template, handlebarsOnly) { partials = template.partials, handlebarsOut, + compatOut, dustOut, ecoOut, mustacheOut; var handlebar = Handlebars.compile(template.handlebars, {data: false}), + compat = Handlebars.compile(template.handlebars, {data: false, compat: true}), options = {helpers: template.helpers}; _.each(template.partials && template.partials.handlebars, function(partial, name) { Handlebars.registerPartial(name, Handlebars.compile(partial, {data: false})); @@ -43,6 +45,11 @@ function makeSuite(bench, name, template, handlebarsOnly) { handlebar(context, options); }); + compatOut = compat(context, options); + bench("compat", function() { + compat(context, options); + }); + if (handlebarsOnly) { return; } @@ -107,6 +114,7 @@ function makeSuite(bench, name, template, handlebarsOnly) { } } + compare(compatOut, 'compat'); compare(dustOut, 'dust'); compare(ecoOut, 'eco'); compare(mustacheOut, 'mustache'); diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js index 9b910c7..e11f032 100644 --- a/lib/handlebars/base.js +++ b/lib/handlebars/base.js @@ -30,12 +30,11 @@ HandlebarsEnvironment.prototype = { logger: logger, log: log, - registerHelper: function(name, fn, inverse) { + registerHelper: function(name, fn) { if (toString.call(name) === objectType) { - if (inverse || fn) { throw new Exception('Arg not supported with multiple helpers'); } + if (fn) { throw new Exception('Arg not supported with multiple helpers'); } Utils.extend(this.helpers, name); } else { - if (inverse) { fn.not = inverse; } this.helpers[name] = fn; } }, @@ -67,9 +66,8 @@ function registerDefaultHelpers(instance) { }); instance.registerHelper('blockHelperMissing', function(context, options) { - var inverse = options.inverse || function() {}, fn = options.fn; - - if (isFunction(context)) { context = context.call(this); } + var inverse = options.inverse, + fn = options.fn; if(context === true) { return fn(this); @@ -185,6 +183,8 @@ function registerDefaultHelpers(instance) { } return fn(context, options); + } else { + return options.inverse(this); } }); @@ -219,7 +219,7 @@ export var logger = { } }; -export function log(level, obj) { logger.log(level, obj); } +export var log = logger.log; export var createFrame = function(object) { var frame = Utils.extend({}, object); diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 5a3057f..5a47e2b 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -77,6 +77,8 @@ var AST = { this.context = context; this.hash = hash; this.strip = strip; + + this.strip.inlineStandalone = true; }, BlockNode: function(mustache, program, inverse, strip, locInfo) { @@ -200,9 +202,14 @@ var AST = { LocationInfo.call(this, locInfo); this.type = "comment"; this.comment = comment; + + this.strip = { + inlineStandalone: true + }; } }; + // Must be exported as an object rather than the root of the module as the jison lexer // most modify the object to operate properly. export default AST; diff --git a/lib/handlebars/compiler/base.js b/lib/handlebars/compiler/base.js index 7ab1843..1378463 100644 --- a/lib/handlebars/compiler/base.js +++ b/lib/handlebars/compiler/base.js @@ -1,19 +1,18 @@ import parser from "./parser"; import AST from "./ast"; -import { stripFlags, prepareBlock } from "./helpers"; +module Helpers from "./helpers"; +import { extend } from "../utils"; export { parser }; +var yy = {}; +extend(yy, Helpers, AST); + export function parse(input) { // Just return if an already-compile AST was passed in. if (input.constructor === AST.ProgramNode) { return input; } - for (var key in AST) { - parser.yy[key] = AST[key]; - } - - parser.yy.stripFlags = stripFlags; - parser.yy.prepareBlock = prepareBlock; + parser.yy = yy; return parser.parse(input); } diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 3e83a62..a4bd09a 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -1,4 +1,5 @@ import Exception from "../exception"; +import {isArray} from "../utils"; export function Compiler() {} @@ -10,30 +11,6 @@ export function Compiler() {} Compiler.prototype = { compiler: Compiler, - disassemble: function() { - var opcodes = this.opcodes, opcode, out = [], params, param; - - for (var i=0, l=opcodes.length; i<l; i++) { - opcode = opcodes[i]; - - if (opcode.opcode === 'DECLARE') { - out.push("DECLARE " + opcode.name + "=" + opcode.value); - } else { - params = []; - for (var j=0; j<opcode.args.length; j++) { - param = opcode.args[j]; - if (typeof param === "string") { - param = "\"" + param.replace("\n", "\\n") + "\""; - } - params.push(param); - } - out.push(opcode.opcode + " " + params.join(" ")); - } - } - - return out.join("\n"); - }, - equals: function(other) { var len = this.opcodes.length; if (other.opcodes.length !== len) { @@ -43,20 +20,14 @@ Compiler.prototype = { for (var i = 0; i < len; i++) { var opcode = this.opcodes[i], otherOpcode = other.opcodes[i]; - if (opcode.opcode !== otherOpcode.opcode || opcode.args.length !== otherOpcode.args.length) { + if (opcode.opcode !== otherOpcode.opcode || !argEquals(opcode.args, otherOpcode.args)) { return false; } - for (var j = 0; j < opcode.args.length; j++) { - if (opcode.args[j] !== otherOpcode.args[j]) { - return false; - } - } } + // We know that length is the same between the two arrays because they are directly tied + // to the opcode behavior above. len = this.children.length; - if (other.children.length !== len) { - return false; - } for (i = 0; i < len; i++) { if (!this.children[i].equals(other.children[i])) { return false; @@ -214,15 +185,18 @@ Compiler.prototype = { if (partial.context) { this.accept(partial.context); } else { - this.opcode('push', 'depth0'); + this.opcode('getContext', 0); + this.opcode('pushContext'); } - this.opcode('invokePartial', partialName.name); + this.opcode('invokePartial', partialName.name, partial.indent || ''); this.opcode('append'); }, content: function(content) { - this.opcode('appendContent', content.string); + if (!content.omit) { + this.opcode('appendContent', content.string); + } }, mustache: function(mustache) { @@ -305,7 +279,7 @@ Compiler.prototype = { // Context reference, i.e. `{{foo .}}` or `{{foo ..}}` this.opcode('pushContext'); } else { - this.opcode('lookupOnContext', id.parts, id.falsy); + this.opcode('lookupOnContext', id.parts, id.falsy, id.isScoped); } }, @@ -333,10 +307,6 @@ Compiler.prototype = { this.opcodes.push({ opcode: name, args: [].slice.call(arguments, 1) }); }, - declare: function(name, value) { - this.opcodes.push({ opcode: 'DECLARE', name: name, value: value }); - }, - addDepth: function(depth) { if(depth === 0) { return; } @@ -421,6 +391,9 @@ export function precompile(input, options, env) { if (!('data' in options)) { options.data = true; } + if (options.compat) { + options.useDepths = true; + } var ast = env.parse(input); var environment = new env.Compiler().compile(ast, options); @@ -437,6 +410,9 @@ export function compile(input, options, env) { if (!('data' in options)) { options.data = true; } + if (options.compat) { + options.useDepths = true; + } var compiled; @@ -468,3 +444,18 @@ export function compile(input, options, env) { }; return ret; } + +function argEquals(a, b) { + if (a === b) { + return true; + } + + if (isArray(a) && isArray(b) && a.length === b.length) { + for (var i = 0; i < a.length; i++) { + if (!argEquals(a[i], b[i])) { + return false; + } + } + return true; + } +} diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index 1a2bd26..5d8fec1 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -1,5 +1,4 @@ import Exception from "../exception"; -import AST from "./ast"; export function stripFlags(open, close) { return { @@ -8,34 +7,157 @@ export function stripFlags(open, close) { }; } + export function prepareBlock(mustache, program, inverseAndProgram, close, inverted, locInfo) { + /*jshint -W040 */ if (mustache.sexpr.id.original !== close.path.original) { - throw new Exception(mustache.sexpr.id.original + " doesn't match " + close.path.original, mustache); + throw new Exception(mustache.sexpr.id.original + ' doesn\'t match ' + close.path.original, mustache); } - var inverse, strip; + var inverse = inverseAndProgram && inverseAndProgram.program; - strip = { + var strip = { left: mustache.strip.left, - right: close.strip.right + right: close.strip.right, + + // Determine the standalone candiacy. Basically flag our content as being possibly standalone + // so our parent can determine if we actually are standalone + openStandalone: isNextWhitespace(program.statements), + closeStandalone: isPrevWhitespace((inverse || program).statements) }; - if (inverseAndProgram) { - inverse = inverseAndProgram.program; + if (inverse) { var inverseStrip = inverseAndProgram.strip; program.strip.left = mustache.strip.right; program.strip.right = inverseStrip.left; inverse.strip.left = inverseStrip.right; inverse.strip.right = close.strip.left; + + // Find standalone else statments + if (isPrevWhitespace(program.statements) + && isNextWhitespace(inverse.statements)) { + + omitLeft(program.statements); + omitRight(inverse.statements); + } } else { program.strip.left = mustache.strip.right; program.strip.right = close.strip.left; } if (inverted) { - return new AST.BlockNode(mustache, inverse, program, strip, locInfo); + return new this.BlockNode(mustache, inverse, program, strip, locInfo); } else { - return new AST.BlockNode(mustache, program, inverse, strip, locInfo); + return new this.BlockNode(mustache, program, inverse, strip, locInfo); + } +} + + +export function prepareProgram(statements, isRoot) { + for (var i = 0, l = statements.length; i < l; i++) { + var current = statements[i], + strip = current.strip; + + if (!strip) { + continue; + } + + var _isPrevWhitespace = isPrevWhitespace(statements, i, isRoot, current.type === 'partial'), + _isNextWhitespace = isNextWhitespace(statements, i, isRoot), + + openStandalone = strip.openStandalone && _isPrevWhitespace, + closeStandalone = strip.closeStandalone && _isNextWhitespace, + inlineStandalone = strip.inlineStandalone && _isPrevWhitespace && _isNextWhitespace; + + if (inlineStandalone) { + omitRight(statements, i); + + if (omitLeft(statements, i)) { + // If we are on a standalone node, save the indent info for partials + if (current.type === 'partial') { + current.indent = statements[i-1].original; + } + } + } + if (openStandalone) { + omitRight((current.program || current.inverse).statements); + + // Strip out the previous content node if it's whitespace only + omitLeft(statements, i); + } + if (closeStandalone) { + // Always strip the next node + omitRight(statements, i); + + omitLeft((current.inverse || current.program).statements); + } + } + + return statements; +} + +function isPrevWhitespace(statements, i, isRoot, disallowIndent) { + if (i === undefined) { + i = statements.length; + } + + // Nodes that end with newlines are considered whitespace (but are special + // cased for strip operations) + var prev = statements[i-1]; + if (prev && /\n$/.test(prev.string)) { + return true; + } + + return checkWhitespace(isRoot, prev, statements[i-2]); +} +function isNextWhitespace(statements, i, isRoot) { + if (i === undefined) { + i = -1; + } + + return checkWhitespace(isRoot, statements[i+1], statements[i+2]); +} +function checkWhitespace(isRoot, next1, next2, disallowIndent) { + if (!next1) { + return isRoot; + } else if (next1.type === 'content') { + // Check if the previous node is empty or whitespace only + if (disallowIndent ? !next1.string : /^[\s]*$/.test(next1.string)) { + if (next2) { + return next2.type === 'content' || /\n$/.test(next1.string); + } else { + return isRoot || (next1.string.indexOf('\n') >= 0); + } + } + } +} + +// Marks the node to the right of the position as omitted. +// I.e. {{foo}}' ' will mark the ' ' node as omitted. +// +// If i is undefined, then the first child will be marked as such. +function omitRight(statements, i) { + var first = statements[i == null ? 0 : i + 1]; + if (first) { + first.omit = true; + } +} + +// Marks the node to the left of the position as omitted. +// I.e. ' '{{foo}} will mark the ' ' node as omitted. +// +// If i is undefined then the last child will be marked as such. +function omitLeft(statements, i) { + if (i === undefined) { + i = statements.length; + } + + var last = statements[i-1], + prev = statements[i-2]; + + // We omit the last node if it's whitespace only and not preceeded by a non-content node. + if (last && /^[\s]*$/.test(last.string) && (!prev || prev.type === 'content')) { + return last.omit = true; } } diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index c4f3c27..25d45d1 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -17,6 +17,11 @@ JavaScriptCompiler.prototype = { return parent + "['" + name + "']"; } }, + depthedLookup: function(name) { + this.aliases.lookup = 'this.lookup'; + + return 'lookup(depths, "' + name + '")'; + }, compilerInfo: function() { var revision = COMPILER_REVISION, @@ -69,6 +74,8 @@ JavaScriptCompiler.prototype = { this.compileChildren(environment, options); + this.useDepths = this.useDepths || environment.depths.list.length || this.options.compat; + var opcodes = environment.opcodes, opcode, i, @@ -77,11 +84,7 @@ JavaScriptCompiler.prototype = { for (i = 0, l = opcodes.length; i < l; i++) { opcode = opcodes[i]; - if(opcode.opcode === 'DECLARE') { - this[opcode.name] = opcode.value; - } else { - this[opcode.opcode].apply(this, opcode.args); - } + this[opcode.opcode].apply(this, opcode.args); // Reset the stripNext flag if it was not set by this operation. if (opcode.opcode !== this.stripNext) { @@ -92,6 +95,7 @@ JavaScriptCompiler.prototype = { // Flush any trailing content that might be pending. this.pushSource(''); + /* istanbul ignore next */ if (this.stackSlot || this.inlineStack.length || this.compileStack.length) { throw new Exception('Compile completed with content left on stack'); } @@ -115,6 +119,12 @@ JavaScriptCompiler.prototype = { if (this.options.data) { ret.useData = true; } + if (this.useDepths) { + ret.useDepths = true; + } + if (this.options.compat) { + ret.compat = true; + } if (!asObject) { ret.compiler = JSON.stringify(ret.compiler); @@ -151,8 +161,8 @@ JavaScriptCompiler.prototype = { var params = ["depth0", "helpers", "partials", "data"]; - for(var i=0, l=this.environment.depths.list.length; i<l; i++) { - params.push("depth" + this.environment.depths.list[i]); + if (this.useDepths) { + params.push('depths'); } // Perform a second pass over the output to merge content when possible @@ -230,13 +240,13 @@ JavaScriptCompiler.prototype = { blockValue: function(name) { this.aliases.blockHelperMissing = 'helpers.blockHelperMissing'; - var params = ["depth0"]; + var params = [this.contextName(0)]; this.setupParams(name, 0, params); - this.replaceStack(function(current) { - params.splice(1, 0, current); - return "blockHelperMissing.call(" + params.join(", ") + ")"; - }); + var blockName = this.popStack(); + params.splice(1, 0, blockName); + + this.push('blockHelperMissing.call(' + params.join(', ') + ')'); }, // [ambiguousBlockValue] @@ -249,7 +259,7 @@ JavaScriptCompiler.prototype = { this.aliases.blockHelperMissing = 'helpers.blockHelperMissing'; // We're being a bit cheeky and reusing the options value from the prior exec - var params = ["depth0"]; + var params = [this.contextName(0)]; this.setupParams('', 0, params, true); this.flushInline(); @@ -341,7 +351,7 @@ JavaScriptCompiler.prototype = { // // Pushes the value of the current context onto the stack. pushContext: function() { - this.pushStackLiteral('depth' + this.lastContext); + this.pushStackLiteral(this.contextName(this.lastContext)); }, // [lookupOnContext] @@ -351,21 +361,25 @@ JavaScriptCompiler.prototype = { // // Looks up the value of `name` on the current context and pushes // it onto the stack. - lookupOnContext: function(parts, falsy) { + lookupOnContext: function(parts, falsy, scoped) { /*jshint -W083 */ - this.pushContext(); - if (!parts) { - return; + var i = 0, + len = parts.length; + + if (!scoped && this.options.compat && !this.lastContext) { + // The depthed query is expected to handle the undefined logic for the root level that + // is implemented below, so we evaluate that directly in compat mode + this.push(this.depthedLookup(parts[i++])); + } else { + this.pushContext(); } - var len = parts.length; - for (var i = 0; i < len; i++) { + for (; i < len; i++) { this.replaceStack(function(current) { var lookup = this.nameLookup(current, parts[i], 'context'); - // We want to ensure that zero and false are handled properly for the first element - // of non-chained elements, if the context (falsy flag) needs to have the special - // handling for these values. - if (!falsy && !i && len === 1) { + // We want to ensure that zero and false are handled properly if the context (falsy flag) + // needs to have the special handling for these values. + if (!falsy) { return ' != null ? ' + lookup + ' : ' + current; } else { // Otherwise we can use generic falsy handling @@ -388,9 +402,6 @@ JavaScriptCompiler.prototype = { } else { this.pushStackLiteral('this.data(data, ' + depth + ')'); } - if (!parts) { - return; - } var len = parts.length; for (var i = 0; i < len; i++) { @@ -410,7 +421,7 @@ JavaScriptCompiler.prototype = { resolvePossibleLambda: function() { this.aliases.lambda = 'this.lambda'; - this.push('lambda(' + this.popStack() + ', depth0)'); + this.push('lambda(' + this.popStack() + ', ' + this.contextName(0) + ')'); }, // [pushStringParam] @@ -422,8 +433,7 @@ JavaScriptCompiler.prototype = { // provides the string value of a parameter along with its // depth rather than resolving it immediately. pushStringParam: function(string, type) { - this.pushStackLiteral('depth' + this.lastContext); - + this.pushContext(); this.pushString(type); // If it's a subexpression, the string result @@ -534,18 +544,7 @@ JavaScriptCompiler.prototype = { var helper = this.setupHelper(paramSize, name); var lookup = (isSimple ? helper.name + ' || ' : '') + nonHelper + ' || helperMissing'; - if (helper.paramsInit) { - lookup += ',' + helper.paramsInit; - } - this.push('((' + lookup + ').call(' + helper.callParams + '))'); - - // Always flush subexpressions. This is both to prevent the compounding size issue that - // occurs when the code has to be duplicated for inlining and also to prevent errors - // due to the incorrect options object being passed due to the shared register. - if (!isRoot) { - this.flushInline(); - } }, // [invokeKnownHelper] @@ -596,11 +595,16 @@ JavaScriptCompiler.prototype = { // // This operation pops off a context, invokes a partial with that context, // and pushes the result of the invocation back. - invokePartial: function(name) { - var params = [this.nameLookup('partials', name, 'partial'), "'" + name + "'", this.popStack(), this.popStack(), "helpers", "partials"]; + invokePartial: function(name, indent) { + var params = [this.nameLookup('partials', name, 'partial'), "'" + indent + "'", "'" + name + "'", this.popStack(), this.popStack(), "helpers", "partials"]; if (this.options.data) { params.push("data"); + } else if (this.options.compat) { + params.push('undefined'); + } + if (this.options.compat) { + params.push('depths'); } this.push("this.invokePartial(" + params.join(", ") + ")"); @@ -669,6 +673,8 @@ JavaScriptCompiler.prototype = { child.name = 'program' + index; this.context.programs[index] = compiler.compile(child, options, this.context, !this.precompile); this.context.environments[index] = child; + + this.useDepths = this.useDepths || compiler.useDepths; } else { child.index = index; child.name = 'program' + index; @@ -685,27 +691,18 @@ JavaScriptCompiler.prototype = { }, programExpression: function(guid) { - if(guid == null) { - return 'this.noop'; - } - var child = this.environment.children[guid], - depths = child.depths.list, depth; + depths = child.depths.list, + useDepths = this.useDepths, + depth; var programParams = [child.index, 'data']; - for(var i=0, l = depths.length; i<l; i++) { - depth = depths[i]; - - programParams.push('depth' + (depth - 1)); + if (useDepths) { + programParams.push('depths'); } - return (depths.length === 0 ? 'this.program(' : 'this.programWithDepth(') + programParams.join(', ') + ')'; - }, - - register: function(name, val) { - this.useRegister(name); - this.pushSource(name + " = " + val + ";"); + return 'this.program(' + programParams.join(', ') + ')'; }, useRegister: function(name) { @@ -734,9 +731,7 @@ JavaScriptCompiler.prototype = { this.flushInline(); var stack = this.incrStack(); - if (item) { - this.pushSource(stack + " = " + item + ";"); - } + this.pushSource(stack + " = " + item + ";"); this.compileStack.push(stack); return stack; }, @@ -748,54 +743,40 @@ JavaScriptCompiler.prototype = { createdStack, usedLiteral; - // If we are currently inline then we want to merge the inline statement into the - // replacement statement via ',' - if (inline) { - var top = this.popStack(true); + /* istanbul ignore next */ + if (!this.isInline()) { + throw new Exception('replaceStack on non-inline'); + } - if (top instanceof Literal) { - // Literals do not need to be inlined - stack = top.value; - usedLiteral = true; + // We want to merge the inline statement into the replacement statement via ',' + var top = this.popStack(true); - if (chain) { - prefix = stack; - } - } else { - // Get or create the current stack name for use by the inline - createdStack = !this.stackSlot; - var name = !createdStack ? this.topStackName() : this.incrStack(); + if (top instanceof Literal) { + // Literals do not need to be inlined + stack = top.value; + usedLiteral = true; - prefix = '(' + this.push(name) + ' = ' + top + (chain ? ')' : '),'); - stack = this.topStack(); + if (chain) { + prefix = stack; } } else { + // Get or create the current stack name for use by the inline + createdStack = !this.stackSlot; + var name = !createdStack ? this.topStackName() : this.incrStack(); + + prefix = '(' + this.push(name) + ' = ' + top + (chain ? ')' : '),'); stack = this.topStack(); } var item = callback.call(this, stack); - if (inline) { - if (!usedLiteral) { - this.popStack(); - } - if (createdStack) { - this.stackSlot--; - } - this.push('(' + prefix + item + ')'); - } else { - // Prevent modification of the context depth variable. Through replaceStack - if (!/^stack/.test(stack)) { - stack = this.nextStack(); - } - - this.pushSource(stack + " = (" + prefix + item + ");"); + if (!usedLiteral) { + this.popStack(); } - return stack; - }, - - nextStack: function() { - return this.pushStack(); + if (createdStack) { + this.stackSlot--; + } + this.push('(' + prefix + item + ')'); }, incrStack: function() { @@ -832,6 +813,7 @@ JavaScriptCompiler.prototype = { return item.value; } else { if (!inline) { + /* istanbul ignore next */ if (!this.stackSlot) { throw new Exception('Invalid stack pop'); } @@ -841,17 +823,25 @@ JavaScriptCompiler.prototype = { } }, - topStack: function(wrapped) { + topStack: function() { var stack = (this.isInline() ? this.inlineStack : this.compileStack), item = stack[stack.length - 1]; - if (!wrapped && (item instanceof Literal)) { + if (item instanceof Literal) { return item.value; } else { return item; } }, + contextName: function(context) { + if (this.useDepths && context) { + return 'depths[' + context + ']'; + } else { + return 'depth' + context; + } + }, + quotedString: function(str) { return '"' + str .replace(/\\/g, '\\\\') @@ -883,7 +873,7 @@ JavaScriptCompiler.prototype = { params: params, paramsInit: paramsInit, name: foundHelper, - callParams: ["depth0"].concat(params).join(", ") + callParams: [this.contextName(0)].concat(params).join(", ") }; }, diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index b9fc77d..0780fbf 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -23,26 +23,43 @@ export function checkRevision(compilerInfo) { // TODO: Remove this line and break up compilePartial export function template(templateSpec, env) { + /* istanbul ignore next */ if (!env) { throw new Exception("No environment passed to template"); } + if (!templateSpec || !templateSpec.main) { + throw new Exception('Unknown template object: ' + typeof templateSpec); + } // Note: Using env.VM references rather than local var references throughout this section to allow // for external users to override these as psuedo-supported APIs. env.VM.checkRevision(templateSpec.compiler); - var invokePartialWrapper = function(partial, name, context, hash, helpers, partials, data) { + var invokePartialWrapper = function(partial, indent, name, context, hash, helpers, partials, data, depths) { if (hash) { context = Utils.extend({}, context, hash); } - var result = env.VM.invokePartial.call(this, partial, name, context, helpers, partials, data); - if (result != null) { return result; } + var result = env.VM.invokePartial.call(this, partial, name, context, helpers, partials, data, depths); - if (env.compile) { - var options = { helpers: helpers, partials: partials, data: data }; - partials[name] = env.compile(partial, { data: data !== undefined }, env); - return partials[name](context, options); + if (result == null && env.compile) { + var options = { helpers: helpers, partials: partials, data: data, depths: depths }; + partials[name] = env.compile(partial, { data: data !== undefined, compat: templateSpec.compat }, env); + result = partials[name](context, options); + } + if (result != null) { + if (indent) { + var lines = result.split('\n'); + for (var i = 0, l = lines.length; i < l; i++) { + if (!lines[i] && i + 1 === l) { + break; + } + + lines[i] = indent + lines[i]; + } + result = lines.join('\n'); + } + return result; } else { throw new Exception("The partial " + name + " could not be compiled when running in runtime-only mode"); } @@ -50,6 +67,14 @@ export function template(templateSpec, env) { // Just add water var container = { + lookup: function(depths, name) { + var len = depths.length; + for (var i = 0; i < len; i++) { + if (depths[i] && depths[i][name] != null) { + return depths[i][name]; + } + } + }, lambda: function(current, context) { return typeof current === 'function' ? current.call(context) : current; }, @@ -62,17 +87,16 @@ export function template(templateSpec, env) { }, programs: [], - program: function(i, data) { + program: function(i, data, depths) { var programWrapper = this.programs[i], fn = this.fn(i); - if(data) { - programWrapper = program(this, i, fn, data); + if (data || depths) { + programWrapper = program(this, i, fn, data, depths); } else if (!programWrapper) { programWrapper = this.programs[i] = program(this, i, fn); } return programWrapper; }, - programWithDepth: env.VM.programWithDepth, data: function(data, depth) { while (data && depth--) { @@ -104,7 +128,12 @@ export function template(templateSpec, env) { if (!options.partial && templateSpec.useData) { data = initData(context, data); } - return templateSpec.main.call(container, context, container.helpers, container.partials, data); + var depths; + if (templateSpec.useDepths) { + depths = options.depths ? [context].concat(options.depths) : [context]; + } + + return templateSpec.main.call(container, context, container.helpers, container.partials, data, depths); }; ret._setup = function(options) { @@ -121,40 +150,29 @@ export function template(templateSpec, env) { }; ret._child = function(i) { + if (templateSpec.depth) { + throw new Exception('_child can not be used with depthed methods'); + } + + // TODO : Fix this return container.programWithDepth(i); }; return ret; } -export function programWithDepth(i, data /*, $depth */) { - /*jshint -W040 */ - var args = Array.prototype.slice.call(arguments, 2), - container = this, - fn = container.fn(i); - - var prog = function(context, options) { - options = options || {}; - - return fn.apply(container, [context, container.helpers, container.partials, options.data || data].concat(args)); - }; - prog.program = i; - prog.depth = args.length; - return prog; -} - -export function program(container, i, fn, data) { +export function program(container, i, fn, data, depths) { var prog = function(context, options) { options = options || {}; - return fn.call(container, context, container.helpers, container.partials, options.data || data); + return fn.call(container, context, container.helpers, container.partials, options.data || data, depths && [context].concat(depths)); }; prog.program = i; - prog.depth = 0; + prog.depth = depths ? depths.length : 0; return prog; } -export function invokePartial(partial, name, context, helpers, partials, data) { - var options = { partial: true, helpers: helpers, partials: partials, data: data }; +export function invokePartial(partial, name, context, helpers, partials, data, depths) { + var options = { partial: true, helpers: helpers, partials: partials, data: data, depths: depths }; if(partial === undefined) { throw new Exception("The partial " + name + " could not be found"); diff --git a/lib/handlebars/utils.js b/lib/handlebars/utils.js index f2f1a54..087183e 100644 --- a/lib/handlebars/utils.js +++ b/lib/handlebars/utils.js @@ -14,7 +14,7 @@ var badChars = /[&<>"'`]/g; var possible = /[&<>"'`]/; function escapeChar(chr) { - return escape[chr] || "&"; + return escape[chr]; } export function extend(obj /* , ...source */) { @@ -37,6 +37,7 @@ var isFunction = function(value) { return typeof value === 'function'; }; // fallback for older versions of Chrome and Safari +/* istanbul ignore next */ if (isFunction(/x/)) { isFunction = function(value) { return typeof value === 'function' && toString.call(value) === '[object Function]'; @@ -44,6 +45,7 @@ if (isFunction(/x/)) { } export var isFunction; +/* istanbul ignore next */ export var isArray = Array.isArray || function(value) { return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false; }; diff --git a/lib/index.js b/lib/index.js index e150524..790aab7 100644 --- a/lib/index.js +++ b/lib/index.js @@ -14,6 +14,7 @@ handlebars.print = printer.print; module.exports = handlebars; // Publish a Node.js require() handler for .handlebars and .hbs files +/* istanbul ignore else */ if (typeof require !== 'undefined' && require.extensions) { var extension = function(module, filename) { var fs = require("fs"); diff --git a/package.json b/package.json index b8b3b3b..8e69350 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "benchmark": "~1.0", "dustjs-linkedin": "~2.0.2", "eco": "~1.1.0-rc-3", + "es6-module-packager": "1.x", "grunt": "~0.4.1", "grunt-cli": "~0.1.10", "grunt-contrib-clean": "~0.4.1", @@ -42,11 +43,11 @@ "grunt-contrib-requirejs": "~0.4.1", "grunt-contrib-uglify": "~0.2.2", "grunt-contrib-watch": "~0.5.3", - "grunt-saucelabs": "~5.0.1", - "es6-module-packager": "1.x", + "grunt-saucelabs": "8.x", + "istanbul": "^0.3.0", "jison": "~0.3.0", "keen.io": "0.0.3", - "mocha": "*", + "mocha": "~1.20.0", "mustache": "~0.7.2", "semver": "~2.1.0", "underscore": "~1.5.1" diff --git a/spec/ast.js b/spec/ast.js index f703054..cba148b 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -78,7 +78,7 @@ describe('ast', function() { var sexprNode = new handlebarsEnv.AST.SexprNode([{ original: 'foo'}], null); var mustacheNode = new handlebarsEnv.AST.MustacheNode(sexprNode, null, '{{', {}); var block = new handlebarsEnv.AST.BlockNode(mustacheNode, - {strip: {}}, {strip: {}}, + {statements: [], strip: {}}, {statements: [], strip: {}}, { strip: {}, path: {original: 'foo'} @@ -190,9 +190,8 @@ describe('ast', function() { }); }); - describe("ProgramNode", function(){ - - it("storing location info", function(){ + describe('ProgramNode', function(){ + it('storing location info', function(){ var pn = new handlebarsEnv.AST.ProgramNode([], {}, LOCATION_INFO); testLocationInfoStorage(pn); }); @@ -223,33 +222,181 @@ describe('ast', function() { }); it('gets line numbers correct when newlines appear', function(){ - var secondContentNode = statements[2]; - testColumns(secondContentNode, 1, 2, 21, 8); + testColumns(statements[2], 1, 2, 21, 0); + testColumns(statements[3], 2, 2, 0, 8); }); it('gets MustacheNode line numbers correct across newlines', function(){ - var secondMustacheNode = statements[3]; + var secondMustacheNode = statements[4]; testColumns(secondMustacheNode, 2, 2, 8, 22); }); it('gets the block helper information correct', function(){ - var blockHelperNode = statements[5]; + var blockHelperNode = statements[7]; testColumns(blockHelperNode, 3, 7, 8, 23); }); it('correctly records the line numbers the program of a block helper', function(){ - var blockHelperNode = statements[5], + var blockHelperNode = statements[7], program = blockHelperNode.program; testColumns(program, 3, 5, 8, 5); }); it('correctly records the line numbers of an inverse of a block helper', function(){ - var blockHelperNode = statements[5], + var blockHelperNode = statements[7], inverse = blockHelperNode.inverse; testColumns(inverse, 5, 7, 5, 0); }); }); + + describe('standalone flags', function(){ + describe('mustache', function() { + it('does not mark mustaches as standalone', function() { + var ast = Handlebars.parse(' {{comment}} '); + equals(ast.statements[0].omit, undefined); + equals(ast.statements[2].omit, undefined); + }); + }); + describe('blocks', function() { + it('marks block mustaches as standalone', function() { + var ast = Handlebars.parse(' {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '), + block = ast.statements[1]; + + equals(ast.statements[0].omit, true); + + equals(block.program.statements[0].omit, true); + equals(block.program.statements[1].string, 'foo\n'); + equals(block.program.statements[2].omit, true); + + equals(block.inverse.statements[0].omit, true); + equals(block.inverse.statements[1].string, ' bar \n'); + equals(block.inverse.statements[2].omit, true); + + equals(ast.statements[2].omit, true); + }); + it('marks initial block mustaches as standalone', function() { + var ast = Handlebars.parse('{{# comment}} \nfoo\n {{/comment}}'), + block = ast.statements[0]; + + equals(block.program.statements[0].omit, true); + equals(block.program.statements[1].string, 'foo\n'); + equals(block.program.statements[2].omit, true); + }); + it('marks mustaches with children as standalone', function() { + var ast = Handlebars.parse('{{# comment}} \n{{foo}}\n {{/comment}}'), + block = ast.statements[0]; + + equals(block.program.statements[0].omit, true); + equals(block.program.statements[1].id.original, 'foo'); + equals(block.program.statements[2].omit, undefined); + equals(block.program.statements[3].omit, true); + }); + it('marks nested block mustaches as standalone', function() { + var ast = Handlebars.parse('{{#foo}} \n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} \n{{/foo}}'), + statements = ast.statements[0].program.statements, + block = statements[1]; + + equals(statements[0].omit, true); + + equals(block.program.statements[0].omit, true); + equals(block.program.statements[1].string, 'foo\n'); + equals(block.program.statements[2].omit, true); + + equals(block.inverse.statements[0].omit, true); + equals(block.inverse.statements[1].string, ' bar \n'); + equals(block.inverse.statements[2].omit, true); + + equals(statements[0].omit, true); + }); + it('does not mark nested block mustaches as standalone', function() { + var ast = Handlebars.parse('{{#foo}} {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} {{/foo}}'), + statements = ast.statements[0].program.statements, + block = statements[1]; + + equals(statements[0].omit, undefined); + + equals(block.program.statements[0].omit, undefined); + equals(block.program.statements[1].string, 'foo\n'); + equals(block.program.statements[2].omit, true); + + equals(block.inverse.statements[0].omit, true); + equals(block.inverse.statements[1].string, ' bar \n'); + equals(block.inverse.statements[2].omit, undefined); + + equals(statements[0].omit, undefined); + }); + it('does not mark nested initial block mustaches as standalone', function() { + var ast = Handlebars.parse('{{#foo}}{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}}{{/foo}}'), + statements = ast.statements[0].program.statements, + block = statements[0]; + + equals(block.program.statements[0].omit, undefined); + equals(block.program.statements[1].string, 'foo\n'); + equals(block.program.statements[2].omit, true); + + equals(block.inverse.statements[0].omit, true); + equals(block.inverse.statements[1].string, ' bar \n'); + equals(block.inverse.statements[2].omit, undefined); + + equals(statements[0].omit, undefined); + }); + + it('marks column 0 block mustaches as standalone', function() { + var ast = Handlebars.parse('test\n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '), + block = ast.statements[1]; + + equals(ast.statements[0].omit, undefined); + + equals(block.program.statements[0].omit, true); + equals(block.program.statements[1].string, 'foo\n'); + equals(block.program.statements[2].omit, true); + + equals(block.inverse.statements[0].omit, true); + equals(block.inverse.statements[1].string, ' bar \n'); + equals(block.inverse.statements[2].omit, true); + + equals(ast.statements[2].omit, true); + }); + }); + describe('partials', function() { + it('marks partial as standalone', function() { + var ast = Handlebars.parse('{{> partial }} '); + equals(ast.statements[1].omit, true); + }); + it('marks indented partial as standalone', function() { + var ast = Handlebars.parse(' {{> partial }} '); + equals(ast.statements[0].omit, true); + equals(ast.statements[1].indent, ' '); + equals(ast.statements[2].omit, true); + }); + it('marks those around content as not standalone', function() { + var ast = Handlebars.parse('a{{> partial }}'); + equals(ast.statements[0].omit, undefined); + + ast = Handlebars.parse('{{> partial }}a'); + equals(ast.statements[1].omit, undefined); + }); + }); + describe('comments', function() { + it('marks comment as standalone', function() { + var ast = Handlebars.parse('{{! comment }} '); + equals(ast.statements[1].omit, true); + }); + it('marks indented comment as standalone', function() { + var ast = Handlebars.parse(' {{! comment }} '); + equals(ast.statements[0].omit, true); + equals(ast.statements[2].omit, true); + }); + it('marks those around content as not standalone', function() { + var ast = Handlebars.parse('a{{! comment }}'); + equals(ast.statements[0].omit, undefined); + + ast = Handlebars.parse('{{! comment }}a'); + equals(ast.statements[1].omit, undefined); + }); + }); + }); }); diff --git a/spec/blocks.js b/spec/blocks.js index 8f7c242..9a5cb40 100644 --- a/spec/blocks.js +++ b/spec/blocks.js @@ -1,4 +1,4 @@ -/*global CompilerContext, shouldCompileTo */ +/*global CompilerContext, shouldCompileTo, shouldThrow */ describe('blocks', function() { it("array", function() { var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!"; @@ -8,7 +8,12 @@ describe('blocks', function() { shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", "Arrays ignore the contents when empty"); + }); + it('array without data', function() { + var string = '{{#goodbyes}}{{text}}{{/goodbyes}} {{#goodbyes}}{{text}}{{/goodbyes}}'; + var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'}; + shouldCompileTo(string, [hash,,,,false], 'goodbyeGoodbyeGOODBYE goodbyeGoodbyeGOODBYE'); }); it("array with @index", function() { @@ -39,6 +44,13 @@ describe('blocks', function() { "Templates can access variables in contexts up the stack with relative path syntax"); }); + it('multiple blocks with complex lookup', function() { + var string = '{{#goodbyes}}{{../name}}{{../name}}{{/goodbyes}}'; + var hash = {name: 'Alan', goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}]}; + + shouldCompileTo(string, hash, 'AlanAlanAlanAlanAlanAlan'); + }); + it("block with complex lookup using nested context", function() { var string = "{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}"; @@ -48,10 +60,10 @@ describe('blocks', function() { }); it("block with deep nested complex lookup", function() { - var string = "{{#outer}}Goodbye {{#inner}}cruel {{../../omg}}{{/inner}}{{/outer}}"; - var hash = {omg: "OMG!", outer: [{ inner: [{ text: "goodbye" }] }] }; + var string = "{{#outer}}Goodbye {{#inner}}cruel {{../sibling}} {{../../omg}}{{/inner}}{{/outer}}"; + var hash = {omg: "OMG!", outer: [{ sibling: 'sad', inner: [{ text: "goodbye" }] }] }; - shouldCompileTo(string, hash, "Goodbye cruel OMG!"); + shouldCompileTo(string, hash, "Goodbye cruel sad OMG!"); }); describe('inverted sections', function() { @@ -83,4 +95,36 @@ describe('blocks', function() { "No people"); }); }); + + describe('standalone sections', function() { + it('block standalone else sections', function() { + shouldCompileTo('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n', {none: 'No people'}, + 'No people\n'); + shouldCompileTo('{{#none}}\n{{.}}\n{{^}}\n{{none}}\n{{/none}}\n', {none: 'No people'}, + 'No people\n'); + shouldCompileTo('\n{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n', {none: 'No people'}, + 'No people\n'); + }); + }); + + describe('compat mode', function() { + it("block with deep recursive lookup lookup", function() { + var string = "{{#outer}}Goodbye {{#inner}}cruel {{omg}}{{/inner}}{{/outer}}"; + var hash = {omg: "OMG!", outer: [{ inner: [{ text: "goodbye" }] }] }; + + shouldCompileTo(string, [hash, undefined, undefined, true], "Goodbye cruel OMG!"); + }); + it("block with deep recursive pathed lookup", function() { + var string = "{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}"; + var hash = {omg: {yes: "OMG!"}, outer: [{ inner: [{ yes: 'no', text: "goodbye" }] }] }; + + shouldCompileTo(string, [hash, undefined, undefined, true], "Goodbye cruel OMG!"); + }); + it("block with missed recursive lookup", function() { + var string = "{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}"; + var hash = {omg: {no: "OMG!"}, outer: [{ inner: [{ yes: 'no', text: "goodbye" }] }] }; + + shouldCompileTo(string, [hash, undefined, undefined, true], "Goodbye cruel "); + }); + }); }); diff --git a/spec/builtins.js b/spec/builtins.js index a28f400..2cd6bac 100644 --- a/spec/builtins.js +++ b/spec/builtins.js @@ -44,6 +44,10 @@ describe('builtin helpers', function() { var string = "{{#with person}}{{first}} {{last}}{{/with}}"; shouldCompileTo(string, {person: function() { return {first: "Alan", last: "Johnson"};}}, "Alan Johnson"); }); + it("with with else", function() { + var string = "{{#with person}}Person is present{{else}}Person is not present{{/with}}"; + shouldCompileTo(string, {}, "Person is not present"); + }); }); describe('#each', function() { @@ -62,9 +66,29 @@ describe('builtin helpers', function() { "each with array argument ignores the contents when empty"); }); + it('each without data', function() { + var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!'; + var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'}; + shouldCompileTo(string, [hash,,,,false], 'goodbye! Goodbye! GOODBYE! cruel world!'); + + hash = {goodbyes: 'cruel', world: 'world'}; + shouldCompileTo('{{#each .}}{{.}}{{/each}}', [hash,,,,false], 'cruelworld'); + }); + + it('each without context', function() { + var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!'; + shouldCompileTo(string, [,,,,], 'cruel !'); + }); + it("each with an object and @key", function() { var string = "{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!"; - var hash = {goodbyes: {"<b>#1</b>": {text: "goodbye"}, 2: {text: "GOODBYE"}}, world: "world"}; + + function Clazz() { + this['<b>#1</b>'] = {text: 'goodbye'}; + this[2] = {text: 'GOODBYE'}; + } + Clazz.prototype.foo = 'fail'; + var hash = {goodbyes: new Clazz(), world: 'world'}; // Object property iteration order is undefined according to ECMA spec, // so we need to check both possible orders @@ -189,19 +213,81 @@ describe('builtin helpers', function() { }); }); - it("#log", function() { - var string = "{{log blah}}"; - var hash = { blah: "whee" }; + describe("#log", function() { + if (typeof console === 'undefined') { + return; + } + + var info, + error; + beforeEach(function() { + info = console.info; + error = console.error; + }); + afterEach(function() { + console.info = info; + console.error = error; + }); - var levelArg, logArg; - handlebarsEnv.log = function(level, arg){ - levelArg = level; - logArg = arg; - }; + it('should call logger at default level', function() { + var string = "{{log blah}}"; + var hash = { blah: "whee" }; - shouldCompileTo(string, hash, "", "log should not display"); - equals(1, levelArg, "should call log with 1"); - equals("whee", logArg, "should call log with 'whee'"); + var levelArg, logArg; + handlebarsEnv.log = function(level, arg){ + levelArg = level; + logArg = arg; + }; + + shouldCompileTo(string, hash, "", "log should not display"); + equals(1, levelArg, "should call log with 1"); + equals("whee", logArg, "should call log with 'whee'"); + }); + it('should call logger at data level', function() { + var string = "{{log blah}}"; + var hash = { blah: "whee" }; + + var levelArg, logArg; + handlebarsEnv.log = function(level, arg){ + levelArg = level; + logArg = arg; + }; + + shouldCompileTo(string, [hash,,,,{level: '03'}], ""); + equals(3, levelArg); + equals("whee", logArg); + }); + it('should not output to console', function() { + var string = "{{log blah}}"; + var hash = { blah: "whee" }; + + console.info = function() { + throw new Error(); + }; + + shouldCompileTo(string, hash, "", "log should not display"); + }); + it('should log at data level', function() { + var string = "{{log blah}}"; + var hash = { blah: "whee" }; + var called; + + console.error = function(log) { + equals("whee", log); + called = true; + }; + + shouldCompileTo(string, [hash,,,,{level: '03'}], ""); + equals(true, called); + }); + it('should handle missing logger', function() { + var string = "{{log blah}}"; + var hash = { blah: "whee" }; + + console.error = undefined; + + shouldCompileTo(string, [hash,,,,{level: '03'}], ""); + }); }); diff --git a/spec/compiler.js b/spec/compiler.js new file mode 100644 index 0000000..250dbc7 --- /dev/null +++ b/spec/compiler.js @@ -0,0 +1,70 @@ +/*global Handlebars, shouldThrow */ + +describe('compiler', function() { + if (!Handlebars.compile) { + return; + } + + describe('#equals', function() { + function compile(string) { + var ast = Handlebars.parse(string); + return new Handlebars.Compiler().compile(ast, {}); + } + + it('should treat as equal', function() { + equal(compile('foo').equals(compile('foo')), true); + equal(compile('{{foo}}').equals(compile('{{foo}}')), true); + equal(compile('{{foo.bar}}').equals(compile('{{foo.bar}}')), true); + equal(compile('{{foo.bar baz "foo" true false bat=1}}').equals(compile('{{foo.bar baz "foo" true false bat=1}}')), true); + equal(compile('{{foo.bar (baz bat=1)}}').equals(compile('{{foo.bar (baz bat=1)}}')), true); + equal(compile('{{#foo}} {{/foo}}').equals(compile('{{#foo}} {{/foo}}')), true); + }); + it('should treat as not equal', function() { + equal(compile('foo').equals(compile('bar')), false); + equal(compile('{{foo}}').equals(compile('{{bar}}')), false); + equal(compile('{{foo.bar}}').equals(compile('{{bar.bar}}')), false); + equal(compile('{{foo.bar baz bat=1}}').equals(compile('{{foo.bar bar bat=1}}')), false); + equal(compile('{{foo.bar (baz bat=1)}}').equals(compile('{{foo.bar (bar bat=1)}}')), false); + equal(compile('{{#foo}} {{/foo}}').equals(compile('{{#bar}} {{/bar}}')), false); + equal(compile('{{#foo}} {{/foo}}').equals(compile('{{#foo}} {{foo}}{{/foo}}')), false); + }); + }); + + describe('#compile', function() { + it('should fail with invalid input', function() { + shouldThrow(function() { + Handlebars.compile(null); + }, Error, 'You must pass a string or Handlebars AST to Handlebars.compile. You passed null'); + shouldThrow(function() { + Handlebars.compile({}); + }, Error, 'You must pass a string or Handlebars AST to Handlebars.compile. You passed [object Object]'); + }); + + it('can utilize AST instance', function() { + equal(Handlebars.compile(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")], {}))(), 'Hello'); + }); + + it("can pass through an empty string", function() { + equal(Handlebars.compile('')(), ''); + }); + }); + + describe('#precompile', function() { + it('should fail with invalid input', function() { + shouldThrow(function() { + Handlebars.precompile(null); + }, Error, 'You must pass a string or Handlebars AST to Handlebars.precompile. You passed null'); + shouldThrow(function() { + Handlebars.precompile({}); + }, Error, 'You must pass a string or Handlebars AST to Handlebars.precompile. You passed [object Object]'); + }); + + it('can utilize AST instance', function() { + equal(/return "Hello"/.test(Handlebars.precompile(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")]), {})), true); + }); + + it("can pass through an empty string", function() { + equal(/return ""/.test(Handlebars.precompile('')), true); + }); + }); +}); diff --git a/spec/env/common.js b/spec/env/common.js index 53bf977..92cc611 100644 --- a/spec/env/common.js +++ b/spec/env/common.js @@ -1,3 +1,4 @@ +/*global CompilerContext, compileWithPartials, shouldCompileToWithPartials */ global.shouldCompileTo = function(string, hashOrArray, expected, message) { shouldCompileToWithPartials(string, hashOrArray, false, expected, message); }; @@ -5,27 +6,35 @@ global.shouldCompileTo = function(string, hashOrArray, expected, message) { global.shouldCompileToWithPartials = function(string, hashOrArray, partials, expected, message) { var result = compileWithPartials(string, hashOrArray, partials); if (result !== expected) { - throw new Error("'" + expected + "' should === '" + result + "': " + message); + throw new Error("'" + result + "' should === '" + expected + "': " + message); } }; global.compileWithPartials = function(string, hashOrArray, partials) { - var template = CompilerContext[partials ? 'compileWithPartial' : 'compile'](string), ary; + var template, + ary, + options; if(Object.prototype.toString.call(hashOrArray) === "[object Array]") { ary = []; ary.push(hashOrArray[0]); ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] }); + options = {compat: hashOrArray[3]}; + if (hashOrArray[4] != null) { + options.data = !!hashOrArray[4]; + ary[1].data = hashOrArray[4]; + } } else { ary = [hashOrArray]; } + template = CompilerContext[partials ? 'compileWithPartial' : 'compile'](string, options); return template.apply(this, ary); }; global.equals = global.equal = function(a, b, msg) { if (a !== b) { - throw new Error("'" + b + "' should === '" + a + "'" + (msg ? ": " + msg : '')); + throw new Error("'" + a + "' should === '" + b + "'" + (msg ? ": " + msg : '')); } }; @@ -39,7 +48,7 @@ global.shouldThrow = function(callback, type, msg) { throw new Error('Type failure'); } if (msg && !(msg.test ? msg.test(err.message) : msg === err.message)) { - throw new Error('Message failure'); + equal(msg, err.message); } } if (failed) { diff --git a/spec/helpers.js b/spec/helpers.js index d686b64..e3b5863 100644 --- a/spec/helpers.js +++ b/spec/helpers.js @@ -208,20 +208,41 @@ describe('helpers', function() { }); }); - it("Multiple global helper registration", function() { - var helpers = handlebarsEnv.helpers; - handlebarsEnv.helpers = {}; + describe('registration', function() { + it('unregisters', function() { + var helpers = handlebarsEnv.helpers; + handlebarsEnv.helpers = {}; - handlebarsEnv.registerHelper({ - 'if': helpers['if'], - world: function() { return "world!"; }, - test_helper: function() { return 'found it!'; } + handlebarsEnv.registerHelper('foo', function() { + return 'fail'; + }); + handlebarsEnv.unregisterHelper('foo'); + equals(handlebarsEnv.helpers.foo, undefined); }); - shouldCompileTo( - "{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}", - [{cruel: "cruel"}], - "found it! Goodbye cruel world!!"); + it('allows multiple globals', function() { + var helpers = handlebarsEnv.helpers; + handlebarsEnv.helpers = {}; + + handlebarsEnv.registerHelper({ + 'if': helpers['if'], + world: function() { return "world!"; }, + test_helper: function() { return 'found it!'; } + }); + + shouldCompileTo( + "{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}", + [{cruel: "cruel"}], + "found it! Goodbye cruel world!!"); + }); + it('fails with multiple and args', function() { + shouldThrow(function() { + handlebarsEnv.registerHelper({ + world: function() { return "world!"; }, + test_helper: function() { return 'found it!'; } + }, {}); + }, Error, 'Arg not supported with multiple helpers'); + }); }); it("decimal number literals work", function() { diff --git a/spec/mustache b/spec/mustache new file mode 160000 +Subproject 72233f3ffda9e33915fd3022d0a9ebbcce265ac diff --git a/spec/parser.js b/spec/parser.js index 34f6a1d..131160a 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -1,4 +1,4 @@ -/*global Handlebars */ +/*global Handlebars, shouldThrow */ describe('parser', function() { if (!Handlebars.print) { return; @@ -147,6 +147,9 @@ describe('parser', function() { it('parses a standalone inverse section', function() { equals(ast_for("{{^foo}}bar{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n {{^}}\n CONTENT[ 'bar' ]\n"); }); + it('parses a standalone inverse section', function() { + equals(ast_for("{{else foo}}bar{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n {{^}}\n CONTENT[ 'bar' ]\n"); + }); it("raises if there's a Parse error", function() { shouldThrow(function() { diff --git a/spec/partials.js b/spec/partials.js index 732436a..20187f8 100644 --- a/spec/partials.js +++ b/spec/partials.js @@ -1,11 +1,11 @@ -/*global CompilerContext, shouldCompileTo, shouldCompileToWithPartials */ +/*global CompilerContext, Handlebars, handlebarsEnv, shouldCompileTo, shouldCompileToWithPartials, shouldThrow */ describe('partials', function() { - it("basic partials", function() { - var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}"; - var partial = "{{name}} ({{url}}) "; - var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; - shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Yehuda (http://yehuda) Alan (http://alan) ", - "Basic partials output based on current context."); + it('basic partials', function() { + var string = 'Dudes: {{#dudes}}{{> dude}}{{/dudes}}'; + var partial = '{{name}} ({{url}}) '; + var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]}; + shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) '); + shouldCompileToWithPartials(string, [hash, {}, {dude: partial},,false], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) '); }); it("partials with context", function() { @@ -91,6 +91,9 @@ describe('partials', function() { var dude = "{{name}}"; var hash = {name:"Jeepers", another_dude:"Creepers"}; shouldCompileToWithPartials(string, [hash, {}, {'shared/dude':dude}], true, "Dudes: Jeepers Creepers", "Partials can use globals or passed"); + + handlebarsEnv.unregisterPartial('global_test'); + equals(handlebarsEnv.partials.global_test, undefined); }); it("Multiple partial registration", function() { @@ -136,5 +139,54 @@ describe('partials', function() { var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}"; var partial = ""; var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; - shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: "); }); + shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: "); + }); + + it("throw on missing partial", function() { + var compile = handlebarsEnv.compile; + handlebarsEnv.compile = undefined; + shouldThrow(function() { + shouldCompileTo('{{> dude}}', [{}, {}, {dude: 'fail'}], ''); + }, Error, /The partial dude could not be compiled/); + handlebarsEnv.compile = compile; + }); + + describe('standalone partials', function() { + it("indented partials", function() { + var string = "Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}"; + var dude = "{{name}}\n"; + var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; + shouldCompileToWithPartials(string, [hash, {}, {dude: dude}], true, + "Dudes:\n Yehuda\n Alan\n"); + }); + it("nested indented partials", function() { + var string = "Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}"; + var dude = "{{name}}\n {{> url}}"; + var url = "{{url}}!\n"; + var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; + shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}], true, + "Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n"); + }); + }); + + describe('compat mode', function() { + it('partials can access parents', function() { + var string = 'Dudes: {{#dudes}}{{> dude}}{{/dudes}}'; + var partial = '{{name}} ({{url}}) {{root}} '; + var hash = {root: 'yes', dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]}; + shouldCompileToWithPartials(string, [hash, {}, {dude: partial}, true], true, 'Dudes: Yehuda (http://yehuda) yes Alan (http://alan) yes '); + }); + it('partials can access parents without data', function() { + var string = 'Dudes: {{#dudes}}{{> dude}}{{/dudes}}'; + var partial = '{{name}} ({{url}}) {{root}} '; + var hash = {root: 'yes', dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]}; + shouldCompileToWithPartials(string, [hash, {}, {dude: partial}, true, false], true, 'Dudes: Yehuda (http://yehuda) yes Alan (http://alan) yes '); + }); + it('partials inherit compat', function() { + var string = 'Dudes: {{> dude}}'; + var partial = '{{#dudes}}{{name}} ({{url}}) {{root}} {{/dudes}}'; + var hash = {root: 'yes', dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]}; + shouldCompileToWithPartials(string, [hash, {}, {dude: partial}, true], true, 'Dudes: Yehuda (http://yehuda) yes Alan (http://alan) yes '); + }); + }); }); diff --git a/spec/precompiler.js b/spec/precompiler.js index 7cd1ffd..57fc280 100644 --- a/spec/precompiler.js +++ b/spec/precompiler.js @@ -10,9 +10,12 @@ describe('precompiler', function() { Precompiler = require('../lib/precompiler'); var log, - logFunction; + logFunction, + + precompile; beforeEach(function() { + precompile = Handlebars.precompile; logFunction = console.log; log = ''; console.log = function() { @@ -20,6 +23,7 @@ describe('precompiler', function() { }; }); afterEach(function() { + Handlebars.precompile = precompile; console.log = logFunction; }); @@ -37,4 +41,43 @@ describe('precompiler', function() { Precompiler.cli({templates: ['foo']}); }, Handlebars.Exception, 'Unable to open template file "foo"'); }); + it('should throw when combining simple and minimized', function() { + shouldThrow(function() { + Precompiler.cli({templates: [__dirname], simple: true, min: true}); + }, Handlebars.Exception, 'Unable to minimze simple output'); + }); + it('should throw when combining simple and multiple templates', function() { + shouldThrow(function() { + Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars', __dirname + '/artifacts/empty.handlebars'], simple: true}); + }, Handlebars.Exception, 'Unable to output multiple templates in simple mode'); + }); + it('should throw when combining simple and directories', function() { + shouldThrow(function() { + Precompiler.cli({templates: [__dirname], simple: true}); + }, Handlebars.Exception, 'Unable to output multiple templates in simple mode'); + }); + it('should enumerate directories by extension', function() { + Precompiler.cli({templates: [__dirname + '/artifacts'], extension: 'hbs'}); + equal(/'example_2'/.test(log), true); + log = ''; + + Precompiler.cli({templates: [__dirname + '/artifacts'], extension: 'handlebars'}); + equal(/'empty'/.test(log), true); + equal(/'example_1'/.test(log), true); + }); + it('should output simple templates', function() { + Handlebars.precompile = function() { return 'simple'; }; + Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars'}); + equal(log, 'simple\n'); + }); + it('should output amd templates', function() { + Handlebars.precompile = function() { return 'amd'; }; + Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], amd: true, extension: 'handlebars'}); + equal(/template\(amd\)/.test(log), true); + }); + it('should output commonjs templates', function() { + Handlebars.precompile = function() { return 'commonjs'; }; + Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], commonjs: true, extension: 'handlebars'}); + equal(/template\(commonjs\)/.test(log), true); + }); }); diff --git a/spec/regressions.js b/spec/regressions.js index c633a21..11207fc 100644 --- a/spec/regressions.js +++ b/spec/regressions.js @@ -24,7 +24,19 @@ describe('Regressions', function() { }); it("bug reported by @fat where lambdas weren't being properly resolved", function() { - var string = "<strong>This is a slightly more complicated {{thing}}.</strong>.\n{{! Just ignore this business. }}\nCheck this out:\n{{#hasThings}}\n<ul>\n{{#things}}\n<li class={{className}}>{{word}}</li>\n{{/things}}</ul>.\n{{/hasThings}}\n{{^hasThings}}\n\n<small>Nothing to check out...</small>\n{{/hasThings}}"; + var string = '<strong>This is a slightly more complicated {{thing}}.</strong>.\n' + + '{{! Just ignore this business. }}\n' + + 'Check this out:\n' + + '{{#hasThings}}\n' + + '<ul>\n' + + '{{#things}}\n' + + '<li class={{className}}>{{word}}</li>\n' + + '{{/things}}</ul>.\n' + + '{{/hasThings}}\n' + + '{{^hasThings}}\n' + + '\n' + + '<small>Nothing to check out...</small>\n' + + '{{/hasThings}}'; var data = { thing: function() { return "blah"; @@ -39,7 +51,13 @@ describe('Regressions', function() { } }; - var output = "<strong>This is a slightly more complicated blah.</strong>.\n\nCheck this out:\n\n<ul>\n\n<li class=one>@fat</li>\n\n<li class=two>@dhg</li>\n\n<li class=three>@sayrer</li>\n</ul>.\n\n"; + var output = '<strong>This is a slightly more complicated blah.</strong>.\n' + + 'Check this out:\n' + + '<ul>\n' + + '<li class=one>@fat</li>\n' + + '<li class=two>@dhg</li>\n' + + '<li class=three>@sayrer</li>\n' + + '</ul>.\n'; shouldCompileTo(string, data, output); }); @@ -112,12 +130,6 @@ describe('Regressions', function() { shouldCompileTo(string, data, "Hello Chris. You have just won $10000! Well, $6000, after taxes.", "the hello world mustache example works"); }); - it("Passing falsy values to Handlebars.compile throws an error", function() { - shouldThrow(function() { - CompilerContext.compile(null); - }, Error, 'You must pass a string or Handlebars AST to Handlebars.precompile. You passed null'); - }); - it('GH-731: zero context rendering', function() { shouldCompileTo('{{#foo}} This is {{bar}} ~ {{/foo}}', {foo: 0, bar: 'OK'}, ' This is ~ '); }); @@ -126,13 +138,11 @@ describe('Regressions', function() { shouldCompileTo('{{foo.bar}}', {foo: 0}, ''); }); - if (Handlebars.AST) { - it("can pass through an already-compiled AST via compile/precompile", function() { - equal(Handlebars.compile(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")]))(), 'Hello'); - }); + it('GH-837: undefined values for helpers', function() { + var helpers = { + str: function(value) { return value + ''; } + }; - it("can pass through an empty string", function() { - equal(Handlebars.compile('')(), ''); - }); - } + shouldCompileTo('{{str bar.baz}}', [{}, helpers], 'undefined'); + }); }); diff --git a/spec/runtime.js b/spec/runtime.js new file mode 100644 index 0000000..4431a68 --- /dev/null +++ b/spec/runtime.js @@ -0,0 +1,36 @@ +/*globals Handlebars, shouldThrow */ + +describe('runtime', function() { + describe('#template', function() { + it('should throw on invalid templates', function() { + shouldThrow(function() { + Handlebars.template({}); + }, Error, 'Unknown template object: object'); + shouldThrow(function() { + Handlebars.template(); + }, Error, 'Unknown template object: undefined'); + shouldThrow(function() { + Handlebars.template(''); + }, Error, 'Unknown template object: string'); + }); + it('should throw on version mismatch', function() { + shouldThrow(function() { + Handlebars.template({ + main: true, + compiler: [Handlebars.COMPILER_REVISION + 1] + }); + }, Error, /Template was precompiled with a newer version of Handlebars than the current runtime/); + shouldThrow(function() { + Handlebars.template({ + main: true, + compiler: [Handlebars.COMPILER_REVISION - 1] + }); + }, Error, /Template was precompiled with an older version of Handlebars than the current runtime/); + shouldThrow(function() { + Handlebars.template({ + main: true + }); + }, Error, /Template was precompiled with an older version of Handlebars than the current runtime/); + }); + }); +}); diff --git a/spec/spec.js b/spec/spec.js new file mode 100644 index 0000000..2dd2bd8 --- /dev/null +++ b/spec/spec.js @@ -0,0 +1,49 @@ +describe('spec', function() { + // NOP Under non-node environments + if (typeof process === 'undefined') { + return; + } + + var _ = require('underscore'), + Handlebars = require('../lib'), + fs = require('fs'); + + var specDir =__dirname + '/mustache/specs/'; + var specs = _.filter(fs.readdirSync(specDir), function(name) { + return /.*\.json$/.test(name); + }); + + _.each(specs, function(name) { + var spec = require(specDir + name); + _.each(spec.tests, function(test) { + // Our lambda implementation knowingly deviates from the optional Mustace lambda spec + // We also do not support alternative delimeters + if (name === '~lambdas.json' + + // We also choose to throw if paritals are not found + || (name === 'partials.json' && test.name === 'Failed Lookup') + + // We nest the entire response from partials, not just the literals + || (name === 'partials.json' && test.name === 'Standalone Indentation') + + || /\{\{\=/.test(test.template) + || _.any(test.partials, function(partial) { return /\{\{\=/.test(partial); })) { + it.skip(name + ' - ' + test.name); + return; + } + + var data = _.clone(test.data); + if (data.lambda) { + // Blergh + data.lambda = eval('(' + data.lambda.js + ')'); + } + it(name + ' - ' + test.name, function() { + if (test.partials) { + shouldCompileToWithPartials(test.template, [data, {}, test.partials, true], true, test.expected, test.desc + ' "' + test.template + '"'); + } else { + shouldCompileTo(test.template, [data, {}, {}, true], test.expected, test.desc + ' "' + test.template + '"'); + } + }); + }); + }); +}); diff --git a/spec/subexpressions.js b/spec/subexpressions.js index 0ecdbb9..5c9fdfc 100644 --- a/spec/subexpressions.js +++ b/spec/subexpressions.js @@ -139,6 +139,30 @@ describe('subexpressions', function() { shouldCompileTo(string, [{}, helpers], '<input aria-label="Name" placeholder="Example User" />'); }); + it("multiple subexpressions in a hash with context", function() { + var string = '{{input aria-label=(t item.field) placeholder=(t item.placeholder)}}'; + + var context = { + item: { + field: "Name", + placeholder: "Example User" + } + }; + + var helpers = { + input: function(options) { + var hash = options.hash; + var ariaLabel = Handlebars.Utils.escapeExpression(hash['aria-label']); + var placeholder = Handlebars.Utils.escapeExpression(hash.placeholder); + return new Handlebars.SafeString('<input aria-label="' + ariaLabel + '" placeholder="' + placeholder + '" />'); + }, + t: function(defaultString) { + return new Handlebars.SafeString(defaultString); + } + } + shouldCompileTo(string, [context, helpers], '<input aria-label="Name" placeholder="Example User" />'); + }); + it("in string params mode,", function() { var template = CompilerContext.compile('{{snog (blorg foo x=y) yeah a=b}}', {stringParams: true}); diff --git a/spec/utils.js b/spec/utils.js index 390ad05..ea7d782 100644 --- a/spec/utils.js +++ b/spec/utils.js @@ -56,4 +56,20 @@ describe('utils', function() { equals(Handlebars.Utils.isEmpty({bar: 1}), false); }); }); + + describe('#extend', function() { + it('should ignore prototype values', function() { + function A() { + this.a = 1; + } + A.prototype.b = 4; + + var b = {b: 2}; + + Handlebars.Utils.extend(b, new A()); + + equals(b.a, 1); + equals(b.b, 2); + }); + }); }); diff --git a/src/handlebars.l b/src/handlebars.l index 006f2c7..f775cc4 100644 --- a/src/handlebars.l +++ b/src/handlebars.l @@ -28,7 +28,7 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD} %% -[^\x00]*?/("{{") { +[^\x00\n]*?\n?/("{{") { if(yytext.slice(-2) === "\\\\") { strip(0,1); this.begin("mu"); @@ -41,7 +41,7 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD} if(yytext) return 'CONTENT'; } -[^\x00]+ return 'CONTENT'; +([^\x00\n]+\n?|\n) return 'CONTENT'; // marks CONTENT up to the next mustache or escaped mustache <emu>[^\x00]{2,}?/("{{"|"\\{{"|"\\\\{{"|<<EOF>>) { @@ -67,11 +67,6 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD} this.begin('raw'); return 'CLOSE_RAW_BLOCK'; } -<mu>"{{{{"[^\x00]*"}}}}" { - yytext = yytext.substr(4, yyleng-8); - this.popState(); - return 'RAW_BLOCK'; - } <mu>"{{"{LEFT_STRIP}?">" return 'OPEN_PARTIAL'; <mu>"{{"{LEFT_STRIP}?"#" return 'OPEN_BLOCK'; <mu>"{{"{LEFT_STRIP}?"/" return 'OPEN_ENDBLOCK'; diff --git a/src/handlebars.yy b/src/handlebars.yy index 112c1ad..a8d288f 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -5,11 +5,11 @@ %% root - : program EOF { return $1; } + : program EOF { yy.prepareProgram($1.statements, true); return $1; } ; program - : statement* -> new yy.ProgramNode($1, {}, @$) + : statement* -> new yy.ProgramNode(yy.prepareProgram($1), {}, @$) ; statement @@ -62,10 +62,6 @@ partial | OPEN_PARTIAL partialName hash? CLOSE -> new yy.PartialNode($2, undefined, $3, yy.stripFlags($1, $4), @$) ; -simpleInverse - : INVERSE -> yy.stripFlags($1, $1) - ; - sexpr : path param* hash? -> new yy.SexprNode([$1].concat($2), $3, @$) | dataName -> new yy.SexprNode([$1], null, @$) diff --git a/tasks/test.js b/tasks/test.js index 664af60..ad8a911 100644 --- a/tasks/test.js +++ b/tasks/test.js @@ -29,5 +29,16 @@ module.exports = function(grunt) { done(); }); }); - grunt.registerTask('test', ['test:bin', 'test:mocha']); + grunt.registerTask('test:cov', function() { + var done = this.async(); + + var runner = childProcess.fork('node_modules/.bin/istanbul', ['cover', '--', './spec/env/runner.js'], {stdio: 'inherit'}); + runner.on('close', function(code) { + if (code != 0) { + grunt.fatal(code + ' tests failed'); + } + done(); + }); + }); + grunt.registerTask('test', ['test:bin', 'test:cov']); }; |