From ebd6a3867234401c93af5388412b6eaa8dbd7ce1 Mon Sep 17 00:00:00 2001 From: Kevin Phillips Date: Tue, 26 Jul 2016 21:28:31 -0500 Subject: [PATCH] Adding ability to lookup values by index using `[]` --- src/expression.js | 62 ++++++++++++++-- src/mustache_core.js | 7 +- test/expression-test.js | 154 ++++++++++++++++++++++++++++++++++++++++ test/stache-test.js | 19 +++++ 4 files changed, 235 insertions(+), 7 deletions(-) diff --git a/src/expression.js b/src/expression.js index d03d3f2f..1eb8ef63 100644 --- a/src/expression.js +++ b/src/expression.js @@ -61,6 +61,40 @@ var getKeyComputeData = function (key, scope, readOptions) { // // These expression types return a value. They are assembled by `expression.parse`. +// ### Bracket +// For accessing properties using bracket notation like `foo[bar]` +var Bracket = function (key, root) { + this.root = root; + this.key = key; +}; +Bracket.prototype.value = function (scope) { + var prop = this.key; + var obj = this.root; + + if (prop instanceof Literal) { + prop = prop.value(); + } else if (prop instanceof Lookup) { + prop = lookupValue(prop.key, scope, {}, {}); + prop = prop.value(); + } else { + prop = prop.value(scope, {}, {})(); + } + + if (!obj) { + return lookupValue(prop, scope, {}, {}).value; + } else { + return compute(function() { + if (obj instanceof Lookup) { + obj = lookupValue(obj.key, scope, {}, {}).value(); + } else { + obj = obj.value(scope, {}, {})(); + } + + return obj[prop]; + }); + } +}; + // ### Literal // For inline static values like `{{"Hello World"}}` var Literal = function(value){ @@ -198,8 +232,6 @@ Call.prototype.value = function(scope, helperScope, helperOptions){ }; - - // ### HelperLookup // An expression that looks up a value in the helper or scope. // Any functions found prior to the last one are called with @@ -384,7 +416,7 @@ Helper.prototype.value = function(scope, helperOptions, nodeList, truthyRenderer // AT @NAME // var keyRegExp = /[\w\.\\\-_@\/\&%]+/, - tokensRegExp = /('.*?'|".*?"|=|[\w\.\\\-_@\/*%\$]+|[\(\)]|,|\~)/g, + tokensRegExp = /('.*?'|".*?"|=|[\w\.\\\-_@\/*%\$]+|[\(\)]|,|\~|\[|\])/g, literalRegExp = /^('.*?'|".*?"|[0-9]+\.?[0-9]*|true|false|null|undefined)$/; var isTokenKey = function(token){ @@ -529,7 +561,7 @@ var expression = { Helper: Helper, HelperLookup: HelperLookup, HelperScopeLookup: HelperScopeLookup, - + Bracket: Bracket, SetIdentifier: function(value){ this.value = value; }, tokenize: function(expression){ @@ -614,6 +646,11 @@ var expression = { return new (options.methodRule(ast))(this.hydrateAst(ast.method, options, ast.type), args, hashes); + } else if (ast.type === "Bracket") { + return new Bracket( + this.hydrateAst(ast.children[0], options), + ast.root ? this.hydrateAst(ast.root, options) : undefined + ); } }, ast: function(expression){ @@ -635,7 +672,7 @@ var expression = { // Literal if(literalRegExp.test( token )) { convertToHelperIfTopIsLookup(stack); - stack.addTo(["Helper", "Call", "Hash"], {type: "Literal", value: utils.jsonParse( token )}); + stack.addTo(["Helper", "Call", "Hash", "Bracket"], {type: "Literal", value: utils.jsonParse( token )}); } // Hash else if(nextToken === "=") { @@ -680,7 +717,7 @@ var expression = { // if two scopes, that means a helper convertToHelperIfTopIsLookup(stack); - stack.addToAndPush(["Helper", "Call","Hash","Arg"], {type: "Lookup", key: token}); + stack.addToAndPush(["Helper", "Call", "Hash", "Arg", "Bracket"], {type: "Lookup", key: token}); } } @@ -709,6 +746,19 @@ var expression = { else if(token === ",") { stack.popUntil(["Call"]); } + // Bracket + else if(token === "[") { + top = stack.top(); + lastToken = stack.topLastChild(); + + if (lastToken && lastToken.type === "Call") { + stack.replaceTopAndPush({type: "Bracket", root: lastToken}); + } else if (top.type === "Lookup") { + stack.replaceTopAndPush({type: "Bracket", root: top}); + } else { + stack.replaceTopAndPush({type: "Bracket"}); + } + } } return stack.root.children[0]; } diff --git a/src/mustache_core.js b/src/mustache_core.js index 5854e229..e691d189 100644 --- a/src/mustache_core.js +++ b/src/mustache_core.js @@ -107,6 +107,11 @@ var core = { if(exprData.isHelper) { return value; } + } else if (exprData instanceof expression.Bracket) { + value = exprData.value(scope); + if(exprData.isHelper) { + return value; + } } else { var readOptions = { // will return a function instead of calling it. @@ -307,7 +312,7 @@ var core = { // Pre-process the expression. var exprData = core.expression.parse(expressionString); - if(!(exprData instanceof expression.Helper) && !(exprData instanceof expression.Call)) { + if(!(exprData instanceof expression.Helper) && !(exprData instanceof expression.Call) && !(exprData instanceof expression.Bracket)) { exprData = new expression.Helper(exprData,[],{}); } // A branching renderer takes truthy and falsey renderer. diff --git a/test/expression-test.js b/test/expression-test.js index e2f3804b..5e572ebd 100644 --- a/test/expression-test.js +++ b/test/expression-test.js @@ -382,3 +382,157 @@ test("registerConverter helpers are chainable", function () { equal(data.attr("observeVal"), 127, 'push converter called'); }); +test("expression.ast - [] operator", function(){ + var ast = expression.ast("['propName']"); + + deepEqual(ast, { + type: "Bracket", + children: [{type: "Literal", value: "propName"}] + }); + + var ast2 = expression.ast("[propName]"); + + deepEqual(ast2, { + type: "Bracket", + children: [{type: "Lookup", key: "propName"}] + }); + + var ast3 = expression.ast("foo['bar']"); + + deepEqual(ast3, { + type: "Bracket", + root: {type: "Lookup", key: "foo"}, + children: [{type: "Literal", value: "bar"}] + }); + + var ast3 = expression.ast("foo[bar]"); + + deepEqual(ast3, { + type: "Bracket", + root: {type: "Lookup", key: "foo"}, + children: [{type: "Lookup", key: "bar"}] + }); + + var ast4 = expression.ast("foo()[bar]"); + + deepEqual(ast4, { + type: "Bracket", + root: {type: "Call", method: {key: "@foo", type: "Lookup" } }, + children: [{type: "Lookup", key: "bar"}] + }); +}); + +test("expression.parse - [] operator", function(){ + var exprData = expression.parse("['propName']"); + deepEqual(exprData, + new expression.Bracket( + new expression.Literal('propName') + ) + ); + + exprData = expression.parse("[propName]"); + deepEqual(exprData, + new expression.Bracket( + new expression.Lookup('propName') + ) + ); + + exprData = expression.parse("foo['bar']"); + deepEqual(exprData, + new expression.Bracket( + new expression.Literal('bar'), + new expression.Lookup('foo') + ) + ); + + exprData = expression.parse("foo[bar]"); + deepEqual(exprData, + new expression.Bracket( + new expression.Lookup('bar'), + new expression.Lookup('foo') + ) + ); + + exprData = expression.parse("foo()[bar]"); + deepEqual(exprData, + new expression.Bracket( + new expression.Lookup('bar'), + new expression.Call( new expression.Lookup('@foo'), [], {} ) + ) + ); +}); + +test("Bracket expression", function(){ + // ["bar"] + var expr = new expression.Bracket( + new expression.Literal("bar") + ); + var compute = expr.value( + new Scope( + new CanMap({bar: "name"}) + ) + ); + equal(compute(), "name"); + + // [bar] + expr = new expression.Bracket( + new expression.Lookup("bar") + ); + var compute = expr.value( + new Scope( + new CanMap({bar: "name", name: "Kevin"}) + ) + ); + equal(compute(), "Kevin"); + + // foo["bar"] + expr = new expression.Bracket( + new expression.Literal("bar"), + new expression.Lookup("foo") + ); + var compute = expr.value( + new Scope( + new CanMap({foo: {bar: "name"}}) + ) + ); + equal(compute(), "name"); + + // foo[bar] + expr = new expression.Bracket( + new expression.Lookup("bar"), + new expression.Lookup("foo") + ); + var compute = expr.value( + new Scope( + new CanMap({foo: {name: "Kevin"}, bar: "name"}) + ) + ); + equal(compute(), "Kevin"); + + // foo()[bar] + expr = new expression.Bracket( + new expression.Lookup("bar"), + new expression.Call( new expression.Lookup("@foo"), [], {} ) + ); + var compute = expr.value( + new Scope( + new CanMap({foo: function() { return {name: "Kevin"}; }, bar: "name"}) + ) + ); + equal(compute(), "Kevin"); + + // foo()[bar()] + expr = new expression.Bracket( + new expression.Call( new expression.Lookup("@bar"), [], {} ), + new expression.Call( new expression.Lookup("@foo"), [], {} ) + ); + var compute = expr.value( + new Scope( + new CanMap({ + foo: function() { return {name: "Kevin"}; }, + bar: function () { return "name"; } + }) + ) + ); + equal(compute(), "Kevin"); +}); diff --git a/test/stache-test.js b/test/stache-test.js index c38f316f..3b0cccf7 100644 --- a/test/stache-test.js +++ b/test/stache-test.js @@ -4960,6 +4960,25 @@ function makeTest(name, doc, mutation) { QUnit.equal( lis[1].innerHTML, "2", "is 2"); }); + test("Bracket expression", function () { + var template; + var div = doc.createElement('div'); + + template = stache("

{{ foo[bar] }}

"); + + var data = new CanMap({ + bar: "name", + foo: { + name: "Kevin" + } + }); + var dom = template(data); + div.appendChild(dom); + var p = div.getElementsByTagName('p'); + + equal(innerHTML(p[0]), 'Kevin', 'correct value for foo[bar]'); + }); + // PUT NEW TESTS RIGHT BEFORE THIS! }