Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c6829f8af | |||
| 2b9f3c195f | |||
| f445b96cb7 | |||
| e0d0ad5027 | |||
| 3f71fde155 | |||
| 6e4e1f8404 | |||
| 3c85720137 | |||
| 4713abc8f1 | |||
| e7e94dc697 | |||
| c1a93d33e7 | |||
| cd885bf855 | |||
| ddfe457abf | |||
| f4d337d252 | |||
| cbfec5b9e9 | |||
| af3358d195 | |||
| 641358a905 | |||
| f2df220a1f | |||
| a2ca31bb19 | |||
| b09333db79 | |||
| ac98e7b177 | |||
| 14d1d4270f | |||
| 150e55aa00 | |||
| 2f0c96b618 | |||
| 6c2137a420 | |||
| e1878050b5 |
@@ -35,7 +35,7 @@
|
||||
"evil": true,
|
||||
"forin": false,
|
||||
"immed": false,
|
||||
"laxbreak": false,
|
||||
"laxbreak": true,
|
||||
"newcap": true,
|
||||
"noarg": true,
|
||||
"noempty": false,
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "handlebars",
|
||||
"version": "1.2.1",
|
||||
"version": "1.3.0",
|
||||
"main": "handlebars.js",
|
||||
"dependencies": {}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 + "'");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(", ");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Vendored
+15
-1
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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!");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user