Javascript Expression Language: Powerful context-based expression parser and evaluator
npm install mozjexlmozJexl is a fork of Jexl for use at Mozilla, specifically as a part
of SHIELD and Normandy.
---
align="right" valign="top" alt="Promises/A+ logo" />
``javascript
var context = {
name: {first: 'Sterling', last: 'Archer'},
assoc: [
{first: 'Lana', last: 'Kane'},
{first: 'Cyril', last: 'Figgis'},
{first: 'Pam', last: 'Poovey'}
],
age: 36
};
// Filter an array
jexl.eval('assoc[.first == "Lana"].last', context).then(function(res) {
console.log(res); // Output: Kane
});
// Do math
jexl.eval('age * (3 - 1)', context, function(err, res) {
console.log(res); // Output: 72
});
// Concatenate
jexl.eval('name.first + " " + name["la" + "st"]', context).then(function(res) {
console.log(res); // Output: Sterling Archer
});
// Compound
jexl.eval('assoc[.last == "Figgis"].first == "Cyril" && assoc[.last == "Poovey"].first == "Pam"', context)
.then(function(res) {
console.log(res); // Output: true
});
// Use array indexes
jexl.eval('assoc[1]', context, function(err, res) {
console.log(res.first + ' ' + res.last); // Output: Cyril Figgis
});
// Use conditional logic
jexl.eval('age > 62 ? "retired" : "working"', context).then(function(res) {
console.log(res); // Output: working
});
// Transform
jexl.addTransform('upper', function(val) {
return val.toUpperCase();
});
jexl.eval('"duchess"|upper + " " + name.last|upper', context).then(function(res) {
console.log(res); // Output: DUCHESS ARCHER
});
// Transform asynchronously, with arguments
jexl.addTransform('getStat', function(val, stat) {
return dbSelectByLastName(val, stat); // Returns a promise
});
jexl.eval('name.last|getStat("weight")', context, function(err, res) {
if (err) console.log('Database Error', err.stack);
else console.log(res); // Output: 184
});
// Add your own (a)synchronous operators
// Here's a case-insensitive string equality
jexl.addBinaryOp('_=', 20, function(left, right) {
return left.toLowerCase() === right.toLowerCase();
});
jexl.eval('"Guest" _= "gUeSt"').then(function(val) {
console.log(res); // Output: true
});
`
For Node.js, type this in your project folder:
npm install jexl --save
For the frontend, drop dist/jexl.min.js into your project and include it on
your page with:
Access Jexl the same way, backend or front:
var jexl = require('Jexl');
| Operation | Symbol |
|-----------|:------:|
| Negate | ! |
| Operation | Symbol |
|------------------|:----------------:|
| Add, Concat | + |
| Subtract | - |
| Multiply | * |
| Divide | / |
| Divide and floor | // |
| Modulus | % |
| Power of | ^ |
| Logical AND | && |
| Logical OR | || |
| Comparison | Symbol |
|----------------------------|:------:|
| Equal | == |
| Not equal | != |
| Greater than | > |
| Greater than or equal | >= |
| Less than | < |
| Less than or equal | <= |
| Element in array or string | in |
#### A note about in:in
The operator can be used to check for a substring:"Cad" in "Ron Cadillac", and it can be used to check for an array element:"coarse" in ['fine', 'medium', 'coarse']. However, the == operator is used{a: 'b'} in [{a: 'b'}]
behind-the-scenes to search arrays, so it should not be used with arrays of
objects. The following expression returns false: .
Conditional expressions check to see if the first segment evaluates to a truthy
value. If so, the consequent segment is evaluated. Otherwise, the alternate
is. If the consequent section is missing, the test result itself will be used
instead.
| Expression | Result |
|-----------------------------------|--------|
| "" ? "Full" : "Empty" | Empty |
| "foo" in "foobar" ? "Yes" : "No" | Yes |
| {agent: "Archer"}.agent ?: "Kane" | Archer |
| Type | Examples |
|----------|:------------------------------:|
| Booleans | true, false |
| Strings | "Hello \"user\"", 'Hey there!' |
| Numerics | 6, -7.2, 5, -3.14159 |
| Objects | {hello: "world!"} |
| Arrays | ['hello', 'world!'] |
Parentheses work just how you'd expect them to:
| Expression | Result |
|-------------------------------------|:-------|
| (83 + 1) / 2 | 42 |
| 1 < 3 && (4 > 2 || 2 > 4) | true |
Access variables in the context object by just typing their name. Objects can
be traversed with dot notation, or by using brackets to traverse to a dynamic
property name.
Example context:
`javascript`
{
name: {
first: "Malory",
last: "Archer"
},
exes: [
"Nikolai Jakov",
"Len Trexler",
"Burt Reynolds"
],
lastEx: 2
}
| Expression | Result |
|-------------------|---------------|
| name.first | Malory |
| name['la' + 'st'] | Archer |
| exes[2] | Burt Reynolds |
| exes[lastEx - 1] | Len Trexler |
Collections, or arrays of objects, can be filtered by including a filter
expression in brackets. Properties of each collection can be referenced by
prefixing them with a leading dot. The result will be an array of the objects
for which the filter expression resulted in a truthy value.
Example context:
`javascript`
{
employees: [
{first: 'Sterling', last: 'Archer', age: 36},
{first: 'Malory', last: 'Archer', age: 75},
{first: 'Lana', last: 'Kane', age: 33},
{first: 'Cyril', last: 'Figgis', age: 45},
{first: 'Cheryl', last: 'Tunt', age: 28}
],
retireAge: 62
}
| Expression | Result |
|-----------------------------------------------|---------------------------------------------------------------------------------------|
| employees[.first == 'Sterling'] | [{first: 'Sterling', last: 'Archer', age: 36}] |
| employees[.last == 'Tu' + 'nt'].first | Cheryl |
| employees[.age >= 30 && .age < 40] | [{first: 'Sterling', last: 'Archer', age: 36},{first: 'Lana', last: 'Kane', age: 33}] |
| employees[.age >= 30 && .age < 40][.age < 35] | [{first: 'Lana', last: 'Kane', age: 33}] |
| employees[.age >= retireAge].first | Malory |
The power of Jexl is in transforming data, synchronously or asynchronously.
Transform functions take one or more arguments: The value to be transformed,
followed by anything else passed to it in the expression. They must return
either the transformed value, or a Promise that resolves with the transformed
value. Add them with jexl.addTransform(name, function).
`javascript`
jexl.addTransform('split', function(val, char) {
return val.split(char);
});
jexl.addTransform('lower', function(val) {
return val.toLowerCase();
});
| Expression | Result |
|--------------------------------------------|-----------------------|
| "Pam Poovey"|lower|split(' ')[1] | poovey |
| "password==guest"|split('=' + '=') | ['password', 'guest'] |
#### Advanced Transforms
Using Transforms, Jexl can support additional string formats like embedded
JSON, YAML, XML, and more. The following, with the help of the
xml2json module, allows XML to be
traversed just as easily as plain javascript objects:
`javascript
var xml2json = require('xml2json');
jexl.addTransform('xml', function(val) {
return xml2json.toJson(val, {object: true});
});
var context = {
xmlDoc:
"
"
"
"
"
"
"
"
"
"
};
var expr = 'xmlDoc|xml.Employees.Employee[.LastName == "Figgis"].FirstName';
jexl.eval(expr, context).then(function(res) {
console.log(res); // Output: Cyril
});
`
Variable contexts are straightforward Javascript objects that can be accessed
in the expression, but they have a hidden feature: they can include a Promise
object, and when that property is used, Jexl will wait for the Promise to
resolve and use that value!
#### jexl.Jexl
A reference to the Jexl constructor. To maintain separate instances of Jexl
with each maintaining its own set of transforms, simply re-instantiate with
new jexl.Jexl().
#### jexl.addBinaryOp(_{string} operator_, _{number} precedence_, _{function} fn_)
Adds a binary operator to the Jexl instance. A binary operator is one that
considers the values on both its left and right, such as "+" or "==", in order
to calculate a result. The precedence determines the operator's position in the
order of operations (please refer to lib/grammar.js to see the precedence of
existing operators). The provided function will be called with two arguments:
a left value and a right value. It should return either the resulting value,
or a Promise that resolves to the resulting value.
#### jexl.addUnaryOp(_{string} operator_, _{function} fn_)
Adds a unary operator to the Jexl instance. A unary operator is one that
considers only the value on its right, such as "!", in order to calculate a
result. The provided function will be called with one argument: the value to
the operator's right. It should return either the resulting value, or a Promise
that resolves to the resulting value.
#### jexl.addTransform(_{string} name_, _{function} transform_)
Adds a transform function to this Jexl instance. See the Transforms
section above for information on the structure of a transform function.
#### jexl.addTransforms(_{{}} map_)
Adds multiple transforms from a supplied map of transform name to transform
function.
#### jexl.getTransform(_{string} name_)
Returns {function|undefined}. Gets a previously set transform function,
or undefined if no function of that name exists.
#### jexl.eval(_{string} expression_, _{{}} [context]_, _{function} [callback]_)
Returns {Promise<*>}. Evaluates an expression. The context map and
callback function are optional. If a callback is specified, it will be called
with the standard signature of {Error} first argument, and the expression's.catch()
result in the second argument. Note that if a callback function is supplied,
the returned Promise will already have a attached to it.
#### jexl.removeOp(_{string} operator_)
Removes a binary or unary operator from the Jexl instance. For example, "^" can
be passed to eliminate the "power of" operator.