Compare commits

..

25 Commits

Author SHA1 Message Date
kpdecker 0c6829f8af v1.3.0 2014-01-01 22:10:31 -06:00
kpdecker 2b9f3c195f Update release notes 2014-01-01 22:09:50 -06:00
kpdecker f445b96cb7 Merge branch 'master' of github.com:wycats/handlebars.js 2014-01-01 19:42:16 -06:00
kpdecker e0d0ad5027 Merge branch 'compiler-options' of github.com:blakeembrey/handlebars.js into blakeembrey-compiler-options
Conflicts:
	lib/handlebars/compiler/javascript-compiler.js
2014-01-01 19:38:31 -06:00
Kevin Decker 3f71fde155 Merge pull request #696 from nateirwin/master
Fix for reserved keyword "default"
2014-01-01 17:17:19 -08:00
kpdecker 6e4e1f8404 Include line info in compiler thrown exceptions
Fixes #636
2014-01-01 19:14:51 -06:00
kpdecker 3c85720137 Remove duplication from generated subexpressions 2014-01-01 18:59:21 -06:00
kpdecker 4713abc8f1 Add subexpression benchmark 2014-01-01 18:15:39 -06:00
kpdecker e7e94dc697 Whitespace cleanup 2013-12-31 21:20:59 -06:00
kpdecker c1a93d33e7 Use literal for data lookup 2013-12-31 21:19:00 -06:00
kpdecker cd885bf855 Add stack handling sanity checks 2013-12-31 21:18:48 -06:00
kpdecker ddfe457abf Fix stack id "leak" on replaceStack 2013-12-31 21:17:50 -06:00
kpdecker f4d337d252 Fix incorrect stack pop when replacing literals 2013-12-31 21:17:16 -06:00
kpdecker cbfec5b9e9 Set laxbreak jshint flag 2013-12-31 20:25:46 -06:00
kpdecker af3358d195 Fix multiple hash handling in subexpressions 2013-12-31 16:10:51 -06:00
Nate Irwin 641358a905 Fix for reserved keyword "default" 2013-12-31 15:09:41 -07:00
kpdecker f2df220a1f Add location tracking to sexpr 2013-12-31 13:31:47 -06:00
Kevin Decker a2ca31bb19 Merge pull request #690 from machty/subsexpr
Added support for subexpressions
2013-12-30 16:09:41 -08:00
machty b09333db79 Added support for subexpressions
Handlebars now supports subexpressions.

    {{foo (bar 3)}}

Subexpressions are always evaluated as helpers; if
`3` were omitted from the above example, `bar`
would be invoked as a param-less helper, even
though a top-levell `{{bar}}` would be considered
ambiguous.

The return value of a subexpression helper is
passed in as a parameter of a parent subexpression
helper, even in string params mode. Their type,
as listed in `options.types` or `options.hashTypes`
in string params mode, is "sexpr".

The main conceptual change in the Handlebars code
is that there is a new AST.SexprNode that manages
the params/hash passed into a mustache, as well
as the logic that governs whether that mustache
is a helper invocation, property lookup, etc.
MustacheNode, which used to manage this stuff,
still exists, but only manages things like
white-space stripping and whether the mustache
is escaped or not. So, a MustacheNode _has_
a SexprNode.

The introduction of subexpressions is fully
backwards compatible, but a few things needed
to change about the compiled output of a template
in order to support subexpressions. The main one
is that the options hash is no longer stashed in
a local `options` var before being passed to
either the helper being invoked or the
`helperMissing` fallback. Rather, the options
object is inlined in these cases. This does
mean compiled template sizes will be a little
bit larger, even those that don't make use of
subexpressions, but shouldn't have any noticeable
impact on performance when actually rendering
templates as only one of these inlined objects
will actually get evaluated.
2013-12-30 19:07:04 -05:00
Kevin Decker ac98e7b177 Merge pull request #694 from blakeembrey/compile-env
Make the environment reusable
2013-12-29 15:23:47 -08:00
kpdecker 14d1d4270f Fix ProgramNode parameter handling under IE 2013-12-29 17:16:59 -06:00
Blake Embrey 150e55aa00 Pull options out from param setup to allow easier extension. 2013-12-28 22:31:45 -05:00
Blake Embrey 2f0c96b618 Make the environment reusable. 2013-12-28 21:35:40 -05:00
Yehuda Katz 6c2137a420 Merge pull request #692 from fivetanley/line-numbers
add line numbers to nodes when parsing
2013-12-28 17:12:25 -08:00
Stanley Stuart e1878050b5 add line numbers to nodes when parsing
closes #691
2013-12-28 18:55:36 -06:00
22 changed files with 761 additions and 180 deletions
+1 -1
View File
@@ -35,7 +35,7 @@
"evil": true,
"forin": false,
"immed": false,
"laxbreak": false,
"laxbreak": true,
"newcap": true,
"noarg": true,
"noempty": false,
+14
View File
@@ -0,0 +1,14 @@
module.exports = {
helpers: {
echo: function(value) {
return 'foo ' + value;
},
header: function() {
return "Colors";
}
},
handlebars: "{{echo (header)}}",
eco: "<%= @echo(@header()) %>"
};
module.exports.context = module.exports.helpers;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "handlebars",
"version": "1.2.1",
"version": "1.3.0",
"main": "handlebars.js",
"dependencies": {}
}
+1 -1
View File
@@ -2,7 +2,7 @@
<package>
<metadata>
<id>handlebars.js</id>
<version>1.2.1</version>
<version>1.3.0</version>
<authors>handlebars.js Authors</authors>
<licenseUrl>https://github.com/wycats/handlebars.js/blob/master/LICENSE</licenseUrl>
<projectUrl>https://github.com/wycats/handlebars.js/</projectUrl>
+3 -1
View File
@@ -14,7 +14,9 @@ var create = function() {
hb.compile = function(input, options) {
return compile(input, options, hb);
};
hb.precompile = precompile;
hb.precompile = function (input, options) {
return precompile(input, options, hb);
};
hb.AST = AST;
hb.Compiler = Compiler;
+2 -2
View File
@@ -1,7 +1,7 @@
module Utils from "./utils";
import Exception from "./exception";
export var VERSION = "1.2.1";
export var VERSION = "1.3.0";
export var COMPILER_REVISION = 4;
export var REVISION_CHANGES = {
@@ -53,7 +53,7 @@ function registerDefaultHelpers(instance) {
if(arguments.length === 2) {
return undefined;
} else {
throw new Error("Missing helper: '" + arg + "'");
throw new Exception("Missing helper: '" + arg + "'");
}
});
+91 -22
View File
@@ -1,22 +1,51 @@
import Exception from "../exception";
function LocationInfo(locInfo){
locInfo = locInfo || {};
this.firstLine = locInfo.first_line;
this.firstColumn = locInfo.first_column;
this.lastColumn = locInfo.last_column;
this.lastLine = locInfo.last_line;
}
var AST = {
ProgramNode: function(statements, inverseStrip, inverse) {
ProgramNode: function(statements, inverseStrip, inverse, locInfo) {
var inverseLocationInfo, firstInverseNode;
if (arguments.length === 3) {
locInfo = inverse;
inverse = null;
} else if (arguments.length === 2) {
locInfo = inverseStrip;
inverseStrip = null;
}
LocationInfo.call(this, locInfo);
this.type = "program";
this.statements = statements;
this.strip = {};
if(inverse) {
this.inverse = new AST.ProgramNode(inverse, inverseStrip);
firstInverseNode = inverse[0];
if (firstInverseNode) {
inverseLocationInfo = {
first_line: firstInverseNode.firstLine,
last_line: firstInverseNode.lastLine,
last_column: firstInverseNode.lastColumn,
first_column: firstInverseNode.firstColumn
};
this.inverse = new AST.ProgramNode(inverse, inverseStrip, inverseLocationInfo);
} else {
this.inverse = new AST.ProgramNode(inverse, inverseStrip);
}
this.strip.right = inverseStrip.left;
} else if (inverseStrip) {
this.strip.left = inverseStrip.right;
}
},
MustacheNode: function(rawParams, hash, open, strip) {
MustacheNode: function(rawParams, hash, open, strip, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "mustache";
this.hash = hash;
this.strip = strip;
// Open may be a string parsed from the parser or a passed boolean flag
@@ -28,6 +57,29 @@ var AST = {
this.escaped = !!open;
}
if (rawParams instanceof AST.SexprNode) {
this.sexpr = rawParams;
} else {
// Support old AST API
this.sexpr = new AST.SexprNode(rawParams, hash);
}
this.sexpr.isRoot = true;
// Support old AST API that stored this info in MustacheNode
this.id = this.sexpr.id;
this.params = this.sexpr.params;
this.hash = this.sexpr.hash;
this.eligibleHelper = this.sexpr.eligibleHelper;
this.isHelper = this.sexpr.isHelper;
},
SexprNode: function(rawParams, hash, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "sexpr";
this.hash = hash;
var id = this.id = rawParams[0];
var params = this.params = rawParams.slice(1);
@@ -45,19 +97,22 @@ var AST = {
// pass or at runtime.
},
PartialNode: function(partialName, context, strip) {
PartialNode: function(partialName, context, strip, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "partial";
this.partialName = partialName;
this.context = context;
this.strip = strip;
},
BlockNode: function(mustache, program, inverse, close) {
if(mustache.id.original !== close.path.original) {
throw new Exception(mustache.id.original + " doesn't match " + close.path.original);
BlockNode: function(mustache, program, inverse, close, locInfo) {
LocationInfo.call(this, locInfo);
if(mustache.sexpr.id.original !== close.path.original) {
throw new Exception(mustache.sexpr.id.original + " doesn't match " + close.path.original, this);
}
this.type = "block";
this.type = 'block';
this.mustache = mustache;
this.program = program;
this.inverse = inverse;
@@ -75,17 +130,20 @@ var AST = {
}
},
ContentNode: function(string) {
ContentNode: function(string, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "content";
this.string = string;
},
HashNode: function(pairs) {
HashNode: function(pairs, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "hash";
this.pairs = pairs;
},
IdNode: function(parts) {
IdNode: function(parts, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "ID";
var original = "",
@@ -97,11 +155,16 @@ var AST = {
original += (parts[i].separator || '') + part;
if (part === ".." || part === "." || part === "this") {
if (dig.length > 0) { throw new Exception("Invalid path: " + original); }
else if (part === "..") { depth++; }
else { this.isScoped = true; }
if (dig.length > 0) {
throw new Exception("Invalid path: " + original, this);
} else if (part === "..") {
depth++;
} else {
this.isScoped = true;
}
} else {
dig.push(part);
}
else { dig.push(part); }
}
this.original = original;
@@ -116,37 +179,43 @@ var AST = {
this.stringModeValue = this.string;
},
PartialNameNode: function(name) {
PartialNameNode: function(name, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "PARTIAL_NAME";
this.name = name.original;
},
DataNode: function(id) {
DataNode: function(id, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "DATA";
this.id = id;
},
StringNode: function(string) {
StringNode: function(string, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "STRING";
this.original =
this.string =
this.stringModeValue = string;
},
IntegerNode: function(integer) {
IntegerNode: function(integer, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "INTEGER";
this.original =
this.integer = integer;
this.stringModeValue = Number(integer);
},
BooleanNode: function(bool) {
BooleanNode: function(bool, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "BOOLEAN";
this.bool = bool;
this.stringModeValue = bool === "true";
},
CommentNode: function(comment) {
CommentNode: function(comment, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "comment";
this.comment = comment;
}
+58 -60
View File
@@ -1,7 +1,4 @@
import Exception from "../exception";
import { parse } from "./base";
import JavaScriptCompiler from "./javascript-compiler";
import AST from "./ast";
export function Compiler() {}
@@ -159,12 +156,13 @@ Compiler.prototype = {
inverse = this.compileProgram(inverse);
}
var type = this.classifyMustache(mustache);
var sexpr = mustache.sexpr;
var type = this.classifySexpr(sexpr);
if (type === "helper") {
this.helperMustache(mustache, program, inverse);
this.helperSexpr(sexpr, program, inverse);
} else if (type === "simple") {
this.simpleMustache(mustache);
this.simpleSexpr(sexpr);
// now that the simple mustache is resolved, we need to
// evaluate it by executing `blockHelperMissing`
@@ -173,7 +171,7 @@ Compiler.prototype = {
this.opcode('emptyHash');
this.opcode('blockValue');
} else {
this.ambiguousMustache(mustache, program, inverse);
this.ambiguousSexpr(sexpr, program, inverse);
// now that the simple mustache is resolved, we need to
// evaluate it by executing `blockHelperMissing`
@@ -201,6 +199,12 @@ Compiler.prototype = {
}
this.opcode('getContext', val.depth || 0);
this.opcode('pushStringParam', val.stringModeValue, val.type);
if (val.type === 'sexpr') {
// Subexpressions get evaluated and passed in
// in string params mode.
this.sexpr(val);
}
} else {
this.accept(val);
}
@@ -229,26 +233,17 @@ Compiler.prototype = {
},
mustache: function(mustache) {
var options = this.options;
var type = this.classifyMustache(mustache);
this.sexpr(mustache.sexpr);
if (type === "simple") {
this.simpleMustache(mustache);
} else if (type === "helper") {
this.helperMustache(mustache);
} else {
this.ambiguousMustache(mustache);
}
if(mustache.escaped && !options.noEscape) {
if(mustache.escaped && !this.options.noEscape) {
this.opcode('appendEscaped');
} else {
this.opcode('append');
}
},
ambiguousMustache: function(mustache, program, inverse) {
var id = mustache.id,
ambiguousSexpr: function(sexpr, program, inverse) {
var id = sexpr.id,
name = id.parts[0],
isBlock = program != null || inverse != null;
@@ -260,8 +255,8 @@ Compiler.prototype = {
this.opcode('invokeAmbiguous', name, isBlock);
},
simpleMustache: function(mustache) {
var id = mustache.id;
simpleSexpr: function(sexpr) {
var id = sexpr.id;
if (id.type === 'DATA') {
this.DATA(id);
@@ -277,16 +272,28 @@ Compiler.prototype = {
this.opcode('resolvePossibleLambda');
},
helperMustache: function(mustache, program, inverse) {
var params = this.setupFullMustacheParams(mustache, program, inverse),
name = mustache.id.parts[0];
helperSexpr: function(sexpr, program, inverse) {
var params = this.setupFullMustacheParams(sexpr, program, inverse),
name = sexpr.id.parts[0];
if (this.options.knownHelpers[name]) {
this.opcode('invokeKnownHelper', params.length, name);
} else if (this.options.knownHelpersOnly) {
throw new Error("You specified knownHelpersOnly, but used the unknown helper " + name);
throw new Exception("You specified knownHelpersOnly, but used the unknown helper " + name, sexpr);
} else {
this.opcode('invokeHelper', params.length, name);
this.opcode('invokeHelper', params.length, name, sexpr.isRoot);
}
},
sexpr: function(sexpr) {
var type = this.classifySexpr(sexpr);
if (type === "simple") {
this.simpleSexpr(sexpr);
} else if (type === "helper") {
this.helperSexpr(sexpr);
} else {
this.ambiguousSexpr(sexpr);
}
},
@@ -309,7 +316,7 @@ Compiler.prototype = {
DATA: function(data) {
this.options.data = true;
if (data.id.isScoped || data.id.depth) {
throw new Exception('Scoped data references are not supported: ' + data.original);
throw new Exception('Scoped data references are not supported: ' + data.original, data);
}
this.opcode('lookupData');
@@ -343,7 +350,6 @@ Compiler.prototype = {
},
addDepth: function(depth) {
if(isNaN(depth)) { throw new Error("EWOT"); }
if(depth === 0) { return; }
if(!this.depths[depth]) {
@@ -352,14 +358,14 @@ Compiler.prototype = {
}
},
classifyMustache: function(mustache) {
var isHelper = mustache.isHelper;
var isEligible = mustache.eligibleHelper;
classifySexpr: function(sexpr) {
var isHelper = sexpr.isHelper;
var isEligible = sexpr.eligibleHelper;
var options = this.options;
// if ambiguous, we can possibly resolve the ambiguity now
if (isEligible && !isHelper) {
var name = mustache.id.parts[0];
var name = sexpr.id.parts[0];
if (options.knownHelpers[name]) {
isHelper = true;
@@ -386,35 +392,27 @@ Compiler.prototype = {
this.opcode('getContext', param.depth || 0);
this.opcode('pushStringParam', param.stringModeValue, param.type);
if (param.type === 'sexpr') {
// Subexpressions get evaluated and passed in
// in string params mode.
this.sexpr(param);
}
} else {
this[param.type](param);
}
}
},
setupMustacheParams: function(mustache) {
var params = mustache.params;
this.pushParams(params);
if(mustache.hash) {
this.hash(mustache.hash);
} else {
this.opcode('emptyHash');
}
return params;
},
// this will replace setupMustacheParams when we're done
setupFullMustacheParams: function(mustache, program, inverse) {
var params = mustache.params;
setupFullMustacheParams: function(sexpr, program, inverse) {
var params = sexpr.params;
this.pushParams(params);
this.opcode('pushProgram', program);
this.opcode('pushProgram', inverse);
if(mustache.hash) {
this.hash(mustache.hash);
if (sexpr.hash) {
this.hash(sexpr.hash);
} else {
this.opcode('emptyHash');
}
@@ -423,8 +421,8 @@ Compiler.prototype = {
}
};
export function precompile(input, options) {
if (input == null || (typeof input !== 'string' && input.constructor !== AST.ProgramNode)) {
export function precompile(input, options, env) {
if (input == null || (typeof input !== 'string' && input.constructor !== env.AST.ProgramNode)) {
throw new Exception("You must pass a string or Handlebars AST to Handlebars.precompile. You passed " + input);
}
@@ -433,13 +431,13 @@ export function precompile(input, options) {
options.data = true;
}
var ast = parse(input);
var environment = new Compiler().compile(ast, options);
return new JavaScriptCompiler().compile(environment, options);
var ast = env.parse(input);
var environment = new env.Compiler().compile(ast, options);
return new env.JavaScriptCompiler().compile(environment, options);
}
export function compile(input, options, env) {
if (input == null || (typeof input !== 'string' && input.constructor !== AST.ProgramNode)) {
if (input == null || (typeof input !== 'string' && input.constructor !== env.AST.ProgramNode)) {
throw new Exception("You must pass a string or Handlebars AST to Handlebars.compile. You passed " + input);
}
@@ -452,9 +450,9 @@ export function compile(input, options, env) {
var compiled;
function compileInput() {
var ast = parse(input);
var environment = new Compiler().compile(ast, options);
var templateSpec = new JavaScriptCompiler().compile(environment, options, undefined, true);
var ast = env.parse(input);
var environment = new env.Compiler().compile(ast, options);
var templateSpec = new env.JavaScriptCompiler().compile(environment, options, undefined, true);
return env.template(templateSpec);
}
+85 -37
View File
@@ -1,4 +1,5 @@
import { COMPILER_REVISION, REVISION_CHANGES, log } from "../base";
import Exception from "../exception";
function Literal(value) {
this.value = value;
@@ -76,6 +77,7 @@ JavaScriptCompiler.prototype = {
this.stackSlot = 0;
this.stackVars = [];
this.registers = { list: [] };
this.hashes = [];
this.compileStack = [];
this.inlineStack = [];
@@ -103,6 +105,10 @@ JavaScriptCompiler.prototype = {
// Flush any trailing content that might be pending.
this.pushSource('');
if (this.stackSlot || this.inlineStack.length || this.compileStack.length) {
throw new Exception('Compile completed with content left on stack');
}
return this.createFunctionContext(asObject);
},
@@ -244,9 +250,6 @@ JavaScriptCompiler.prototype = {
var current = this.topStack();
params.splice(1, 0, current);
// Use the options value generated from the invocation
params[params.length-1] = 'options';
this.pushSource("if (!" + this.lastHelper + ") { " + current + " = blockHelperMissing.call(" + params.join(", ") + "); }");
},
@@ -382,7 +385,7 @@ JavaScriptCompiler.prototype = {
//
// Push the data lookup operator
lookupData: function() {
this.push('data');
this.pushStackLiteral('data');
},
// [pushStringParam]
@@ -398,10 +401,14 @@ JavaScriptCompiler.prototype = {
this.pushString(type);
if (typeof string === 'string') {
this.pushString(string);
} else {
this.pushStackLiteral(string);
// If it's a subexpression, the string result
// will be pushed after this opcode.
if (type !== 'sexpr') {
if (typeof string === 'string') {
this.pushString(string);
} else {
this.pushStackLiteral(string);
}
}
},
@@ -409,21 +416,25 @@ JavaScriptCompiler.prototype = {
this.pushStackLiteral('{}');
if (this.options.stringParams) {
this.register('hashTypes', '{}');
this.register('hashContexts', '{}');
this.push('{}'); // hashContexts
this.push('{}'); // hashTypes
}
},
pushHash: function() {
if (this.hash) {
this.hashes.push(this.hash);
}
this.hash = {values: [], types: [], contexts: []};
},
popHash: function() {
var hash = this.hash;
this.hash = undefined;
this.hash = this.hashes.pop();
if (this.options.stringParams) {
this.register('hashContexts', '{' + hash.contexts.join(',') + '}');
this.register('hashTypes', '{' + hash.types.join(',') + '}');
this.push('{' + hash.contexts.join(',') + '}');
this.push('{' + hash.types.join(',') + '}');
}
this.push('{\n ' + hash.values.join(',\n ') + '\n }');
},
@@ -485,18 +496,31 @@ JavaScriptCompiler.prototype = {
// and pushes the helper's return value onto the stack.
//
// If the helper is not found, `helperMissing` is called.
invokeHelper: function(paramSize, name) {
invokeHelper: function(paramSize, name, isRoot) {
this.context.aliases.helperMissing = 'helpers.helperMissing';
this.useRegister('helper');
var helper = this.lastHelper = this.setupHelper(paramSize, name, true);
var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context');
this.push(helper.name + ' || ' + nonHelper);
this.replaceStack(function(name) {
return name + ' ? ' + name + '.call(' +
helper.callParams + ") " + ": helperMissing.call(" +
helper.helperMissingParams + ")";
});
var lookup = 'helper = ' + helper.name + ' || ' + nonHelper;
if (helper.paramsInit) {
lookup += ',' + helper.paramsInit;
}
this.push(
'('
+ lookup
+ ',helper '
+ '? helper.call(' + helper.callParams + ') '
+ ': helperMissing.call(' + helper.helperMissingParams + '))');
// 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]
@@ -525,8 +549,9 @@ JavaScriptCompiler.prototype = {
// `knownHelpersOnly` flags at compile-time.
invokeAmbiguous: function(name, helperCall) {
this.context.aliases.functionType = '"function"';
this.useRegister('helper');
this.pushStackLiteral('{}'); // Hash value
this.emptyHash();
var helper = this.setupHelper(0, name, helperCall);
var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper');
@@ -534,8 +559,11 @@ JavaScriptCompiler.prototype = {
var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context');
var nextStack = this.nextStack();
this.pushSource('if (' + nextStack + ' = ' + helperName + ') { ' + nextStack + ' = ' + nextStack + '.call(' + helper.callParams + '); }');
this.pushSource('else { ' + nextStack + ' = ' + nonHelper + '; ' + nextStack + ' = typeof ' + nextStack + ' === functionType ? ' + nextStack + '.call(' + helper.callParams + ') : ' + nextStack + '; }');
if (helper.paramsInit) {
this.pushSource(helper.paramsInit);
}
this.pushSource('if (helper = ' + helperName + ') { ' + nextStack + ' = helper.call(' + helper.callParams + '); }');
this.pushSource('else { helper = ' + nonHelper + '; ' + nextStack + ' = typeof helper === functionType ? helper.call(' + helper.callParams + ') : helper; }');
},
// [invokePartial]
@@ -681,7 +709,9 @@ JavaScriptCompiler.prototype = {
replaceStack: function(callback) {
var prefix = '',
inline = this.isInline(),
stack;
stack,
createdStack,
usedLiteral;
// If we are currently inline then we want to merge the inline statement into the
// replacement statement via ','
@@ -691,9 +721,11 @@ JavaScriptCompiler.prototype = {
if (top instanceof Literal) {
// Literals do not need to be inlined
stack = top.value;
usedLiteral = true;
} else {
// Get or create the current stack name for use by the inline
var name = this.stackSlot ? this.topStackName() : this.incrStack();
createdStack = !this.stackSlot;
var name = !createdStack ? this.topStackName() : this.incrStack();
prefix = '(' + this.push(name) + ' = ' + top + '),';
stack = this.topStack();
@@ -705,9 +737,12 @@ JavaScriptCompiler.prototype = {
var item = callback.call(this, stack);
if (inline) {
if (this.inlineStack.length || this.compileStack.length) {
if (!usedLiteral) {
this.popStack();
}
if (createdStack) {
this.stackSlot--;
}
this.push('(' + prefix + item + ')');
} else {
// Prevent modification of the context depth variable. Through replaceStack
@@ -758,6 +793,9 @@ JavaScriptCompiler.prototype = {
return item.value;
} else {
if (!inline) {
if (!this.stackSlot) {
throw new Exception('Invalid stack pop');
}
this.stackSlot--;
}
return item;
@@ -786,25 +824,29 @@ JavaScriptCompiler.prototype = {
},
setupHelper: function(paramSize, name, missingParams) {
var params = [];
this.setupParams(paramSize, params, missingParams);
var params = [],
paramsInit = this.setupParams(paramSize, params, missingParams);
var foundHelper = this.nameLookup('helpers', name, 'helper');
return {
params: params,
paramsInit: paramsInit,
name: foundHelper,
callParams: ["depth0"].concat(params).join(", "),
helperMissingParams: missingParams && ["depth0", this.quotedString(name)].concat(params).join(", ")
};
},
// the params and contexts arguments are passed in arrays
// to fill in
setupParams: function(paramSize, params, useRegister) {
setupOptions: function(paramSize, params) {
var options = [], contexts = [], types = [], param, inverse, program;
options.push("hash:" + this.popStack());
if (this.options.stringParams) {
options.push("hashTypes:" + this.popStack());
options.push("hashContexts:" + this.popStack());
}
inverse = this.popStack();
program = this.popStack();
@@ -817,7 +859,7 @@ JavaScriptCompiler.prototype = {
}
if (!inverse) {
this.context.aliases.self = "this";
this.context.aliases.self = "this";
inverse = "self.noop";
}
@@ -838,22 +880,28 @@ JavaScriptCompiler.prototype = {
if (this.options.stringParams) {
options.push("contexts:[" + contexts.join(",") + "]");
options.push("types:[" + types.join(",") + "]");
options.push("hashContexts:hashContexts");
options.push("hashTypes:hashTypes");
}
if(this.options.data) {
options.push("data:data");
}
options = "{" + options.join(",") + "}";
return options;
},
// the params and contexts arguments are passed in arrays
// to fill in
setupParams: function(paramSize, params, useRegister) {
var options = '{' + this.setupOptions(paramSize, params).join(',') + '}';
if (useRegister) {
this.register('options', options);
this.useRegister('options');
params.push('options');
return 'options=' + options;
} else {
params.push(options);
return '';
}
return params.join(", ");
}
};
+8 -4
View File
@@ -62,8 +62,8 @@ PrintVisitor.prototype.block = function(block) {
return out;
};
PrintVisitor.prototype.mustache = function(mustache) {
var params = mustache.params, paramStrings = [], hash;
PrintVisitor.prototype.sexpr = function(sexpr) {
var params = sexpr.params, paramStrings = [], hash;
for(var i=0, l=params.length; i<l; i++) {
paramStrings.push(this.accept(params[i]));
@@ -71,9 +71,13 @@ PrintVisitor.prototype.mustache = function(mustache) {
params = "[" + paramStrings.join(", ") + "]";
hash = mustache.hash ? " " + this.accept(mustache.hash) : "";
hash = sexpr.hash ? " " + this.accept(sexpr.hash) : "";
return this.pad("{{ " + this.accept(mustache.id) + " " + params + hash + " }}");
return this.accept(sexpr.id) + " " + params + hash;
};
PrintVisitor.prototype.mustache = function(mustache) {
return this.pad("{{ " + this.accept(mustache.sexpr) + " }}");
};
PrintVisitor.prototype.partial = function(partial) {
+14 -2
View File
@@ -1,13 +1,25 @@
var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack'];
function Exception(/* message */) {
var tmp = Error.prototype.constructor.apply(this, arguments);
function Exception(message, node) {
var line;
if (node && node.firstLine) {
line = node.firstLine;
message += ' - ' + line + ':' + node.firstColumn;
}
var tmp = Error.prototype.constructor.call(this, message);
// Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work.
for (var idx = 0; idx < errorProps.length; idx++) {
this[errorProps[idx]] = tmp[errorProps[idx]];
}
if (line) {
this.lineNumber = line;
this.column = node.firstColumn;
}
}
Exception.prototype = new Error();
+3 -3
View File
@@ -10,11 +10,11 @@ export function checkRevision(compilerInfo) {
if (compilerRevision < currentRevision) {
var runtimeVersions = REVISION_CHANGES[currentRevision],
compilerVersions = REVISION_CHANGES[compilerRevision];
throw new Error("Template was precompiled with an older version of Handlebars than the current runtime. "+
throw new Exception("Template was precompiled with an older version of Handlebars than the current runtime. "+
"Please update your precompiler to a newer version ("+runtimeVersions+") or downgrade your runtime to an older version ("+compilerVersions+").");
} else {
// Use the embedded version info since the runtime doesn't know about this revision yet
throw new Error("Template was precompiled with a newer version of Handlebars than the current runtime. "+
throw new Exception("Template was precompiled with a newer version of Handlebars than the current runtime. "+
"Please update your runtime to a newer version ("+compilerInfo[1]+").");
}
}
@@ -24,7 +24,7 @@ export function checkRevision(compilerInfo) {
export function template(templateSpec, env) {
if (!env) {
throw new Error("No environment passed to template");
throw new Exception("No environment passed to template");
}
// Note: Using env.VM references rather than local var references throughout this section to allow
+2 -2
View File
@@ -3,9 +3,9 @@
// var local = handlebars.create();
var handlebars = require('../dist/cjs/handlebars').default;
var handlebars = require('../dist/cjs/handlebars')["default"];
handlebars.Visitor = require('../dist/cjs/handlebars/compiler/visitor').default;
handlebars.Visitor = require('../dist/cjs/handlebars/compiler/visitor')["default"];
var printer = require('../dist/cjs/handlebars/compiler/printer');
handlebars.PrintVisitor = printer.PrintVisitor;
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "handlebars",
"barename": "handlebars",
"version": "1.2.1",
"version": "1.3.0",
"description": "Handlebars provides the power necessary to let you build semantic templates effectively with no frustration",
"homepage": "http://www.handlebarsjs.com/",
"keywords": [
+15 -1
View File
@@ -2,7 +2,21 @@
## Development
[Commits](https://github.com/wycats/handlebars.js/compare/v1.2.1...master)
[Commits](https://github.com/wycats/handlebars.js/compare/v1.3.0...master)
## v1.3.0 - January 1st, 2014
- [#690](https://github.com/wycats/handlebars.js/pull/690) - Added support for subexpressions ([@machty](https://api.github.com/users/machty))
- [#696](https://github.com/wycats/handlebars.js/pull/696) - Fix for reserved keyword "default" ([@nateirwin](https://api.github.com/users/nateirwin))
- [#692](https://github.com/wycats/handlebars.js/pull/692) - add line numbers to nodes when parsing ([@fivetanley](https://api.github.com/users/fivetanley))
- [#695](https://github.com/wycats/handlebars.js/pull/695) - Pull options out from param setup to allow easier extension ([@blakeembrey](https://api.github.com/users/blakeembrey))
- [#694](https://github.com/wycats/handlebars.js/pull/694) - Make the environment reusable ([@blakeembrey](https://api.github.com/users/blakeembrey))
- [#636](https://github.com/wycats/handlebars.js/issues/636) - Print line and column of errors ([@sgronblo](https://api.github.com/users/sgronblo))
- Use literal for data lookup - c1a93d3
- Add stack handling sanity checks - cd885bf
- Fix stack id "leak" on replaceStack - ddfe457
- Fix incorrect stack pop when replacing literals - f4d337d
[Commits](https://github.com/wycats/handlebars.js/compare/v1.2.1...v1.3.0)
## v1.2.1 - December 26th, 2013
- [#684](https://github.com/wycats/handlebars.js/pull/684) - Allow any number of trailing characters for valid JavaScript variable ([@blakeembrey](https://api.github.com/users/blakeembrey))
+203 -9
View File
@@ -4,6 +4,25 @@ describe('ast', function() {
return;
}
var LOCATION_INFO = {
last_line: 0,
first_line: 0,
first_column: 0,
last_column: 0
};
function testLocationInfoStorage(node){
var properties = [ 'firstLine', 'lastLine', 'firstColumn', 'lastColumn' ],
property,
propertiesLen = properties.length,
i;
for (i = 0; i < propertiesLen; i++){
property = properties[0];
equals(node[property], 0);
}
}
describe('MustacheNode', function() {
function testEscape(open, expected) {
var mustache = new handlebarsEnv.AST.MustacheNode([{}], {}, open, false);
@@ -13,7 +32,7 @@ describe('ast', function() {
it('should store args', function() {
var id = {isSimple: true},
hash = {},
mustache = new handlebarsEnv.AST.MustacheNode([id, 'param1'], hash, '', false);
mustache = new handlebarsEnv.AST.MustacheNode([id, 'param1'], hash, '', false, LOCATION_INFO);
equals(mustache.type, 'mustache');
equals(mustache.hash, hash);
equals(mustache.escaped, true);
@@ -21,6 +40,7 @@ describe('ast', function() {
equals(mustache.params.length, 1);
equals(mustache.params[0], 'param1');
equals(!!mustache.isHelper, true);
testLocationInfoStorage(mustache);
});
it('should accept token for escape', function() {
testEscape('{{', true);
@@ -48,10 +68,31 @@ describe('ast', function() {
});
});
describe('BlockNode', function() {
it('should throw on mustache mismatch (old sexpr-less version)', function() {
shouldThrow(function() {
var mustacheNode = new handlebarsEnv.AST.MustacheNode([{ original: 'foo'}], null, '{{', {});
new handlebarsEnv.AST.BlockNode(mustacheNode, {}, {}, {path: {original: 'bar'}});
}, Handlebars.Exception, "foo doesn't match bar");
});
it('should throw on mustache mismatch', function() {
shouldThrow(function() {
new handlebarsEnv.AST.BlockNode({id: {original: 'foo'}}, {}, {}, {path: {original: 'bar'}});
}, Handlebars.Exception, "foo doesn't match bar");
var sexprNode = new handlebarsEnv.AST.SexprNode([{ original: 'foo'}], null);
var mustacheNode = new handlebarsEnv.AST.MustacheNode(sexprNode, null, '{{', {});
new handlebarsEnv.AST.BlockNode(mustacheNode, {}, {}, {path: {original: 'bar'}}, {first_line: 2, first_column: 2});
}, Handlebars.Exception, "foo doesn't match bar - 2:2");
});
it('stores location info', 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: {}},
{
strip: {},
path: {original: 'foo'}
},
LOCATION_INFO);
testLocationInfoStorage(block);
});
});
describe('IdNode', function() {
@@ -61,22 +102,175 @@ describe('ast', function() {
{part: 'foo'},
{part: '..'},
{part: 'bar'}
]);
}, Handlebars.Exception, "Invalid path: foo..");
], {first_line: 1, first_column: 1});
}, Handlebars.Exception, "Invalid path: foo.. - 1:1");
shouldThrow(function() {
new handlebarsEnv.AST.IdNode([
{part: 'foo'},
{part: '.'},
{part: 'bar'}
]);
}, Handlebars.Exception, "Invalid path: foo.");
], {first_line: 1, first_column: 1});
}, Handlebars.Exception, "Invalid path: foo. - 1:1");
shouldThrow(function() {
new handlebarsEnv.AST.IdNode([
{part: 'foo'},
{part: 'this'},
{part: 'bar'}
]);
}, Handlebars.Exception, "Invalid path: foothis");
], {first_line: 1, first_column: 1});
}, Handlebars.Exception, "Invalid path: foothis - 1:1");
});
it('stores location info', function(){
var idNode = new handlebarsEnv.AST.IdNode([], LOCATION_INFO);
testLocationInfoStorage(idNode);
});
});
describe("HashNode", function(){
it('stores location info', function(){
var hash = new handlebarsEnv.AST.HashNode([], LOCATION_INFO);
testLocationInfoStorage(hash);
});
});
describe("ContentNode", function(){
it('stores location info', function(){
var content = new handlebarsEnv.AST.ContentNode("HI", LOCATION_INFO);
testLocationInfoStorage(content);
});
});
describe("CommentNode", function(){
it('stores location info', function(){
var comment = new handlebarsEnv.AST.CommentNode("HI", LOCATION_INFO);
testLocationInfoStorage(comment);
});
});
describe("IntegerNode", function(){
it('stores location info', function(){
var integer = new handlebarsEnv.AST.IntegerNode("6", LOCATION_INFO);
testLocationInfoStorage(integer);
});
});
describe("StringNode", function(){
it('stores location info', function(){
var string = new handlebarsEnv.AST.StringNode("6", LOCATION_INFO);
testLocationInfoStorage(string);
});
});
describe("BooleanNode", function(){
it('stores location info', function(){
var bool = new handlebarsEnv.AST.BooleanNode("true", LOCATION_INFO);
testLocationInfoStorage(bool);
});
});
describe("DataNode", function(){
it('stores location info', function(){
var data = new handlebarsEnv.AST.DataNode("YES", LOCATION_INFO);
testLocationInfoStorage(data);
});
});
describe("PartialNameNode", function(){
it('stores location info', function(){
var pnn = new handlebarsEnv.AST.PartialNameNode({original: "YES"}, LOCATION_INFO);
testLocationInfoStorage(pnn);
});
});
describe("PartialNode", function(){
it('stores location info', function(){
var pn = new handlebarsEnv.AST.PartialNode("so_partial", {}, {}, LOCATION_INFO);
testLocationInfoStorage(pn);
});
});
describe("ProgramNode", function(){
describe("storing location info", function(){
it("stores when `inverse` argument isn't passed", function(){
var pn = new handlebarsEnv.AST.ProgramNode([], LOCATION_INFO);
testLocationInfoStorage(pn);
});
it("stores when `inverse` or `stripInverse` arguments passed", function(){
var pn = new handlebarsEnv.AST.ProgramNode([], {strip: {}}, undefined, LOCATION_INFO);
testLocationInfoStorage(pn);
var clone = {
strip: {},
firstLine: 0,
lastLine: 0,
firstColumn: 0,
lastColumn: 0
};
pn = new handlebarsEnv.AST.ProgramNode([], {strip: {}}, [ clone ], LOCATION_INFO);
testLocationInfoStorage(pn);
// Assert that the newly created ProgramNode has the same location
// information as the inverse
testLocationInfoStorage(pn.inverse);
});
});
});
describe("Line Numbers", function(){
var ast, statements;
function testColumns(node, firstLine, lastLine, firstColumn, lastColumn){
equals(node.firstLine, firstLine);
equals(node.lastLine, lastLine);
equals(node.firstColumn, firstColumn);
equals(node.lastColumn, lastColumn);
}
ast = Handlebars.parse("line 1 {{line1Token}}\n line 2 {{line2token}}\n line 3 {{#blockHelperOnLine3}}\nline 4{{line4token}}\n" +
"line5{{else}}\n{{line6Token}}\n{{/blockHelperOnLine3}}");
statements = ast.statements;
it('gets ContentNode line numbers', function(){
var contentNode = statements[0];
testColumns(contentNode, 1, 1, 0, 7);
});
it('gets MustacheNode line numbers', function(){
var mustacheNode = statements[1];
testColumns(mustacheNode, 1, 1, 7, 21);
});
it('gets line numbers correct when newlines appear', function(){
var secondContentNode = statements[2];
testColumns(secondContentNode, 1, 2, 21, 8);
});
it('gets MustacheNode line numbers correct across newlines', function(){
var secondMustacheNode = statements[3];
testColumns(secondMustacheNode, 2, 2, 8, 22);
});
it('gets the block helper information correct', function(){
var blockHelperNode = statements[5];
testColumns(blockHelperNode, 3, 7, 8, 23);
});
it('correctly records the line numbers of an inverse of a block helper', function(){
var blockHelperNode = statements[5],
inverse = blockHelperNode.inverse;
testColumns(inverse, 5, 6, 13, 0);
});
});
});
+15 -1
View File
@@ -8,11 +8,21 @@ var _ = require('underscore'),
global.Handlebars = undefined;
vm.runInThisContext(fs.readFileSync(__dirname + '/../../dist/handlebars.runtime.js'), 'dist/handlebars.runtime.js');
var parse = require('../../dist/cjs/handlebars/compiler/base').parse;
var compiler = require('../../dist/cjs/handlebars/compiler/compiler');
var JavaScriptCompiler = require('../../dist/cjs/handlebars/compiler/javascript-compiler')['default'];
global.CompilerContext = {
compile: function(template, options) {
var templateSpec = compiler.precompile(template, options);
// Hack the compiler on to the environment for these specific tests
handlebarsEnv.precompile = function(template, options) {
return compiler.precompile(template, options, handlebarsEnv);
};
handlebarsEnv.parse = parse;
handlebarsEnv.Compiler = compiler.Compiler;
handlebarsEnv.JavaScriptCompiler = JavaScriptCompiler;
var templateSpec = handlebarsEnv.precompile(template, options);
return handlebarsEnv.template(safeEval(templateSpec));
},
compileWithPartial: function(template, options) {
@@ -20,6 +30,10 @@ global.CompilerContext = {
handlebarsEnv.compile = function(template, options) {
return compiler.compile(template, options, handlebarsEnv);
};
handlebarsEnv.parse = parse;
handlebarsEnv.Compiler = compiler.Compiler;
handlebarsEnv.JavaScriptCompiler = JavaScriptCompiler;
return handlebarsEnv.compile(template, options);
}
};
+16
View File
@@ -142,4 +142,20 @@ describe('string params mode', function() {
equals(result, "STOP ME FROM READING HACKER NEWS I need-a dad.joke wot", "Proper context variable output");
});
it("with nested block ambiguous", function() {
var template = CompilerContext.compile('{{#with content}}{{#view}}{{firstName}} {{lastName}}{{/view}}{{/with}}', {stringParams: true});
var helpers = {
with: function(options) {
return "WITH";
},
view: function() {
return "VIEW";
}
};
var result = template({}, {helpers: helpers});
equals(result, "WITH");
});
});
+166
View File
@@ -0,0 +1,166 @@
/*global CompilerContext, shouldCompileTo */
describe('subexpressions', function() {
it("arg-less helper", function() {
var string = "{{foo (bar)}}!";
var context = {};
var helpers = {
foo: function(val) {
return val+val;
},
bar: function() {
return "LOL";
}
};
shouldCompileTo(string, [context, helpers], "LOLLOL!");
});
it("helper w args", function() {
var string = '{{blog (equal a b)}}';
var context = { bar: "LOL" };
var helpers = {
blog: function(val) {
return "val is " + val;
},
equal: function(x, y) {
return x === y;
}
};
shouldCompileTo(string, [context, helpers], "val is true");
});
it("supports much nesting", function() {
var string = '{{blog (equal (equal true true) true)}}';
var context = { bar: "LOL" };
var helpers = {
blog: function(val) {
return "val is " + val;
},
equal: function(x, y) {
return x === y;
}
};
shouldCompileTo(string, [context, helpers], "val is true");
});
it("provides each nested helper invocation its own options hash", function() {
var string = '{{equal (equal true true) true}}';
var lastOptions = null;
var helpers = {
equal: function(x, y, options) {
if (!options || options === lastOptions) {
throw new Error("options hash was reused");
}
lastOptions = options;
return x === y;
}
};
shouldCompileTo(string, [{}, helpers], "true");
});
it("with hashes", function() {
var string = '{{blog (equal (equal true true) true fun="yes")}}';
var context = { bar: "LOL" };
var helpers = {
blog: function(val) {
return "val is " + val;
},
equal: function(x, y) {
return x === y;
}
};
shouldCompileTo(string, [context, helpers], "val is true");
});
it("as hashes", function() {
var string = '{{blog fun=(equal (blog fun=1) "val is 1")}}';
var helpers = {
blog: function(options) {
return "val is " + options.hash.fun;
},
equal: function(x, y) {
return x === y;
}
};
shouldCompileTo(string, [{}, helpers], "val is true");
});
it("in string params mode,", function() {
var template = CompilerContext.compile('{{snog (blorg foo x=y) yeah a=b}}', {stringParams: true});
var helpers = {
snog: function(a, b, options) {
equals(a, 'foo');
equals(options.types.length, 2, "string params for outer helper processed correctly");
equals(options.types[0], 'sexpr', "string params for outer helper processed correctly");
equals(options.types[1], 'ID', "string params for outer helper processed correctly");
return a + b;
},
blorg: function(a, options) {
equals(options.types.length, 1, "string params for inner helper processed correctly");
equals(options.types[0], 'ID', "string params for inner helper processed correctly");
return a;
}
};
var result = template({
foo: {},
yeah: {}
}, {helpers: helpers});
equals(result, "fooyeah");
});
it("as hashes in string params mode", function() {
var template = CompilerContext.compile('{{blog fun=(bork)}}', {stringParams: true});
var helpers = {
blog: function(options) {
equals(options.hashTypes.fun, 'sexpr');
return "val is " + options.hash.fun;
},
bork: function() {
return "BORK";
}
};
var result = template({}, {helpers: helpers});
equals(result, "val is BORK");
});
it("subexpression functions on the context", function() {
var string = "{{foo (bar)}}!";
var context = {
bar: function() {
return "LOL";
}
};
var helpers = {
foo: function(val) {
return val+val;
}
};
shouldCompileTo(string, [context, helpers], "LOLLOL!");
});
it("subexpressions can't just be property lookups", function() {
var string = "{{foo (bar)}}!";
var context = {
bar: "LOL"
};
var helpers = {
foo: function(val) {
return val+val;
}
};
shouldThrow(function() {
shouldCompileTo(string, [context, helpers], "LOLLOL!");
});
});
});
+27
View File
@@ -364,4 +364,31 @@ describe('Tokenizer', function() {
it('does not time out in a mustache when invalid ID characters are used', function() {
shouldMatchTokens(tokenize("{{foo & }}"), ['OPEN', 'ID']);
});
it('tokenizes subexpressions', function() {
var result = tokenize("{{foo (bar)}}");
shouldMatchTokens(result, ['OPEN', 'ID', 'OPEN_SEXPR', 'ID', 'CLOSE_SEXPR', 'CLOSE']);
shouldBeToken(result[1], "ID", "foo");
shouldBeToken(result[3], "ID", "bar");
result = tokenize("{{foo (a-x b-y)}}");
shouldMatchTokens(result, ['OPEN', 'ID', 'OPEN_SEXPR', 'ID', 'ID', 'CLOSE_SEXPR', 'CLOSE']);
shouldBeToken(result[1], "ID", "foo");
shouldBeToken(result[3], "ID", "a-x");
shouldBeToken(result[4], "ID", "b-y");
});
it('tokenizes nested subexpressions', function() {
var result = tokenize("{{foo (bar (lol rofl)) (baz)}}");
shouldMatchTokens(result, ['OPEN', 'ID', 'OPEN_SEXPR', 'ID', 'OPEN_SEXPR', 'ID', 'ID', 'CLOSE_SEXPR', 'CLOSE_SEXPR', 'OPEN_SEXPR', 'ID', 'CLOSE_SEXPR', 'CLOSE']);
shouldBeToken(result[3], "ID", "bar");
shouldBeToken(result[5], "ID", "lol");
shouldBeToken(result[6], "ID", "rofl");
shouldBeToken(result[10], "ID", "baz");
});
it('tokenizes nested subexpressions: literals', function() {
var result = tokenize("{{foo (bar (lol true) false) (baz 1) (blah 'b') (blorg \"c\")}}");
shouldMatchTokens(result, ['OPEN', 'ID', 'OPEN_SEXPR', 'ID', 'OPEN_SEXPR', 'ID', 'BOOLEAN', 'CLOSE_SEXPR', 'BOOLEAN', 'CLOSE_SEXPR', 'OPEN_SEXPR', 'ID', 'INTEGER', 'CLOSE_SEXPR', 'OPEN_SEXPR', 'ID', 'STRING', 'CLOSE_SEXPR', 'OPEN_SEXPR', 'ID', 'STRING', 'CLOSE_SEXPR', 'CLOSE']);
});
});
+5 -2
View File
@@ -12,8 +12,8 @@ function strip(start, end) {
LEFT_STRIP "~"
RIGHT_STRIP "~"
LOOKAHEAD [=~}\s\/.]
LITERAL_LOOKAHEAD [~}\s]
LOOKAHEAD [=~}\s\/.)]
LITERAL_LOOKAHEAD [~}\s)]
/*
ID is the inverse of control characters.
@@ -51,6 +51,9 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD}
<com>[\s\S]*?"--}}" strip(0,4); this.popState(); return 'COMMENT';
<mu>"(" return 'OPEN_SEXPR';
<mu>")" return 'CLOSE_SEXPR';
<mu>"{{"{LEFT_STRIP}?">" return 'OPEN_PARTIAL';
<mu>"{{"{LEFT_STRIP}?"#" return 'OPEN_BLOCK';
<mu>"{{"{LEFT_STRIP}?"/" return 'OPEN_ENDBLOCK';
+30 -30
View File
@@ -16,17 +16,17 @@ function stripFlags(open, close) {
%%
root
: statements EOF { return new yy.ProgramNode($1); }
| EOF { return new yy.ProgramNode([]); }
: statements EOF { return new yy.ProgramNode($1, @$); }
| EOF { return new yy.ProgramNode([], @$); }
;
program
: simpleInverse statements -> new yy.ProgramNode([], $1, $2)
| statements simpleInverse statements -> new yy.ProgramNode($1, $2, $3)
| statements simpleInverse -> new yy.ProgramNode($1, $2, [])
| statements -> new yy.ProgramNode($1)
| simpleInverse -> new yy.ProgramNode([])
| "" -> new yy.ProgramNode([])
: simpleInverse statements -> new yy.ProgramNode([], $1, $2, @$)
| statements simpleInverse statements -> new yy.ProgramNode($1, $2, $3, @$)
| statements simpleInverse -> new yy.ProgramNode($1, $2, [], @$)
| statements -> new yy.ProgramNode($1, @$)
| simpleInverse -> new yy.ProgramNode([], @$)
| "" -> new yy.ProgramNode([], @$)
;
statements
@@ -35,20 +35,20 @@ statements
;
statement
: openInverse program closeBlock -> new yy.BlockNode($1, $2.inverse, $2, $3)
| openBlock program closeBlock -> new yy.BlockNode($1, $2, $2.inverse, $3)
: openInverse program closeBlock -> new yy.BlockNode($1, $2.inverse, $2, $3, @$)
| openBlock program closeBlock -> new yy.BlockNode($1, $2, $2.inverse, $3, @$)
| mustache -> $1
| partial -> $1
| CONTENT -> new yy.ContentNode($1)
| COMMENT -> new yy.CommentNode($1)
| CONTENT -> new yy.ContentNode($1, @$)
| COMMENT -> new yy.CommentNode($1, @$)
;
openBlock
: OPEN_BLOCK inMustache CLOSE -> new yy.MustacheNode($2[0], $2[1], $1, stripFlags($1, $3))
: OPEN_BLOCK sexpr CLOSE -> new yy.MustacheNode($2, null, $1, stripFlags($1, $3), @$)
;
openInverse
: OPEN_INVERSE inMustache CLOSE -> new yy.MustacheNode($2[0], $2[1], $1, stripFlags($1, $3))
: OPEN_INVERSE sexpr CLOSE -> new yy.MustacheNode($2, null, $1, stripFlags($1, $3), @$)
;
closeBlock
@@ -58,34 +58,34 @@ closeBlock
mustache
// Parsing out the '&' escape token at AST level saves ~500 bytes after min due to the removal of one parser node.
// This also allows for handler unification as all mustache node instances can utilize the same handler
: OPEN inMustache CLOSE -> new yy.MustacheNode($2[0], $2[1], $1, stripFlags($1, $3))
| OPEN_UNESCAPED inMustache CLOSE_UNESCAPED -> new yy.MustacheNode($2[0], $2[1], $1, stripFlags($1, $3))
: OPEN sexpr CLOSE -> new yy.MustacheNode($2, null, $1, stripFlags($1, $3), @$)
| OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> new yy.MustacheNode($2, null, $1, stripFlags($1, $3), @$)
;
partial
: OPEN_PARTIAL partialName path? CLOSE -> new yy.PartialNode($2, $3, stripFlags($1, $4))
: OPEN_PARTIAL partialName path? CLOSE -> new yy.PartialNode($2, $3, stripFlags($1, $4), @$)
;
simpleInverse
: OPEN_INVERSE CLOSE -> stripFlags($1, $2)
;
inMustache
: path param* hash? -> [[$1].concat($2), $3]
| dataName -> [[$1], null]
sexpr
: path param* hash? -> new yy.SexprNode([$1].concat($2), $3, @$)
| dataName -> new yy.SexprNode([$1], null, @$)
;
param
: path -> $1
| STRING -> new yy.StringNode($1)
| INTEGER -> new yy.IntegerNode($1)
| BOOLEAN -> new yy.BooleanNode($1)
| STRING -> new yy.StringNode($1, @$)
| INTEGER -> new yy.IntegerNode($1, @$)
| BOOLEAN -> new yy.BooleanNode($1, @$)
| dataName -> $1
| OPEN_SEXPR sexpr CLOSE_SEXPR {$2.isHelper = true; $$ = $2;}
;
hash
: hashSegment+ -> new yy.HashNode($1)
: hashSegment+ -> new yy.HashNode($1, @$)
;
hashSegment
@@ -93,17 +93,17 @@ hashSegment
;
partialName
: path -> new yy.PartialNameNode($1)
| STRING -> new yy.PartialNameNode(new yy.StringNode($1))
| INTEGER -> new yy.PartialNameNode(new yy.IntegerNode($1))
: path -> new yy.PartialNameNode($1, @$)
| STRING -> new yy.PartialNameNode(new yy.StringNode($1, @$), @$)
| INTEGER -> new yy.PartialNameNode(new yy.IntegerNode($1, @$))
;
dataName
: DATA path -> new yy.DataNode($2)
: DATA path -> new yy.DataNode($2, @$)
;
path
: pathSegments -> new yy.IdNode($1)
: pathSegments -> new yy.IdNode($1, @$)
;
pathSegments