Adding ol.Expression

This simple expression constructor will be used for symbolizer properties and the layer will generate symbolizer literals for rendering by evaluating any expressions with a feature as the this argument and feature attributes as the scope.  This allows generating labels that concatenate multiple attribute values together or displaying point symbols that are sized according to a population attribute divided by an area attribute, for example.

This implementation will not work in environments where the content security policy disallows the use of the Function constructor.  This is the case on browser extensions.  A more content-security-policy-friendly implementation would be to come up with a restricted grammar and write a lex/parser.  This is the road I started down, but this verison is far less code, and I think the security policy limitations are minor at this point.  This version will always be faster/lighter than a parser, so one is written in the future, it should only be pulled in where content security policy mandates it.
This commit is contained in:
Tim Schaub
2013-02-14 17:19:15 -07:00
parent 59a203b2b7
commit be255ed6c7
2 changed files with 121 additions and 0 deletions

43
src/ol/expression.js Normal file
View File

@@ -0,0 +1,43 @@
goog.provide('ol.Expression');
/**
* @constructor
* @param {string} source Expression to be evaluated.
*/
ol.Expression = function(source) {
/**
* @type {string}
* @private
*/
this.source_ = source;
};
/**
* Evaluate the expression and return the result.
*
* @param {Object=} opt_thisArg Object to use as this when evaluating the
* expression. If not provided, the global object will be used.
* @param {Object=} opt_scope Evaluation scope. All properties of this object
* will be available as variables when evaluating the expression. If not
* provided, the global object will be used.
* @return {*} Result of the expression.
*/
ol.Expression.prototype.evaluate = function(opt_thisArg, opt_scope) {
var thisArg = goog.isDef(opt_thisArg) ? opt_thisArg : goog.global,
scope = goog.isDef(opt_scope) ? opt_scope : goog.global,
names = [],
values = [];
for (var name in scope) {
names.push(name);
values.push(scope[name]);
}
var evaluator = new Function(names.join(','), 'return ' + this.source_);
return evaluator.apply(thisArg, values);
};

View File

@@ -0,0 +1,78 @@
goog.provide('ol.test.Expression');
describe('ol.Expression', function() {
describe('constructor', function() {
it('creates an expression', function() {
var exp = new ol.Expression('foo');
expect(exp).toBeA(ol.Expression);
});
});
describe('#evaluate()', function() {
it('evaluates and returns the result', function() {
// test cases here with unique values only (lack of messages in expect)
var cases = [{
source: '42', result: 42
}, {
source: '10 + 10', result: 20
}, {
source: '"a" + "b"', result: 'ab'
}, {
source: 'Math.floor(Math.PI)', result: 3
}, {
source: 'ol', result: ol
}, {
source: 'this', result: goog.global
}];
var c, exp;
for (var i = 0, ii = cases.length; i < ii; ++i) {
c = cases[i];
exp = new ol.Expression(c.source);
expect(exp.evaluate()).toBe(c.result);
}
});
it('accepts an optional this argument', function() {
function Thing() {
this.works = true;
};
var exp = new ol.Expression('this.works ? "yes" : "no"');
expect(exp.evaluate(new Thing())).toBe('yes');
expect(exp.evaluate({})).toBe('no');
});
it('accepts an optional scope argument', function() {
var exp;
var scope = {
greeting: 'hello world',
punctuation: '!',
pick: function(array, index) {
return array[index];
}
};
// access two members in the scope
exp = new ol.Expression('greeting + punctuation');
expect(exp.evaluate({}, scope)).toBe('hello world!');
// call a function in the scope
exp = new ol.Expression(
'pick([10, 42, "chicken"], 2) + Math.floor(Math.PI)');
expect(exp.evaluate({}, scope)).toBe('chicken3');
});
it('throws on error', function() {
var exp = new ol.Expression('@*)$(&');
expect(function() {exp.evaluate()}).toThrow();
});
});
});
goog.require('ol.Expression');