Skip to content

Commit e4e3229

Browse files
committed
Rm
1 parent 2abb65d commit e4e3229

15 files changed

Lines changed: 334 additions & 679 deletions

CHANGELOG.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,24 @@ Nothing yet!
1313
- **Removed async evaluation support**: Jexl now only supports synchronous evaluation
1414
- **Renamed methods**: `evalSync()` has been renamed to `eval()`. The async `eval()` method has been removed.
1515
- **Removed PromiseSync class**: Internal implementation detail removed
16-
- **Synchronous transforms/functions only**: Custom transforms and functions must now be synchronous. They should return values directly rather than Promises.
16+
- **Removed transforms**: The transform/pipe operator (`|`) has been completely removed. Use functions instead.
17+
- **Removed array filtering**: Relative filter expressions (e.g., `array[.property == value]`) have been removed. Bracket notation for array/object access (e.g., `array[0]`, `object["key"]`) still works.
18+
- **Removed transform methods**: `addTransform()`, `addTransforms()`, and `getTransform()` have been removed.
1719

1820
### Changed
1921

2022
- Simplified codebase by removing Promise/PromiseSync abstraction layer
2123
- All evaluation is now synchronous, improving performance and simplifying error handling
2224
- Errors are now thrown directly rather than being rejected Promises
25+
- Removed pipe operator (`|`) from grammar
26+
- Removed filter bracket syntax for relative filtering
2327

2428
### Migration Guide
2529

2630
- Replace `await jexl.eval(expr)` with `jexl.eval(expr)` (remove await)
2731
- Replace `jexl.evalSync(expr)` with `jexl.eval(expr)` (remove Sync suffix)
28-
- Update custom transforms/functions to be synchronous
32+
- Replace transforms with functions: change `value|transform(arg)` to `transform(value, arg)`
33+
- Remove uses of relative filtering syntax `array[.prop == value]` (note: direct indexing like `array[0]` still works)
2934
- Replace `.catch()` error handling with `try/catch` blocks
3035

3136
## [v2.3.0]

README.md

Lines changed: 49 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
A fork of the jexl lang for jbrowse
44

5+
## What's New in v3.0
6+
7+
Version 3.0 is a major simplification of Jexl with breaking changes:
8+
9+
- **Synchronous only** - All evaluation is synchronous. No async/await needed.
10+
- **No transforms** - The pipe operator (`|`) has been removed. Use functions instead.
11+
- **No filter expressions** - Relative filter syntax like `array[.property == value]` has been removed. Regular bracket notation for indexing (`array[0]`, `object["key"]`) still works.
12+
13+
See [CHANGELOG.md](CHANGELOG.md) for migration guide.
14+
515
## Quick Examples
616

717
```javascript
@@ -22,10 +32,6 @@ jexl.eval('`Hello ${name.first} ${name.last}`', context)
2232
jexl.eval('`Age in 5 years: ${age + 5}`', context)
2333
// "Age in 5 years: 41"
2434

25-
// Filter arrays
26-
jexl.eval('assoc[.first == "Lana"].last', context)
27-
// "Kane"
28-
2935
// Math operations
3036
jexl.eval('age * (3 - 1)', context)
3137
// 72
@@ -109,45 +115,6 @@ jexl.eval('exes[2]', context) // "Burt"
109115
jexl.eval('exes[lastEx - 1]', context) // "Len"
110116
```
111117

112-
### Filtering Collections
113-
114-
Filter arrays using expressions in brackets. Reference properties with a leading dot:
115-
116-
```javascript
117-
const context = {
118-
employees: [
119-
{ first: 'Sterling', last: 'Archer', age: 36 },
120-
{ first: 'Malory', last: 'Archer', age: 75 },
121-
{ first: 'Lana', last: 'Kane', age: 33 },
122-
{ first: 'Cyril', last: 'Figgis', age: 45 }
123-
]
124-
}
125-
126-
jexl.eval('employees[.first == "Sterling"]', context)
127-
// [{ first: 'Sterling', last: 'Archer', age: 36 }]
128-
129-
jexl.eval('employees[.age >= 30 && .age < 40]', context)
130-
// [{ first: 'Sterling', ... }, { first: 'Lana', ... }]
131-
132-
jexl.eval('employees[.last == "Kane"].first', context)
133-
// "Lana"
134-
```
135-
136-
### Transforms
137-
138-
Apply transforms to values using the pipe operator:
139-
140-
```javascript
141-
jexl.addTransform('upper', (val) => val.toUpperCase())
142-
jexl.addTransform('split', (val, char) => val.split(char))
143-
144-
jexl.eval('"hello"|upper')
145-
// "HELLO"
146-
147-
jexl.eval('"firstName lastName"|split(" ")[0]')
148-
// "firstName"
149-
```
150-
151118
### Functions
152119

153120
Call functions in expressions:
@@ -195,7 +162,9 @@ jexl.eval('x = 5; y = x * 2; y', context)
195162
// context is now { x: 5, y: 10 }
196163
```
197164

198-
## Usage
165+
## API
166+
167+
### Evaluation
199168

200169
```javascript
201170
import jexl from '@jbrowse/jexl'
@@ -208,6 +177,42 @@ const expr = jexl.compile('name.first + " " + name.last')
208177
const result = expr.eval({ name: { first: 'John', last: 'Doe' } })
209178
```
210179

180+
### Adding Custom Functions
181+
182+
```javascript
183+
// Add a single function
184+
jexl.addFunction('round', Math.round)
185+
jexl.addFunction('lower', (str) => str.toLowerCase())
186+
187+
// Add multiple functions
188+
jexl.addFunctions({
189+
min: Math.min,
190+
max: Math.max,
191+
abs: Math.abs
192+
})
193+
194+
// Use in expressions
195+
jexl.eval('round(3.7)') // 4
196+
jexl.eval('lower(name)', { name: 'HELLO' }) // "hello"
197+
jexl.eval('max(1, 5, 3)') // 5
198+
```
199+
200+
### Adding Custom Operators
201+
202+
```javascript
203+
// Add a binary operator
204+
jexl.addBinaryOp('~=', 20, (left, right) =>
205+
left.toLowerCase() === right.toLowerCase()
206+
)
207+
208+
jexl.eval('"Hello" ~= "hello"') // true
209+
210+
// Add a unary operator
211+
jexl.addUnaryOp('~', (right) => Math.floor(right))
212+
213+
jexl.eval('~3.7') // 3
214+
```
215+
211216
## License
212217

213218
MIT License, same as TomFrost/Jexl

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"homepage": "https://github.com/TomFrost/jexl",
5353
"devDependencies": {
5454
"@types/node": "^25.0.3",
55+
"@vitest/coverage-v8": "^4.0.16",
5556
"eslint": "^9.39.2",
5657
"eslint-plugin-import": "^2.32.0",
5758
"eslint-plugin-unicorn": "^62.0.0",

src/Jexl.ts

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { getGrammar } from './grammar.ts'
99
interface Grammar {
1010
elements: Record<string, any>
1111
functions: Record<string, (...args: any[]) => any>
12-
transforms: Record<string, (val: any, ...args: any[]) => any>
1312
}
1413

1514
/**
@@ -96,28 +95,6 @@ class Jexl {
9695
})
9796
}
9897

99-
/**
100-
* Adds or replaces a transform function in this Jexl instance.
101-
* @param {string} name The name of the transform function, as it will be used
102-
* within Jexl expressions
103-
* @param {function} fn The function to be executed when this transform is
104-
* invoked. It will be provided with at least one argument:
105-
* - {*} value: The value to be transformed
106-
* - {...*} args: The arguments for this transform
107-
*/
108-
addTransform(name: string, fn: (val: any, ...args: any[]) => any) {
109-
this._grammar.transforms[name] = fn
110-
}
111-
112-
/**
113-
* Syntactic sugar for calling {@link #addTransform} repeatedly. This function
114-
* accepts a map of one or more transform names to their transform function.
115-
* @param {{}} map A map of transform names to transform functions
116-
*/
117-
addTransforms(map: Record<string, (val: any, ...args: any[]) => any>) {
118-
Object.assign(this._grammar.transforms, map)
119-
}
120-
12198
/**
12299
* Creates an Expression object from the given Jexl expression string, and
123100
* immediately compiles it. The returned Expression object can then be
@@ -150,15 +127,6 @@ class Jexl {
150127
return this._grammar.functions[name]
151128
}
152129

153-
/**
154-
* Retrieves a previously set transform function.
155-
* @param {string} name The name of the transform function
156-
* @returns {function} The transform function
157-
*/
158-
getTransform(name: string) {
159-
return this._grammar.transforms[name]
160-
}
161-
162130
/**
163131
* Evaluates a Jexl string within an optional context.
164132
* @param {string} expression The Jexl expression to be evaluated

src/evaluator/Evaluator.ts

Lines changed: 1 addition & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,7 @@ class Evaluator {
3232
_context: any
3333
_relContext: any
3434

35-
constructor(
36-
grammar: Grammar,
37-
context?: any,
38-
relativeContext?: any
39-
) {
35+
constructor(grammar: Grammar, context?: any, relativeContext?: any) {
4036
this._grammar = grammar
4137
this._context = context || {}
4238
this._relContext = relativeContext || this._context
@@ -74,59 +70,6 @@ class Evaluator {
7470
const vals = entries.map(([_, ast]) => this.eval(ast))
7571
return Object.fromEntries(entries.map(([key], idx) => [key, vals[idx]]))
7672
}
77-
78-
/**
79-
* Applies a filter expression with relative identifier elements to a subject.
80-
* The intent is for the subject to be an array of subjects that will be
81-
* individually used as the relative context against the provided expression
82-
* tree. Only the elements whose expressions result in a truthy value will be
83-
* included in the resulting array.
84-
*
85-
* If the subject is not an array of values, it will be converted to a single-
86-
* element array before running the filter.
87-
* @param {*} subject The value to be filtered usually an array. If this value is
88-
* not an array, it will be converted to an array with this value as the
89-
* only element.
90-
* @param {{}} expr The expression tree to run against each subject. If the
91-
* tree evaluates to a truthy result, then the value will be included in
92-
* the returned array otherwise, it will be eliminated.
93-
* @returns {Array} an array of values that passed the expression filter.
94-
* @private
95-
*/
96-
_filterRelative(subject: any, expr: AstNode) {
97-
const arr = Array.isArray(subject)
98-
? subject
99-
: subject == null
100-
? []
101-
: [subject]
102-
103-
return arr.filter((elem) =>
104-
new Evaluator(this._grammar, this._context, elem).eval(expr)
105-
)
106-
}
107-
108-
/**
109-
* Applies a static filter expression to a subject value. If the filter
110-
* expression evaluates to boolean true, the subject is returned if false,
111-
* undefined.
112-
*
113-
* For any other resulting value of the expression, this function will attempt
114-
* to respond with the property at that name or index of the subject.
115-
* @param {*} subject The value to be filtered. Usually an Array (for which
116-
* the expression would generally resolve to a numeric index) or an
117-
* Object (for which the expression would generally resolve to a string
118-
* indicating a property name)
119-
* @param {{}} expr The expression tree to run against the subject
120-
* @returns {*} the value of the drill-down.
121-
* @private
122-
*/
123-
_filterStatic(subject: any, expr: AstNode) {
124-
const res = this.eval(expr)
125-
if (typeof res === 'boolean') {
126-
return res ? subject : undefined
127-
}
128-
return subject?.[res]
129-
}
13073
}
13174

13275
export default Evaluator

src/evaluator/handlers.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ import type { AstNode } from '../types.ts'
77
import type Evaluator from './Evaluator.ts'
88

99
const poolNames: Record<string, string> = {
10-
functions: 'Jexl Function',
11-
transforms: 'Transform'
10+
functions: 'Jexl Function'
1211
}
1312

1413
/**
@@ -67,19 +66,21 @@ export function ConditionalExpression(this: Evaluator, ast: any) {
6766
}
6867

6968
/**
70-
* Evaluates a FilterExpression by applying it to the subject value.
69+
* Evaluates a FilterExpression by applying bracket notation for array/object access.
70+
* Note: Relative filtering (with leading dot) is not supported.
7171
* @param {{type: 'FilterExpression', relative: <boolean>, expr: {},
7272
* subject: {}}} ast An expression tree with a FilterExpression as the top
7373
* node
74-
* @returns {*} the value of the FilterExpression.
74+
* @returns {*} the value at the specified index/property.
7575
* @private
7676
*/
7777
export function FilterExpression(this: Evaluator, ast: any) {
7878
const subject = this.eval(ast.subject)
7979
if (ast.relative) {
80-
return this._filterRelative(subject, ast.expr)
80+
throw new Error('Relative filter expressions are not supported')
8181
}
82-
return this._filterStatic(subject, ast.expr)
82+
const index = this.eval(ast.expr)
83+
return subject?.[index]
8384
}
8485

8586
/**

src/grammar.ts

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ export type GrammarElement = BinaryOp | UnaryOp | SimpleElement
2727
export interface Grammar {
2828
elements: Record<string, GrammarElement>
2929
functions: Record<string, (...args: any[]) => any>
30-
transforms: Record<string, (val: any, ...args: any[]) => any>
3130
}
3231

3332
export const getGrammar = (): Grammar => ({
@@ -40,7 +39,6 @@ export const getGrammar = (): Grammar => ({
4039
'.': { type: 'dot' },
4140
'[': { type: 'openBracket' },
4241
']': { type: 'closeBracket' },
43-
'|': { type: 'pipe' },
4442
'{': { type: 'openCurl' },
4543
'}': { type: 'closeCurl' },
4644
':': { type: 'colon' },
@@ -119,7 +117,9 @@ export const getGrammar = (): Grammar => ({
119117
precedence: 10,
120118
evalOnDemand: (left, right) => {
121119
const leftVal = left.eval()
122-
if (!leftVal) {return leftVal}
120+
if (!leftVal) {
121+
return leftVal
122+
}
123123
return right.eval()
124124
}
125125
},
@@ -128,7 +128,9 @@ export const getGrammar = (): Grammar => ({
128128
precedence: 10,
129129
evalOnDemand: (left, right) => {
130130
const leftVal = left.eval()
131-
if (leftVal) {return leftVal}
131+
if (leftVal) {
132+
return leftVal
133+
}
132134
return right.eval()
133135
}
134136
},
@@ -173,22 +175,5 @@ export const getGrammar = (): Grammar => ({
173175
* than throw. An error is only appropriate when the function would normally
174176
* return a value, but cannot due to some other failure.
175177
*/
176-
functions: {},
177-
178-
/**
179-
* A map of transform names to transform functions. A transform function
180-
* takes one ore more arguemnts:
181-
*
182-
* - {*} val: A value to be transformed
183-
* - {*} ...args: A variable number of arguments passed to this transform.
184-
* All of these are pre-evaluated to their actual values before calling
185-
* the function.
186-
*
187-
* The transform function should return the transformed value, or throw when
188-
* an unrecoverable error occurs. Transforms should generally return undefined
189-
* when they don't make sense to be used on the given value type, rather than
190-
* throw. An error is only appropriate when the transform would normally
191-
* return a value, but cannot due to some other failure.
192-
*/
193-
transforms: {}
178+
functions: {}
194179
})

src/types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,7 @@ export type AstNodeUnion =
104104
| SequenceExpression
105105
| AssignmentExpression
106106

107-
export type NodeByType<T extends AstNodeUnion['type']> = Extract<AstNodeUnion, { type: T }>
107+
export type NodeByType<T extends AstNodeUnion['type']> = Extract<
108+
AstNodeUnion,
109+
{ type: T }
110+
>

0 commit comments

Comments
 (0)