summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.gitmodules3
-rw-r--r--.istanbul.yml2
-rw-r--r--FAQ.md3
-rw-r--r--Gruntfile.js4
-rw-r--r--README.markdown15
-rw-r--r--bench/throughput.js8
-rw-r--r--lib/handlebars/base.js14
-rw-r--r--lib/handlebars/compiler/ast.js7
-rw-r--r--lib/handlebars/compiler/base.js13
-rw-r--r--lib/handlebars/compiler/compiler.js73
-rw-r--r--lib/handlebars/compiler/helpers.js140
-rw-r--r--lib/handlebars/compiler/javascript-compiler.js192
-rw-r--r--lib/handlebars/runtime.js84
-rw-r--r--lib/handlebars/utils.js4
-rw-r--r--lib/index.js1
-rw-r--r--package.json7
-rw-r--r--spec/ast.js167
-rw-r--r--spec/blocks.js52
-rw-r--r--spec/builtins.js110
-rw-r--r--spec/compiler.js70
-rw-r--r--spec/env/common.js17
-rw-r--r--spec/helpers.js43
m---------spec/mustache0
-rw-r--r--spec/parser.js5
-rw-r--r--spec/partials.js68
-rw-r--r--spec/precompiler.js45
-rw-r--r--spec/regressions.js42
-rw-r--r--spec/runtime.js36
-rw-r--r--spec/spec.js49
-rw-r--r--spec/subexpressions.js24
-rw-r--r--spec/utils.js16
-rw-r--r--src/handlebars.l9
-rw-r--r--src/handlebars.yy8
-rw-r--r--tasks/test.js13
35 files changed, 1055 insertions, 290 deletions
diff --git a/.gitignore b/.gitignore
index 0e32d04..3c6d099 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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/**']
diff --git a/FAQ.md b/FAQ.md
index bd4f6aa..4e8b856 100644
--- a/FAQ.md
+++ b/FAQ.md
@@ -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 @@
-[![Travis Build Status](https://travis-ci.org/wycats/handlebars.js.png?branch=master)](https://travis-ci.org/wycats/handlebars.js)
+[![Travis Build Status](https://img.shields.io/travis/wycats/handlebars.js/master.svg)](https://travis-ci.org/wycats/handlebars.js)
[![Selenium Test Status](https://saucelabs.com/buildstatus/handlebars)](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] || "&amp;";
+ 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']);
};