feat(parse): add support for local vars in expressions

This commit is contained in:
Misko Hevery 2012-02-06 21:56:05 -08:00
parent c8ee631c19
commit 761b2ed85a
2 changed files with 88 additions and 65 deletions

View file

@ -1,29 +1,29 @@
'use strict'; 'use strict';
var OPERATORS = { var OPERATORS = {
'null':function(self){return null;}, 'null':function(){return null;},
'true':function(self){return true;}, 'true':function(){return true;},
'false':function(self){return false;}, 'false':function(){return false;},
undefined:noop, undefined:noop,
'+':function(self, a,b){a=a(self); b=b(self); return (isDefined(a)?a:0)+(isDefined(b)?b:0);}, '+':function(self, locals, a,b){a=a(self, locals); b=b(self, locals); return (isDefined(a)?a:0)+(isDefined(b)?b:0);},
'-':function(self, a,b){a=a(self); b=b(self); return (isDefined(a)?a:0)-(isDefined(b)?b:0);}, '-':function(self, locals, a,b){a=a(self, locals); b=b(self, locals); return (isDefined(a)?a:0)-(isDefined(b)?b:0);},
'*':function(self, a,b){return a(self)*b(self);}, '*':function(self, locals, a,b){return a(self, locals)*b(self, locals);},
'/':function(self, a,b){return a(self)/b(self);}, '/':function(self, locals, a,b){return a(self, locals)/b(self, locals);},
'%':function(self, a,b){return a(self)%b(self);}, '%':function(self, locals, a,b){return a(self, locals)%b(self, locals);},
'^':function(self, a,b){return a(self)^b(self);}, '^':function(self, locals, a,b){return a(self, locals)^b(self, locals);},
'=':noop, '=':noop,
'==':function(self, a,b){return a(self)==b(self);}, '==':function(self, locals, a,b){return a(self, locals)==b(self, locals);},
'!=':function(self, a,b){return a(self)!=b(self);}, '!=':function(self, locals, a,b){return a(self, locals)!=b(self, locals);},
'<':function(self, a,b){return a(self)<b(self);}, '<':function(self, locals, a,b){return a(self, locals)<b(self, locals);},
'>':function(self, a,b){return a(self)>b(self);}, '>':function(self, locals, a,b){return a(self, locals)>b(self, locals);},
'<=':function(self, a,b){return a(self)<=b(self);}, '<=':function(self, locals, a,b){return a(self, locals)<=b(self, locals);},
'>=':function(self, a,b){return a(self)>=b(self);}, '>=':function(self, locals, a,b){return a(self, locals)>=b(self, locals);},
'&&':function(self, a,b){return a(self)&&b(self);}, '&&':function(self, locals, a,b){return a(self, locals)&&b(self, locals);},
'||':function(self, a,b){return a(self)||b(self);}, '||':function(self, locals, a,b){return a(self, locals)||b(self, locals);},
'&':function(self, a,b){return a(self)&b(self);}, '&':function(self, locals, a,b){return a(self, locals)&b(self, locals);},
// '|':function(self, a,b){return a|b;}, // '|':function(self, locals, a,b){return a|b;},
'|':function(self, a,b){return b(self)(self, a(self));}, '|':function(self, locals, a,b){return b(self, locals)(self, locals, a(self, locals));},
'!':function(self, a){return !a(self);} '!':function(self, locals, a){return !a(self, locals);}
}; };
var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'};
@ -146,7 +146,7 @@ function lex(text){
function readIdent() { function readIdent() {
var ident = "", var ident = "",
start = index, start = index,
fn, lastDot, peekIndex, methodName; fn, lastDot, peekIndex, methodName, getter;
while (index < text.length) { while (index < text.length) {
var ch = text.charAt(index); var ch = text.charAt(index);
@ -179,15 +179,21 @@ function lex(text){
} }
fn = OPERATORS[ident]; fn = OPERATORS[ident];
getter = getterFn(ident);
tokens.push({ tokens.push({
index:start, index:start,
text:ident, text:ident,
json: fn, json: fn,
fn:fn||extend(getterFn(ident), { fn:fn||extend(
assign:function(self, value){ function(self, locals) {
return setter(self, ident, value); return (getter(self, locals));
} },
}) {
assign:function(self, value){
return setter(self, ident, value);
}
}
)
}); });
if (methodName) { if (methodName) {
@ -257,12 +263,10 @@ function parser(text, json, $filter){
value, value,
tokens = lex(text), tokens = lex(text),
assignment = _assignment, assignment = _assignment,
assignable = logicalOR,
functionCall = _functionCall, functionCall = _functionCall,
fieldAccess = _fieldAccess, fieldAccess = _fieldAccess,
objectIndex = _objectIndex, objectIndex = _objectIndex,
filterChain = _filterChain, filterChain = _filterChain
functionIdent = _functionIdent;
if(json){ if(json){
// The extra level of aliasing is here, just in case the lexer misses something, so that // The extra level of aliasing is here, just in case the lexer misses something, so that
// we prevent any accidental execution in JSON. // we prevent any accidental execution in JSON.
@ -270,9 +274,7 @@ function parser(text, json, $filter){
functionCall = functionCall =
fieldAccess = fieldAccess =
objectIndex = objectIndex =
assignable =
filterChain = filterChain =
functionIdent =
function() { throwError("is not valid json", {text:text, index:0}); }; function() { throwError("is not valid json", {text:text, index:0}); };
value = primary(); value = primary();
} else { } else {
@ -328,14 +330,14 @@ function parser(text, json, $filter){
} }
function unaryFn(fn, right) { function unaryFn(fn, right) {
return function(self) { return function(self, locals) {
return fn(self, right); return fn(self, locals, right);
}; };
} }
function binaryFn(left, fn, right) { function binaryFn(left, fn, right) {
return function(self) { return function(self, locals) {
return fn(self, left, right); return fn(self, locals, left, right);
}; };
} }
@ -353,12 +355,12 @@ function parser(text, json, $filter){
// TODO(size): maybe we should not support multiple statements? // TODO(size): maybe we should not support multiple statements?
return statements.length == 1 return statements.length == 1
? statements[0] ? statements[0]
: function(self){ : function(self, locals){
var value; var value;
for ( var i = 0; i < statements.length; i++) { for ( var i = 0; i < statements.length; i++) {
var statement = statements[i]; var statement = statements[i];
if (statement) if (statement)
value = statement(self); value = statement(self, locals);
} }
return value; return value;
}; };
@ -386,10 +388,10 @@ function parser(text, json, $filter){
if ((token = expect(':'))) { if ((token = expect(':'))) {
argsFn.push(expression()); argsFn.push(expression());
} else { } else {
var fnInvoke = function(self, input){ var fnInvoke = function(self, locals, input){
var args = [input]; var args = [input];
for ( var i = 0; i < argsFn.length; i++) { for ( var i = 0; i < argsFn.length; i++) {
args.push(argsFn[i](self)); args.push(argsFn[i](self, locals));
} }
return fn.apply(self, args); return fn.apply(self, args);
}; };
@ -414,8 +416,8 @@ function parser(text, json, $filter){
text.substring(0, token.index) + "] can not be assigned to", token); text.substring(0, token.index) + "] can not be assigned to", token);
} }
right = logicalOR(); right = logicalOR();
return function(self){ return function(self, locals){
return left.assign(self, right(self)); return left.assign(self, right(self, locals), locals);
}; };
} else { } else {
return left; return left;
@ -546,22 +548,25 @@ function parser(text, json, $filter){
function _fieldAccess(object) { function _fieldAccess(object) {
var field = expect().text; var field = expect().text;
var getter = getterFn(field); var getter = getterFn(field);
return extend(function(self){ return extend(
return getter(object(self)); function(self, locals) {
}, { return getter(object(self, locals), locals);
assign:function(self, value){ },
return setter(object(self), field, value); {
} assign:function(self, value, locals) {
}); return setter(object(self, locals), field, value);
}
}
);
} }
function _objectIndex(obj) { function _objectIndex(obj) {
var indexFn = expression(); var indexFn = expression();
consume(']'); consume(']');
return extend( return extend(
function(self){ function(self, locals){
var o = obj(self), var o = obj(self, locals),
i = indexFn(self), i = indexFn(self, locals),
v, p; v, p;
if (!o) return undefined; if (!o) return undefined;
@ -576,8 +581,8 @@ function parser(text, json, $filter){
} }
return v; return v;
}, { }, {
assign:function(self, value){ assign:function(self, value, locals){
return obj(self)[indexFn(self)] = value; return obj(self, locals)[indexFn(self, locals)] = value;
} }
}); });
} }
@ -590,14 +595,14 @@ function parser(text, json, $filter){
} while (expect(',')); } while (expect(','));
} }
consume(')'); consume(')');
return function(self){ return function(self, locals){
var args = [], var args = [],
context = contextGetter ? contextGetter(self) : self; context = contextGetter ? contextGetter(self, locals) : self;
for ( var i = 0; i < argsFn.length; i++) { for ( var i = 0; i < argsFn.length; i++) {
args.push(argsFn[i](self)); args.push(argsFn[i](self, locals));
} }
var fnPtr = fn(self) || noop; var fnPtr = fn(self, locals) || noop;
// IE stupidity! // IE stupidity!
return fnPtr.apply return fnPtr.apply
? fnPtr.apply(context, args) ? fnPtr.apply(context, args)
@ -614,10 +619,10 @@ function parser(text, json, $filter){
} while (expect(',')); } while (expect(','));
} }
consume(']'); consume(']');
return function(self){ return function(self, locals){
var array = []; var array = [];
for ( var i = 0; i < elementFns.length; i++) { for ( var i = 0; i < elementFns.length; i++) {
array.push(elementFns[i](self)); array.push(elementFns[i](self, locals));
} }
return array; return array;
}; };
@ -635,11 +640,11 @@ function parser(text, json, $filter){
} while (expect(',')); } while (expect(','));
} }
consume('}'); consume('}');
return function(self){ return function(self, locals){
var object = {}; var object = {};
for ( var i = 0; i < keyValues.length; i++) { for ( var i = 0; i < keyValues.length; i++) {
var keyValue = keyValues[i]; var keyValue = keyValues[i];
var value = keyValue.value(self); var value = keyValue.value(self, locals);
object[keyValue.key] = value; object[keyValue.key] = value;
} }
return object; return object;
@ -700,10 +705,14 @@ function getterFn(path) {
if (fn) return fn; if (fn) return fn;
var code = 'var l, fn, p;\n'; var code = 'var l, fn, p;\n';
forEach(path.split('.'), function(key) { forEach(path.split('.'), function(key, index) {
code += 'if(!s) return s;\n' + code += 'if(!s) return s;\n' +
'l=s;\n' + 'l=s;\n' +
's=s' + '["' + key + '"]' + ';\n' + 's='+ (index
// we simply direference 's' on any .dot notation
? 's'
// but if we are first then we check locals firs, and if so read it first
: '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n' +
'if (s && s.then) {\n' + 'if (s && s.then) {\n' +
' if (!("$$v" in s)) {\n' + ' if (!("$$v" in s)) {\n' +
' p=s;\n' + ' p=s;\n' +
@ -714,7 +723,7 @@ function getterFn(path) {
'}\n'; '}\n';
}); });
code += 'return s;'; code += 'return s;';
fn = Function('s', code); fn = Function('s', 'k', code);
fn.toString = function() { return code; }; fn.toString = function() { return code; };
return getterFnCache[path] = fn; return getterFnCache[path] = fn;

View file

@ -603,4 +603,18 @@ describe('parser', function() {
expect(scope).toEqual({a:123}); expect(scope).toEqual({a:123});
})); }));
}); });
describe('locals', function() {
it('should expose local variables', inject(function($parse) {
expect($parse('a')({a: 0}, {a: 1})).toEqual(1);
expect($parse('add(a,b)')({b: 1, add: function(a, b) { return a + b; }}, {a: 2})).toEqual(3);
}));
it('should expose traverse locals', inject(function($parse) {
expect($parse('a.b')({a: {b: 0}}, {a: {b:1}})).toEqual(1);
expect($parse('a.b')({a: null}, {a: {b:1}})).toEqual(1);
expect($parse('a.b')({a: {b: 0}}, {a: null})).toEqual(undefined);
}));
});
}); });