Skip to content

Commit 53f5514

Browse files
author
Sander Toonen
committed
Rearrange documentation and document language server
1 parent 562f025 commit 53f5514

8 files changed

Lines changed: 775 additions & 740 deletions

File tree

README.md

Lines changed: 46 additions & 740 deletions
Large diffs are not rendered by default.

docs/enhancements.md

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
# TypeScript Port Enhancements
2+
3+
This is a modern TypeScript port of the expr-eval library, completely rewritten with contemporary build tools and development practices. Originally based on [expr-eval 2.0.2](http://silentmatt.com/javascript-expression-evaluator/), this version has been restructured with a modular architecture, TypeScript support, and comprehensive testing using Vitest. The library almost maintains backward compatibility while providing enhanced features and improved maintainability.
4+
5+
This port adds the following enhancements over the original:
6+
7+
## Support for json() function
8+
9+
This will return a JSON string:
10+
11+
```js
12+
json([1, 2, 3])
13+
```
14+
15+
## Support for undefined
16+
17+
The concept of JavaScript's undefined has been added to the parser.
18+
19+
### undefined keyword
20+
21+
The undefined keyword has been added to the parser allowing it to be used in expressions.
22+
23+
```js
24+
x > 3 ? undefined : x
25+
x == undefined ? 1 : 2
26+
```
27+
28+
### Setting expression variables to undefined
29+
30+
If you set a local variable to undefined, expr-eval would generate an error saying that your variable was unrecognized.
31+
32+
For example:
33+
34+
```js
35+
/* myCustomFn() returns undefined */
36+
x = myCustomFn(); x > 3
37+
/* Error: unrecognized variable: x */
38+
```
39+
40+
This has been fixed, you can now set expression variables to undefined and they will resolve correctly.
41+
42+
### Operators/functions gracefully support undefined
43+
44+
All operators and built-in functions have been extended to gracefully support undefined. Generally speaking if one of the input values is undefined then the operator/function returns undefined. So `2 + undefined` is `undefined`, `max(0, 1, undefined)` is `undefined`, etc.
45+
46+
Logical operators act just like JavaScript, so `3 > undefined` is `false`.
47+
48+
## Coalesce Operator
49+
50+
The coalesce operator `??` has been added; `x ?? y` will evaluate to y if x is:
51+
52+
* `undefined`
53+
* `null`
54+
* Infinity (divide by zero)
55+
* NaN
56+
57+
Examples:
58+
59+
```js
60+
var parser = new Parser();
61+
var obj = { x: undefined, y: 10, z: 0 };
62+
parser.evaluate('x ?? 0', obj); // 0
63+
parser.evaluate('y ?? 0', obj); // 10
64+
parser.evaluate('x ?? 1 * 3', obj); // (undefined ?? 1) * 3 = 3
65+
parser.evaluate('y ?? 1 * 3', obj); // (10 ?? 1) * 3 = 30
66+
parser.evaluate('10 / z', obj); // Infinity
67+
parser.evaluate('10 / z ?? 0', obj); // 0
68+
parser.evaluate('sqrt -1'); // NaN
69+
parser.evaluate('sqrt -1 ?? 0'); // 0
70+
```
71+
72+
## Not In Operator
73+
74+
The `not in` operator has been added.
75+
76+
`"a" not in ["a", "b", "c"]`
77+
78+
is equivalent to
79+
80+
`not ("a" in ["a", "b", "c"])`
81+
82+
## Optional Chaining for Property Access
83+
84+
Structure/array property references now act like `?.`, meaning if the entire property chain does not exist then instead of throwing an error the value of the property is undefined.
85+
86+
For example:
87+
88+
```js
89+
var parser = new Parser();
90+
var obj = { thingy: { array: [{ value: 10 }] } };
91+
parser.evaluate('thingy.array[0].value', obj); // 10
92+
parser.evaluate('thingy.array[1].value', obj); // undefined
93+
parser.evaluate('thingy.doesNotExist[0].childArray[1].notHere.alsoNotHere', obj); // undefined
94+
parser.evaluate('thingy.array[0].value.doesNotExist', obj); // undefined
95+
```
96+
97+
This can be combined with the coalesce operator to gracefully fall back on a default value if some part of a long property reference is `undefined`.
98+
99+
```js
100+
var parser = new Parser();
101+
var obj = { thingy: { array: [{ value: 10 }] } };
102+
parser.evaluate('thingy.array[1].value ?? 0', obj); // 0
103+
```
104+
105+
## String Concatenation Using +
106+
107+
The + operator can now be used to concatenate strings.
108+
109+
```js
110+
var parser = new Parser();
111+
var obj = { thingy: { array: [{ value: 10 }] } };
112+
parser.evaluate('"abc" + "def" + "ghi"', obj); // 'abcdefghi'
113+
```
114+
115+
## Support for Promises in Custom Functions
116+
117+
Custom functions can return promises. When this happens evaluate will return a promise that when resolved contains the expression value.
118+
119+
```js
120+
const parser = new Parser();
121+
122+
parser.functions.doIt = value => value + value;
123+
parser.evaluate('doIt(2) + 3'); // 7
124+
125+
parser.functions.doIt = value =>
126+
new Promise((resolve) => setTimeout(() => resolve(value + value), 100));
127+
await parser.evaluate('doIt(2) + 3'); // 7
128+
```
129+
130+
## Support for Custom Variable Name Resolution
131+
132+
Custom logic can be provided to resolve unrecognized variable names. The parser has a resolve callback that will be called any time a variable name is not recognized. This can return an object that either indicates that the variable name is an alias for another variable or it can return the variable value.
133+
134+
```js
135+
const parser = new Parser();
136+
const obj = { variables: { a: 5, b: 1 } };
137+
parser.resolve = token => token === '$v' ? { alias: 'variables' } : undefined;
138+
parser.evaluate('$v.a + variables.b', obj); // 6
139+
140+
parser.resolve = token =>
141+
token.startsWith('$') ? { value: obj.variables[token.substring(1)] } : undefined;
142+
assert.strictEqual(parser.evaluate('$a + $b'), 6);
143+
```
144+
145+
## SQL Style Case Blocks
146+
147+
> **NOTE:** `toJSFunction()` is not supported for expressions that use case blocks.
148+
149+
SQL style case blocks are now supported, for both cases which evaluate a value against other values (a switch style case) and cases which test for the first truthy when (if/else/if style cases).
150+
151+
### Switch-style case
152+
153+
```js
154+
const parser = new Parser();
155+
const expr = `
156+
case x
157+
when 1 then 'one'
158+
when 1+1 then 'two'
159+
when 1+1+1 then 'three'
160+
else 'too-big'
161+
end
162+
`;
163+
parser.evaluate(expr, { x: 1 }); // 'one'
164+
parser.evaluate(expr, { x: 2 }); // 'two'
165+
parser.evaluate(expr, { x: 3 }); // 'three'
166+
parser.evaluate(expr, { x: 4 }); // 'too-big'
167+
```
168+
169+
### If/else-style case
170+
171+
```js
172+
const parser = new Parser();
173+
const expr = `
174+
case
175+
when x == 1 then 'one'
176+
when x == 1+1 then 'two'
177+
when x == 1+1+1 then 'three'
178+
else 'too-big'
179+
end
180+
`;
181+
parser.evaluate(expr, { x: 1 }); // 'one'
182+
parser.evaluate(expr, { x: 2 }); // 'two'
183+
parser.evaluate(expr, { x: 3 }); // 'three'
184+
parser.evaluate(expr, { x: 4 }); // 'too-big'
185+
```
186+
187+
## Object Construction
188+
189+
Objects can be created using JavaScript syntax. This allows for expressions that return object values and for object arguments to be passed to custom functions.
190+
191+
```js
192+
const parser = new Parser();
193+
const expr = `{
194+
a: x * 3,
195+
b: {
196+
/*this x is a property and not the x on the input object*/
197+
x: "first" + "_" + "second",
198+
y: min(x, 0),
199+
},
200+
c: [0, 1, 2, x],
201+
}`;
202+
parser.evaluate(expr, { x: 3 });
203+
/*
204+
{
205+
a: 15,
206+
b: {
207+
x: 'first_second',
208+
z: 0
209+
},
210+
c: [0, 1, 2, 3]
211+
}
212+
*/
213+
```
214+
215+
## As Operator (Type Conversion)
216+
217+
An as operator has been added to support type conversion. **This operator is disabled by default and must be explicitly enabled by setting `operators.conversion` to true in the options.** It can be used to perform value conversion. By default is of limited value; it only supports converting values to numbers, int/integer (by rounding the number), and boolean. The intent is to allow integration of more sophisticated value conversion packages such as numeral.js and moment for conversion of other values.
218+
219+
```js
220+
const parser = new Parser({ operators: { conversion: true } });
221+
parser.evaluate('"1.6" as "number"'); // 1.6
222+
parser.evaluate('"1.6" as "int"'); // 2
223+
parser.evaluate('"1.6" as "integer"'); // 2
224+
parser.evaluate('"1.6" as "boolean"'); // true
225+
```
226+
227+
The default `as` implementation can be overridden by replacing `parser.binaryOps.as`.
228+
229+
```js
230+
const parser = new Parser({ operators: { conversion: true } });
231+
parser.binaryOps.as = (a, _b) => a + '_suffix';
232+
parser.evaluate('"abc" as "suffix"'); // 'abc_suffix'
233+
```

docs/expression.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Expression
2+
3+
`Parser.parse(str)` returns an `Expression` object. `Expression`s are similar to JavaScript functions, i.e. they can be "called" with variables bound to passed-in values. In fact, they can even be converted into JavaScript functions.
4+
5+
## evaluate(variables?: object)
6+
7+
Evaluate the expression, with variables bound to the values in `{variables}`. Each variable in the expression is bound to the corresponding member of the `variables` object. If there are unbound variables, `evaluate` will throw an exception.
8+
9+
```js
10+
js> expr = Parser.parse("2 ^ x");
11+
(2^x)
12+
js> expr.evaluate({ x: 3 });
13+
8
14+
```
15+
16+
## substitute(variable: string, expression: Expression | string | number)
17+
18+
Create a new `Expression` with the specified variable replaced with another expression. This is similar to function composition. If `expression` is a string or number, it will be parsed into an `Expression`.
19+
20+
```js
21+
js> expr = Parser.parse("2 * x + 1");
22+
((2*x)+1)
23+
js> expr.substitute("x", "4 * x");
24+
((2*(4*x))+1)
25+
js> expr2.evaluate({ x: 3 });
26+
25
27+
```
28+
29+
## simplify(variables: object)
30+
31+
Simplify constant sub-expressions and replace variable references with literal values. This is basically a partial evaluation, that does as much of the calculation as it can with the provided variables. Function calls are not evaluated (except the built-in operator functions), since they may not be deterministic.
32+
33+
Simplify is pretty simple. For example, it doesn't know that addition and multiplication are associative, so `((2*(4*x))+1)` from the previous example cannot be simplified unless you provide a value for x. `2*4*x+1` can however, because it's parsed as `(((2*4)*x)+1)`, so the `(2*4)` sub-expression will be replaced with "8", resulting in `((8*x)+1)`.
34+
35+
```js
36+
js> expr = Parser.parse("x * (y * atan(1))").simplify({ y: 4 });
37+
(x*3.141592653589793)
38+
js> expr.evaluate({ x: 2 });
39+
6.283185307179586
40+
```
41+
42+
## variables(options?: object)
43+
44+
Get an array of the unbound variables in the expression.
45+
46+
```js
47+
js> expr = Parser.parse("x * (y * atan(1))");
48+
(x*(y*atan(1)))
49+
js> expr.variables();
50+
x,y
51+
js> expr.simplify({ y: 4 }).variables();
52+
x
53+
```
54+
55+
By default, `variables` will return "top-level" objects, so for example, `Parser.parse(x.y.z).variables()` returns `['x']`. If you want to get the whole chain of object members, you can call it with `{ withMembers: true }`. So `Parser.parse(x.y.z).variables({ withMembers: true })` would return `['x.y.z']`.
56+
57+
## symbols(options?: object)
58+
59+
Get an array of variables, including any built-in functions used in the expression.
60+
61+
```js
62+
js> expr = Parser.parse("min(x, y, z)");
63+
(min(x, y, z))
64+
js> expr.symbols();
65+
min,x,y,z
66+
js> expr.simplify({ y: 4, z: 5 }).symbols();
67+
min,x
68+
```
69+
70+
Like `variables`, `symbols` accepts an option argument `{ withMembers: true }` to include object members.
71+
72+
## toString()
73+
74+
Convert the expression to a string. `toString()` surrounds every sub-expression with parentheses (except literal values, variables, and function calls), so it's useful for debugging precedence errors.
75+
76+
## toJSFunction(parameters: array | string, variables?: object)
77+
78+
Convert an `Expression` object into a callable JavaScript function. `parameters` is an array of parameter names, or a string, with the names separated by commas.
79+
80+
If the optional `variables` argument is provided, the expression will be simplified with variables bound to the supplied values.
81+
82+
```js
83+
js> expr = Parser.parse("x + y + z");
84+
((x + y) + z)
85+
js> f = expr.toJSFunction("x,y,z");
86+
[Function] // function (x, y, z) { return x + y + z; };
87+
js> f(1, 2, 3)
88+
6
89+
js> f = expr.toJSFunction("y,z", { x: 100 });
90+
[Function] // function (y, z) { return 100 + y + z; };
91+
js> f(2, 3)
92+
105
93+
```

docs/language-service.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Language Service
2+
3+
The library includes a built-in language service that provides IDE-like features for expr-eval expressions. This is useful for integrating expr-eval into code editors like Monaco Editor (used by VS Code).
4+
5+
## Features
6+
7+
- **Code Completions** - Autocomplete for functions, operators, keywords, and user-defined variables
8+
- **Hover Information** - Documentation tooltips when hovering over functions and variables
9+
- **Syntax Highlighting** - Token-based highlighting for numbers, strings, keywords, operators, etc.
10+
11+
## Basic Usage
12+
13+
```js
14+
import { createLanguageService } from '@pro-fa/expr-eval';
15+
16+
const ls = createLanguageService();
17+
18+
// Define variables available in your expressions
19+
const variables = { x: 42, user: { name: 'Ada' }, flag: true };
20+
21+
// Get completions at a position
22+
const completions = ls.getCompletions({
23+
textDocument: doc, // LSP-compatible text document
24+
position: { line: 0, character: 5 },
25+
variables
26+
});
27+
28+
// Get hover information
29+
const hover = ls.getHover({
30+
textDocument: doc,
31+
position: { line: 0, character: 3 },
32+
variables
33+
});
34+
35+
// Get syntax highlighting tokens
36+
const tokens = ls.getHighlighting(doc);
37+
```
38+
39+
## Monaco Editor Integration Sample
40+
41+
A complete working example of Monaco Editor integration is included in the repository. To run it:
42+
43+
```bash
44+
# Build the UMD bundle and start the sample server
45+
npm run monaco-sample:serve
46+
```
47+
48+
Then open http://localhost:8080 in your browser. The sample demonstrates:
49+
50+
- Autocompletion for built-in functions (`sum`, `max`, `min`, etc.) and user variables
51+
- Hover documentation for functions and variables
52+
- Live syntax highlighting
53+
- Real-time expression evaluation
54+
55+
The sample code is located in `samples/language-service-sample/` and shows how to:
56+
57+
1. Register a custom language with Monaco
58+
2. Connect the language service to Monaco's completion and hover providers
59+
3. Apply syntax highlighting using decorations
60+
4. Create an LSP-compatible text document wrapper for Monaco models

0 commit comments

Comments
 (0)