Does someone know a method to extract paths from a javascript expression for use in a PathObserver like in https://github.com/polymer/observe-js ?
Polymer defines a path by
an ECMAScript expression consisting only of identifiers (myVal),
member accesses (foo.bar) and key lookup with literal values
(arr[0] obj['str-value'].bar.baz).
My aim is to observe all the paths that might affect the result of the expression (low 'observe' the expression). I was hoping for a simple regex, but code using js parsers like Esprima or Shift is fine too.
So a practical example: input is
'if (count(body.arms)/numlegs==1) head[0]=eyes[symmetry]'
and output would be
["body.arms","numlegs","head[0]","eyes","symmetry"]
Fast is better than perfect; the PathObserver would eventually tell me if the path doesn't validate.
I'm using this now. It may not be complete, and it may not be optimally fast, but it does what I wanted: take an expression and return observable paths for Observe.js (https://github.com/polymer/observe-js) . It uses esprima (http://esprima.org) to parse the expression.
Parser = {
// requires esprima.
// @see http://esprima.org/demo/parse.html
outerscope : 'window',
getObservablePaths : function(expression) {
console.log('Parser.getPaths',expression);
var ast = esprima.parse(expression);
if (ast) {
console.log('Parser.getPaths',ast);
var paths = new Array();
this.recurseObservablePaths(ast,paths);
return paths;
} else return false;
},
recurseObservablePaths : function(tree,paths,path) {
if (!tree || !paths) return false;
if (tree.type =='Identifier') {
// some sort of global
console.log('Parser.recurseObservablePaths','adding identifier '+tree.name);
paths.push({object:this.outerscope,path:tree.name});
} else if (tree.type =='MemberExpression') {
// member expression
if (tree.property.type=='Identifier' || tree.property.type=='Literal') {
// like foo[bar][24].quz ; the property is 'quz'
// dabble down the object to get the path
if (tree.property.type=='Identifier') {
path = (path)?'.'+tree.property.name+path:'.'+tree.property.name;
} else {
path = (path)?'['+tree.property.raw+']'+path:'['+tree.property.raw+']';
}
if (tree.object.type=='Identifier') {
// like foo.bar ; were done with this path - push !
console.log('Parser.recurseObservablePaths','adding path '+tree.object.name+path);
if (path.indexOf('.')===0) {
paths.push({object:tree.object.name,path:path.substring(1)});
} else {
paths.push({object:this.outerscope,path:tree.object.name+path});
}
} else {
if (tree.object.type=='MemberExpression') {
// like foo.bar.quz ; recurse the object
console.log('Parser.recurseObservablePaths','recursing member expression ..');
this.recurseObservablePaths(tree.object,paths,path);
} else {
// like foo(bar).quz ; the object is something weird.
// ignore the property .. but recurse the object
this.recurseObservablePaths(tree.object,paths);
}
}
} else {
// the property is some sort of thing itself:
if (tree.object.type=='Identifier') {
// like foo[bar.quz] - push the object, recurse the property
console.log('Parser.recurseObservablePaths','adding identifier '+tree.object.name);
paths.push({object:this.outerscope,path:tree.object.name});
this.recurseObservablePaths(tree.property);
} else {
// like foo.bar[quz(raz)] ; recurse both
console.log('Parser.recurseObservablePaths','recursing member expression ..');
this.recurseObservablePaths(tree.object,paths);
this.recurseObservablePaths(tree.property,paths);
}
}
} else if (tree.type=="CallExpression") {
// like foo.bar(quz.baz) ; we only want the arguments
this.recurseObservablePaths(tree.arguments,paths);
} else if (tree.type=="AssignmentExpression") {
// like foo.bar=baz*quz ; we only want the right hand
this.recurseObservablePaths(tree.right,paths);
} else {
// unknown garbage. dig deeper.
var props = Object.getOwnPropertyNames(tree);
for (var pc=0; pc<props.length; pc++) {
var key = props[pc];
if (typeof tree[key] == 'object') {
if (Array.isArray(tree[key])) {
for (var kc=0;kc<tree[key].length;kc++) {
console.log('Parser.recurseObservablePaths','recursing '+key+':'+kc);
this.recurseObservablePaths(tree[key][kc],paths);
}
} else {
console.log('Parser.recurseObservablePaths','recursing '+key);
this.recurseObservablePaths(tree[key],paths);
}
} else {
console.log('Parser.recurseObservablePaths','ignoring '+key);
}
}
}
}
}
Try it ..
Parser.getObservablePaths('alert(life.and[42].the); universe=everything.else')
[{"object":"life","path":"and[42]"},{"object":"everything","path":"else"}]
Parser.getObservablePaths('if (count(body.arms)/numlegs==1) head[0]=eyes[symmetry]')
[{"object":"body","path":"arms"},{"object":"window","path":"numlegs"},{"object":"eyes","path":"symmetry"}]
Any suggestions are more than welcome !