npm package: @hypatiatech/jexl
Description: Javascript Expression Language: Powerful context-based expression parser and evaluator
Author: Tom Shawver
Latest version: 4.10.5

# Jexl

> **A modern, TypeScript-first fork of the original Jexl library, brought to the XXI century.**
>
> Maintained by [Pawel Uchida-Psztyc (@jarrodek)](https://github.com/jarrodek) with modern tooling, enhanced type safety, and improved developer experience.
>
> Original library created by [Tom Shawver](https://github.com/TomFrost).

Javascript Expression Language: Powerful context-based expression parser and evaluator

## Why This Fork?

This modernized version of Jexl brings several key improvements over the original:

### 🚀 **Modern TypeScript Support**

- **Full TypeScript rewrite** with comprehensive type definitions
- **Enhanced type safety** with `unknown` types instead of unsafe `any`
- **Better IDE support** with full IntelliSense and autocomplete
- **Type-safe transform and function definitions** - write your custom transforms with proper typing

### 🛠 **Modern Development Experience**

- **ESM-first** with proper ES module support
- **Modern build tooling** with up-to-date dependencies
- **Comprehensive test coverage** with modern testing framework
- **Clean, maintainable codebase** following current best practices

### 📦 **Developer-Friendly**

- **Flexible function signatures** - define transforms with specific parameter types
- **Namespace support** - organize functions and transforms with dot notation (e.g., `Utils.String.upper`, `Api.User.getName()`)
- **Better error handling** and debugging experience
- **Consistent API** that works seamlessly in both Node.js and modern browsers
- **Tree-shakeable** for optimal bundle sizes

### 🔧 **Enhanced Type Safety Example**

```typescript
// Before: Unsafe any types
jexl.addTransform('multiply', (val: any, factor: any) => val * factor)

// Now: Type-safe transforms
jexl.addTransform('multiply', (val: number, factor: number) => val * factor)
jexl.addTransform('upperCase', (val: string) => val.toUpperCase())
jexl.addTransform('formatDate', (val: Date, format: string) => /* ... */)
```

## Quick start

Use it with promises or synchronously:

```javascript
const context = {
  name: { first: 'Sterling', last: 'Archer' },
  assoc: [
    { first: 'Lana', last: 'Kane' },
    { first: 'Cyril', last: 'Figgis' },
    { first: 'Pam', last: 'Poovey' }
  ],
  age: 36
}

// Filter an array asynchronously...
await const res = jexl.eval('assoc[.first == "Lana"].last', context)
console.log(res) // Output: Kane

// Do math
await jexl.eval('age * (3 - 1)', context)
// 72

// Concatenate
await jexl.eval('name.first + " " + name["la" + "st"]', context)
// "Sterling Archer"

// Compound
await jexl.eval(
  'assoc[.last == "Figgis"].first == "Cyril" && assoc[.last == "Poovey"].first == "Pam"',
  context
)
// true

// Use array indexes
await jexl.eval('assoc[1]', context)
// { first: 'Cyril', last: 'Figgis' }

// Use conditional logic
await jexl.eval('age > 62 ? "retired" : "working"', context)
// "working"

// Transform
jexl.addTransform('upper', (val) => val.toUpperCase())
await jexl.eval('"duchess"|upper + " " + name.last|upper', context)
// "DUCHESS ARCHER"

// Namespace transforms (NEW!)
jexl.addTransform('String.upper', (val) => val.toUpperCase())
jexl.addTransform('String.repeat', (val, times) => val.repeat(times))
await jexl.eval('"hi"|String.repeat(3)|String.upper', context)
// "HIHIHI"

// Transform asynchronously, with arguments
jexl.addTransform('getStat', async (val, stat) => dbSelectByLastName(val, stat))
try {
  const res = await jexl.eval('name.last|getStat("weight")', context)
  console.log(res) // Output: 184
} catch (e) {
  console.log('Database Error', e.stack)
}

// Functions too, sync or async, args or no args
jexl.addFunction('getOldestAgent', () => db.getOldestAgent())
await jexl.eval('age == getOldestAgent().age', context)
// false

// Namespace functions (NEW!)
jexl.addFunction('Math.max', (a, b) => Math.max(a, b))
jexl.addFunction('Utils.String.slugify', (text) =>
  text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''))
await jexl.eval('Math.max(age, 25)', context)
// 36

// Add your own (a)synchronous operators
// Here's a case-insensitive string equality
jexl.addBinaryOp(
  '_=',
  20,
  (left, right) => left.toLowerCase() === right.toLowerCase()
)
await jexl.eval('"Guest" _= "gUeSt"')
// true

// Compile your expression once, evaluate many times!
const { expr } = jexl
const danger = expr`"Danger " + place` // Also: jexl.compile('"Danger " + place')
await danger.eval({ place: 'zone' }) // Danger zone
await danger.eval({ place: 'ZONE!!!' }) // Danger ZONE!!! (Doesn't recompile the expression!)
```

### Template Literals (Backticks with {{ }})

You can embed expressions inside backtick-quoted template strings using `{{ expr }}` syntax. The text is returned with all interpolations evaluated and concatenated.

Examples:

```js
await jexl.eval('`Total: {{5 + 5}} USD; Note: {{"Approved"}}`') // "Total: 10 USD; Note: Approved"
await jexl.eval('`Hello {{ user.name }}!`', { user: { name: 'Alice' } }) // "Hello Alice!"
await jexl.eval('"Invoice:" + ` Amount {{3}} USD`') // "Invoice: Amount 3 USD"
```

Escapes inside templates:

- Use `\\` to escape characters
- Use `\`` to render a backtick
- Use `\{{` to render literal `{{`

Notes:

- Expressions inside `{{ }}` are full Jexl expressions and evaluated in the same context.
- Errors inside interpolations report positions relative to the original input.
- **Undefined values resolve to empty strings**: If a variable or expression evaluates to `undefined` inside a template, it will be rendered as an empty string rather than the literal "undefined":
  ```js
  await jexl.eval('`Value: {{ missing }}`', {}) // "Value: " (not "Value: undefined")
  ```

## Migration from Original Jexl

Upgrading from the original Jexl library is straightforward:

### Package Installation

```bash
# Remove old package
npm uninstall jexl

# Install modern version
npm install @pawel-up/jexl
```

### Import Changes

```typescript
// Before
const jexl = require('jexl')
// or
import jexl from 'jexl'

// Now
import { Jexl } from '@pawel-up/jexl'
```

### Enhanced Transform and Function Definitions

The API remains the same, but you can now use proper TypeScript types and namespace organization:

```typescript
// Before: No type safety
jexl.addTransform('multiply', (val, factor) => val * factor)

// Now: Full type safety (optional but recommended)
jexl.addTransform('multiply', (val: number, factor: number): number => val * factor)

// NEW: Namespace support for better organization
jexl.addTransform('Math.multiply', (val: number, factor: number): number => val * factor)
jexl.addTransform('String.upper', (val: string): string => val.toUpperCase())
jexl.addFunction('Utils.String.slugify', (text: string): string =>
  text
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-|-$/g, '')
)
```

### Backwards Compatibility

- ✅ **99% API compatible** - existing code mostly works without changes. We removed the `evalSync()` methods.
- ✅ **Same expression syntax** - no need to update your expressions
- ✅ **Same behavior** - results are identical to the original library
- ✅ **Same performance** - optimized for modern JavaScript engines

## Installation

Jexl works on the backend, and on the frontend if bundled using a bundler like Parcel or Webpack.

Install from npm:

```sh
npm install @pawel-up/jexl --save
```

and use it:

```javascript
import { Jexl } from '@pawel-up/jexl'
```

## All the details

### Unary Operators

| Operation | Symbol |
| --------- | :----: |
| Negate    |   !    |

### Binary Operators

| Operation        |    Symbol    |
| ---------------- | :----------: |
| Add, Concat      |      +       |
| Subtract         |      -       |
| Multiply         |      \*      |
| Divide           |      /       |
| Divide and floor |      //      |
| Modulus          |      %       |
| Power of         |      ^       |
| Logical AND      |      &&      |
| Logical OR       | &#124;&#124; |
| Logical AND (textual) |   `and`   |
| Logical OR (textual)  |    `or`    |
| Logical XOR (textual) |   `xor`   |

### Comparisons

| 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`

The `in` 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']`. 

**Deep Comparison for Arrays and Objects:**

The `in` operator uses **deep comparison** when searching arrays, meaning it can find objects and arrays within arrays by comparing their contents recursively:

```javascript
// Deep comparison works for objects
{a: 1, b: 2} in [{a: 1, b: 2}, {a: 3}]  // true

// Deep comparison works for arrays
[1, 2] in [[1, 2], [3, 4]]  // true

// Deep comparison works for nested structures
{a: {x: 1}, b: [2, 3]} in [{a: {x: 1}, b: [2, 3]}, {a: {x: 2}}]  // true
```

The deep comparison:
- Recursively compares object properties and array elements
- Handles nested objects and arrays
- Compares Date objects by their timestamp
- Uses strict equality (`===`) for primitive values

**Performance Optimization:**

For objects with an `id` property, the `in` operator uses an optimized comparison by comparing `id` values first, falling back to deep comparison if needed:

```javascript
const item = { id: 2, name: 'John' }
const list = [{ id: 1, name: 'Jane' }, { id: 2, name: 'John' }]
item in list  // true (compares by id for performance)
```

### Logical Operators with Validation Error Handling

Jexl supports textual logical operators (`and`, `or`, `xor`) that work seamlessly with validation errors. Validation errors are strings or objects where all values are strings (e.g., `{en: "invalid", ar: "خاطئ"}`).

#### `and` Operator

The `and` operator collects validation errors and returns them as an array:

| Expression | Result |
| ---------- | ------ |
| `"invalid" and true` | `["invalid"]` |
| `{en: "invalid", ar: "خاطئ"} and true` | `[{en: "invalid", ar: "خاطئ"}]` |
| `true and true` | `true` |
| `"invalid 1" and "invalid 2"` | `["invalid 1", "invalid 2"]` |
| `"invalid 1" and "invalid 2" and "invalid 3"` | `["invalid 1", "invalid 2", "invalid 3"]` |
| `"invalid" and false` | `false` |
| `false and "invalid"` | `false` |

**Behavior:**
- If any operand is a validation error, all validation errors are collected into an array
- If all operands are truthy (non-validation errors), returns `true`
- If any operand is falsy (non-validation error), returns `false`
- Short-circuits when the left operand is falsy (non-validation error)

#### `or` Operator

The `or` operator returns the first truthy value, or collects validation errors:

| Expression | Result |
| ---------- | ------ |
| `"invalid 1" or "invalid 2"` | `["invalid 2"]` |
| `"invalid 1" or true` | `true` |
| `"invalid 1" or "invalid 2" or "invalid 3"` | `["invalid 3"]` |
| `"invalid 1" or "invalid 2" or true` | `true` |
| `true or "invalid"` | `true` |

**Behavior:**
- If the left operand is a validation error and the right is truthy, returns the right value
- If both operands are validation errors, returns an array with the right error
- If the left operand is truthy (non-validation error), returns the left value
- Otherwise returns the right value

#### `xor` Operator

The `xor` operator handles validation errors appropriately:

| Expression | Result |
| ---------- | ------ |
| `"invalid" xor false` | `["invalid"]` |
| `false xor "invalid"` | `["invalid"]` |
| `"invalid 1" xor "invalid 2"` | `["invalid 1", "invalid 2"]` |
| `"invalid" xor true` | `["invalid"]` |
| `true xor false` | `true` |
| `true xor true` | `false` |

**Behavior:**
- Collects all validation errors into an array
- Uses standard XOR logic (true if exactly one operand is truthy) when no validation errors are present

#### Validation Error Types

Validation errors can be:
- **Strings**: Any non-empty string (e.g., `"invalid"`, `"error message"`)
- **Objects**: Objects where all values are strings (e.g., `{en: "invalid", ar: "خاطئ"}`)

#### Use Cases

These operators are particularly useful for form validation and error collection:

```javascript
// Collect multiple validation errors
const errors = await jexl.eval('"Email is invalid" and "Password too short" and "Age must be 18+"')
console.log(errors) // ["Email is invalid", "Password too short", "Age must be 18+"]

// Multi-language validation errors
const errorObj = { en: "Invalid email", ar: "البريد الإلكتروني غير صحيح" }
const result = await jexl.eval('error and true', { error: errorObj })
console.log(result) // [{en: "Invalid email", ar: "البريد الإلكتروني غير صحيح"}]

// Chain validation checks
const validation = await jexl.eval(
  'validateEmail(email) and validatePassword(password) and validateAge(age)',
  { email: 'test@example.com', password: '123', age: 15 }
)
// Returns array of all validation errors, or true if all validations pass
```

### Ternary operator

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 |

### Native Types

| Type     |            Examples            |
| -------- | :----------------------------: |
| Booleans |        `true`, `false`         |
| Strings  | "Hello \"user\"", 'Hey there!' |
| Numerics |      6, -7.2, 5, -3.14159      |
| Objects  |       {hello: "world!"}        |
| Arrays   |      ['hello', 'world!']       |

### Groups

Parentheses work just how you'd expect them to:

| Expression                          | Result |
| ----------------------------------- | :----- |
| (83 + 1) / 2                        | 42     |
| 1 < 3 && (4 > 2 &#124;&#124; 2 > 4) | true   |

### Identifiers

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

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                                                                                |

#### Array Projection (Mapper)

When a filter expression returns non-boolean values, the collection acts as a mapper, returning an array of the evaluated expression results instead of filtering:

| Expression                                    | Result                                                                                |
| --------------------------------------------- | ------------------------------------------------------------------------------------- |
| employees[.first]                             | ['Sterling', 'Malory', 'Lana', 'Cyril', 'Cheryl']                                     |
| employees[.first + " " + .last]              | ['Sterling Archer', 'Malory Archer', 'Lana Kane', 'Cyril Figgis', 'Cheryl Tunt']      |
| employees[.age * 2]                           | [72, 150, 66, 90, 56]                                                                  |
| employees[{value: .id, label: .name}]        | [{value: 1, label: 'Sterling'}, {value: 2, label: 'Malory'}, ...]                     |

**Behavior:**
- If all filter results are booleans: filters the collection (returns matching objects)
- If any filter result is non-boolean: projects the collection (returns array of evaluated values)

**Object Literal Mapping:**

You can map array elements to objects using object literals with relative identifiers. Each array element becomes the relative context for evaluating the object literal properties:

```javascript
const context = {
  members: {
    value: [
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' }
    ]
  }
}

await jexl.eval('members.value[{value: .id, label: .name}]', context)
// Returns: [{value: 1, label: 'Alice'}, {value: 2, label: 'Bob'}]
```

Object literal mapping supports:
- **Computed values**: `items[{id: .id, fullName: .name + " - $" + .price}]`
- **Nested property access**: `users[{first: .profile.firstName, last: .profile.lastName}]`
- **Any expression**: Object literal property values can contain any valid Jexl expression using relative identifiers

#### Spread Operator

Jexl supports the spread operator (`...`) for arrays and objects, allowing you to combine and merge collections:

**Array Spread:**

```javascript
const context = { a: [1, 2], b: [3, 4], s: 'ab' }

// Spread arrays
await jexl.eval('[...a, 5, ...b]', context)  // [1, 2, 5, 3, 4]

// Spread strings (iterable)
await jexl.eval('[...s, "c"]', context)  // ['a', 'b', 'c']
```

**Object Spread:**

```javascript
const context = { 
  a: { x: 1, y: 2 }, 
  b: { y: 9, z: 3 } 
}

// Spread objects (later properties override earlier ones)
await jexl.eval('{...a, y: 5, ...b, x: 7}', context)  
// { x: 7, y: 9, z: 3 }
// Order: a spreads first (x:1, y:2), then y:5, then b spreads (y:9, z:3), then x:7
```

**Spread in Object Literals:**

The spread operator can be used within object literals to merge properties:

```javascript
const context = { defaults: { theme: 'dark', lang: 'en' }, overrides: { lang: 'fr' } }

await jexl.eval('{...defaults, ...overrides}', context)
// { theme: 'dark', lang: 'fr' }
```

**Notes:**
- Spread operator works with arrays, strings (as iterables), and objects
- For objects, later properties override earlier ones when there are conflicts
- Throws an error if attempting to spread a non-iterable value in arrays

### Transforms

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)`.

Transform functions can also access the current execution context through `this.context`. See the **Accessing Context in Functions and Transforms** section under Functions for more details.

```javascript
jexl.addTransform('split', (val, char) => val.split(char))
jexl.addTransform('lower', (val) => val.toLowerCase())

// Transform accessing context
jexl.addTransform('formatCurrency', function(val) {
  const currency = this.context.currency || 'USD'
  return `${currency} ${val}`
})
```

| 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](https://github.com/buglabs/node-xml2json) module, allows XML to be
traversed just as easily as plain javascript objects:

```javascript
const xml2json = require('xml2json')

jexl.addTransform('xml', (val) => xml2json.toJson(val, { object: true }))

const context = {
  xmlDoc: `
    <Employees>
      <Employee>
        <FirstName>Cheryl</FirstName>
        <LastName>Tunt</LastName>
      </Employee>
      <Employee>
        <FirstName>Cyril</FirstName>
        <LastName>Figgis</LastName>
      </Employee>
    </Employees>`,
}

var expr = 'xmlDoc|xml.Employees.Employee[.LastName == "Figgis"].FirstName'

jexl.eval(expr, context).then(console.log) // Output: Cyril
```

### Namespace Functions and Transforms

Jexl supports organizing functions and transforms into namespaces using dot notation. This allows you to create hierarchical structures and avoid naming conflicts in large applications.

#### Namespace Functions

Functions can be organized into namespaces by using dot-separated names when registering them:

```javascript
// Register namespace functions
jexl.addFunction('Math.max', (a, b) => Math.max(a, b))
jexl.addFunction('Utils.String.slugify', (text) =>
  text
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-|-$/g, '')
)
jexl.addFunction('Api.User.getName', async (userId) => {
  const user = await fetchUser(userId)
  return user.name
})

// Use namespace functions in expressions
await jexl.eval('Math.max(10, 25)') // 35
await jexl.eval('Utils.String.slugify("Hello World!")') // "hello-world"
await jexl.eval('Api.User.getName(123)') // "John Doe"
```

#### Namespace Transforms

Transforms can also be organized into namespaces and used with the pipe operator:

```javascript
// Register namespace transforms
jexl.addTransform('String.upper', (val) => val.toUpperCase())
jexl.addTransform('String.lower', (val) => val.toLowerCase())
jexl.addTransform('String.repeat', (val, times) => val.repeat(times))
jexl.addTransform('Utils.Text.capitalize', (text) => text.charAt(0).toUpperCase() + text.slice(1).toLowerCase())
jexl.addTransform('Format.Date.iso', (date) => new Date(date).toISOString())

// Use namespace transforms in expressions
await jexl.eval('"hello world"|String.upper') // "HELLO WORLD"
await jexl.eval('"HELLO WORLD"|String.lower') // "hello world"
await jexl.eval('"hi"|String.repeat(3)') // "hihihi"
await jexl.eval('"hello world"|Utils.Text.capitalize') // "Hello world"
await jexl.eval('"2024-01-01"|Format.Date.iso') // "2024-01-01T00:00:00.000Z"
```

#### Deeply Nested Namespaces

Namespaces can be nested to any depth:

```javascript
// Deep namespace registration
jexl.addFunction('Company.Departments.HR.getEmployeeCount', () => 150)
jexl.addTransform('Data.Validation.Email.isValid', (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))

// Usage in expressions
await jexl.eval('Company.Departments.HR.getEmployeeCount()') // 150
await jexl.eval('"user@example.com"|Data.Validation.Email.isValid') // true
```

#### Chaining Namespace Transforms

Namespace transforms can be chained just like regular transforms:

```javascript
// Register multiple namespace transforms
jexl.addTransform('String.trim', (val) => val.trim())
jexl.addTransform('String.upper', (val) => val.toUpperCase())
jexl.addTransform('String.split', (val, separator) => val.split(separator))

// Chain namespace transforms
await jexl.eval('"  hello world  "|String.trim|String.upper|String.split(" ")')
// Result: ["HELLO", "WORLD"]
```

#### Namespace Transforms with Arguments

Namespace transforms support arguments just like regular transforms:

```javascript
// Transforms with multiple arguments
jexl.addTransform('String.padStart', (val, length, padString) => val.padStart(length, padString))
jexl.addTransform('Array.slice', (arr, start, end) => arr.slice(start, end))

// Usage with arguments
await jexl.eval('"5"|String.padStart(3, "0")') // "005"
await jexl.eval('[1,2,3,4,5]|Array.slice(1, 4)') // [2,3,4]
```

#### Mixed Namespace and Regular Functions/Transforms

You can mix namespace and regular functions/transforms in the same expression:

```javascript
// Register both types
jexl.addFunction('min', Math.min)
jexl.addFunction('Utils.Math.average', (arr) => arr.reduce((a, b) => a + b) / arr.length)
jexl.addTransform('sort', (arr) => [...arr].sort())
jexl.addTransform('Array.reverse', (arr) => [...arr].reverse())

// Mix in expressions
await jexl.eval('min(Utils.Math.average([1,2,3]), 5)') // 2
await jexl.eval('[3,1,4,2]|sort|Array.reverse') // [4,3,2,1]
```

#### Namespace Organization Best Practices

Consider organizing your functions and transforms by domain or functionality:

```javascript
// By domain
jexl.addFunction('User.authenticate', authenticateUser)
jexl.addFunction('User.authorize', authorizeUser)
jexl.addFunction('Product.getPrice', getProductPrice)
jexl.addFunction('Order.calculate', calculateOrder)

// By data type
jexl.addTransform('String.capitalize', capitalizeString)
jexl.addTransform('String.truncate', truncateString)
jexl.addTransform('Array.unique', uniqueArray)
jexl.addTransform('Array.flatten', flattenArray)
jexl.addTransform('Date.format', formatDate)
jexl.addTransform('Date.addDays', addDaysToDate)

// By utility category
jexl.addFunction('Validation.isEmail', isValidEmail)
jexl.addFunction('Validation.isPhoneNumber', isValidPhoneNumber)
jexl.addTransform('Format.currency', formatCurrency)
jexl.addTransform('Format.percentage', formatPercentage)
```

#### Migration from Non-Namespaced Functions/Transforms

Existing non-namespaced functions and transforms continue to work unchanged:

```javascript
// Existing code continues to work
jexl.addFunction('myFunc', () => 'old way')
jexl.addTransform('myTransform', (val) => val + ' transformed')

await jexl.eval('myFunc()') // "old way"
await jexl.eval('"test"|myTransform') // "test transformed"

// New namespace versions can coexist
jexl.addFunction('Utils.myFunc', () => 'new way')
jexl.addTransform('Utils.myTransform', (val) => val + ' namespace transformed')

await jexl.eval('Utils.myFunc()') // "new way"
await jexl.eval('"test"|Utils.myTransform') // "test namespace transformed"
```

### Functions

While Transforms are the preferred way to change one value into another value,
Jexl also allows top-level expression functions to be defined. Use these to
provide access to functions that either don't require an input, or require
multiple equally-important inputs. They can be added with
`jexl.addFunction(name, function)`. Like transforms, functions can return a
value, or a Promise that resolves to the resulting value.

```javascript
jexl.addFunction('min', Math.min)
jexl.addFunction('expensiveQuery', async () => db.runExpensiveQuery())
```

| Expression                                    | Result                    |
| --------------------------------------------- | ------------------------- |
| min(4, 2, 19)                                 | 2                         |
| counts.missions &#124;&#124; expensiveQuery() | Query only runs if needed |

#### Accessing Context in Functions and Transforms

Functions and transforms added via `addFunction()` or `addTransform()` can access the current execution context through `this.context`. This allows your custom functions to read values from the context object without requiring them as explicit parameters.

**Important:** Use regular `function` declarations, not arrow functions, to access `this.context`. Arrow functions don't have their own `this` binding and won't be able to access the context.

```javascript
// Function accessing context
jexl.addFunction('getUserName', function() {
  return this.context.user.name
})

const context = { user: { name: 'John Doe' } }
await jexl.eval('getUserName()', context) // "John Doe"

// Transform accessing context
jexl.addTransform('formatCurrency', function(val) {
  const currency = this.context.currency || 'USD'
  const symbol = currency === 'EUR' ? '€' : '$'
  return `${symbol}${val.toFixed(2)}`
})

await jexl.eval('100|formatCurrency', { currency: 'USD' }) // "$100.00"
await jexl.eval('100|formatCurrency', { currency: 'EUR' }) // "€100.00"

// Function with arguments and context access
jexl.addFunction('multiplyByFactor', function(value) {
  const factor = this.context.factor || 1
  return value * factor
})

await jexl.eval('multiplyByFactor(10)', { factor: 5 }) // 50

// Complex example: accessing nested context properties
jexl.addFunction('getSetting', function(key) {
  return this.context.settings?.[key] || null
})

const context = {
  settings: {
    theme: 'dark',
    language: 'en'
  }
}
await jexl.eval('getSetting("theme")', context) // "dark"
await jexl.eval('getSetting("language")', context) // "en"

// Works with async functions too
jexl.addFunction('getAsyncValue', async function() {
  await someAsyncOperation()
  return this.context.asyncValue
})

// Works with namespace functions and transforms
jexl.addFunction('Utils.getUser', function() {
  return this.context.user
})

jexl.addTransform('Format.currency', function(val) {
  const currency = this.context.currency || 'USD'
  return `${currency} ${val}`
})
```

**Note:** The context is isolated per evaluation. Each call to `eval()` receives its own context binding, so functions will always access the context from the current evaluation.

### Context

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!

## API Reference

### Jexl Instance

#### 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_, _{boolean} [manualEval]_)

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.

If `manualEval` is true, the `left` and `right` arguments will be wrapped in
objects with an `eval` function. Calling `left.eval()` or `right.eval()` will
return a promise that resolves to that operand's actual value. This is useful to
conditionally evaluate operands, and is how `&&` and `||` work.

#### 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.addFunction(_{string} name_, \_{function} func)

Adds an expression function to this Jexl instance. The `name` can be a simple identifier (e.g., `'myFunction'`) or a namespace path using dot notation (e.g., `'Utils.String.slugify'`). See the **Functions** and **Namespace Functions and Transforms** sections above for information on the structure of an expression function.

#### jexl.addFunctions(_{{}} map_)

Adds multiple functions from a supplied map of function name to expression
function.

#### jexl.addTransform(_{string} name_, _{function} transform_)

Adds a transform function to this Jexl instance. The `name` can be a simple identifier (e.g., `'myTransform'`) or a namespace path using dot notation (e.g., `'String.upper'`). See the **Transforms** and **Namespace Functions and Transforms** sections 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.compile(_{string} expression_)

Constructs an Expression object around the given Jexl expression string.
Expression objects allow a Jexl expression to be compiled only once but
evaluated many times. See the Expression API below. Note that the only
difference between this function and `jexl.createExpression` is that this
function will immediately compile the expression, and throw any errors
associated with invalid expression syntax.

#### jexl.createExpression(_{string} expression_)

Constructs an Expression object around the given Jexl expression string.
Expression objects allow a Jexl expression to be compiled only once but
evaluated many times. See the Expression API below.

#### 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]_)

**Returns `{Promise<*>}`.** Evaluates an expression. The context map is optional.

#### jexl.expr: _tagged template literal_

A convenient bit of syntactic sugar for `jexl.createExpression`

```javascript
const someNumber = 10
const expression = jexl.expr`5 + ${someNumber}`
console.log(await expression.eval()) // 15
```

Note that `expr` will stay bound to its associated Jexl instance even if it's
pulled out of context:

```javascript
const { expr } = jexl
jexl.addTransform('double', (val) => val * 2)
const expression = expr`2|double`
console.log(await expression.eval()) // 4
```

#### 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.

### Expression

Expression objects are created via `jexl.createExpression`, `jexl.compile`, or
`jexl.expr`, and are a convenient way to ensure jexl expressions compile only
once, even if they're evaluated multiple times.

#### expression.compile()

**Returns self `{Expression}`.** Forces the expression to compile, even if it
was compiled before. Note that each compile will happen with the latest grammar
and transforms from the associated Jexl instance.

#### expression.eval(_{{}} [context]_)

**Returns `{Promise<*>}`.** Evaluates the expression. The context map is
optional.

## Expression Validation

This modernized version of Jexl includes a powerful `Validator` class that helps you validate expressions before evaluation, providing comprehensive error reporting and analysis.

### Validator Class

The `Validator` class provides static methods to validate Jexl expressions, offering detailed feedback about syntax errors, semantic issues, and potential problems.

#### Key Features

- 🔍 **Comprehensive Validation**: Checks syntax, semantics, and context usage
- 🚨 **Detailed Error Reporting**: Provides specific error messages with position information
- ⚠️ **Warning System**: Identifies potential issues and performance concerns
- ℹ️ **Informational Messages**: Offers suggestions for improvements
- 🧹 **Automatic Whitespace Trimming**: Cleans expressions before validation
- 🔧 **Context-Agnostic Mode**: Validates expressions without requiring specific context
- **JavaScript-aligned Logic**: Correctly distinguishes between `null` and `undefined` and handles `&&`/`||` operator precedence just like in JS.
- **Powerful Operators**: Full support for unary minus and plus on expressions (e.g., `-myFunc()`, `+variable`).
- **Robust Parsing**: Handles complex cases like negative numbers in function arguments (`myFunc(5, -3)`), ternary operators after function calls, and leading decimal numbers (`.5`).
- **Clear Error Handling**: Provides precise errors for ambiguous syntax like `2++2`.

#### Basic Usage

```typescript
import { Validator, Jexl } from '@pawel-up/jexl'

const jexl = new Jexl()
const validator = new Validator(jexl.grammar)

// Validate a simple expression
const result = validator.validate('name.first + " " + name.last')
console.log(result.isValid) // true
console.log(result.errors) // []

// Validate with context
const context = { name: { first: 'John', last: 'Doe' } }
const result2 = validator.validate('name.middle', context)
console.log(result2.isValid) // false
console.log(result2.errors[0].message) // "Property 'middle' not found in context object 'name'"
```

#### Validation Result

The `validate` method returns a `ValidationResult` object with the following properties:

```typescript
interface ValidationResult {
  isValid: boolean // Overall validation status
  errors: ValidationIssue[] // Critical errors that prevent execution
  warnings: ValidationIssue[] // Non-critical issues that might cause problems
  info: ValidationIssue[] // Informational suggestions
  trimmedExpression: string // Expression after automatic whitespace trimming
}

interface ValidationIssue {
  severity: 'error' | 'warning' | 'info'
  message: string
  position?: number // Character position in expression
  line?: number // Line number (for multi-line expressions)
  column?: number // Column number
}
```

#### Automatic Whitespace Trimming

The Validator automatically trims leading and trailing whitespace from expressions before validation:

```typescript
// These expressions are equivalent after trimming
validator.validate('name.first')
validator.validate('   name.first   ')
validator.validate('\t\nname.first\n\t')
```

#### Context-Agnostic Validation

Use `allowUndefinedContext: true` to validate expressions without providing specific context:

```typescript
// Validate syntax only, ignore missing context
const result = validator.validate('user.profile.email', undefined, {
  allowUndefinedContext: true,
})

console.log(result.isValid) // true (if syntax is correct)
console.log(result.warnings) // May contain warnings about undefined context
```

#### Validation Examples

```typescript
// Syntax error
const syntaxError = validator.validate('name.first +')
console.log(syntaxError.errors[0].message) // "Unexpected end of expression"

// Context validation
const context = { user: { name: 'John' } }
const contextError = validator.validate('user.age', context)
console.log(contextError.errors[0].message) // "Property 'age' not found in context object 'user'"

// Performance warning
const perfWarning = validator.validate('users[.name == "John" && .active == true]')
console.log(perfWarning.warnings[0].message) // "Complex filter expression may impact performance"

// Style suggestion
const styleInfo = validator.validate('name["first"]')
console.log(styleInfo.info[0].message) // "Consider using dot notation: name.first"
```

#### Advanced Usage

```typescript
// Validate multiple expressions
const expressions = ['user.name', 'user.age > 18', 'users[.active == true].length']

expressions.forEach((expr, index) => {
  const result = validator.validate(expr, context)
  if (!result.isValid) {
    console.log(`Expression ${index + 1} has errors:`, result.errors)
  }
  if (result.warnings.length > 0) {
    console.log(`Expression ${index + 1} has warnings:`, result.warnings)
  }
})

// Custom validation with options
const result = validator.validate('   user.email   ', undefined, {
  allowUndefinedContext: true,
})

console.log(`Original: "${result.trimmedExpression}"`)
console.log(`Trimmed: "${result.trimmedExpression}"`)
console.log(`Valid: ${result.isValid}`)
```

#### Validator API Reference

##### validate(expression, context?, options?)

**Parameters:**

- `expression` _{string}_: The Jexl expression to validate
- `context` _{object}_: Optional context object for validation
- `options` _{object}_: Optional validation options
  - `allowUndefinedContext` _{boolean}_: Allow undefined context properties (default: false)

**Returns:** `ValidationResult` object with validation details

##### ValidationResult Properties

- `isValid` _{boolean}_: True if expression has no errors
- `errors` _{ValidationIssue[]}_: Array of critical validation errors
- `warnings` _{ValidationIssue[]}_: Array of non-critical warnings
- `info` _{ValidationIssue[]}_: Array of informational suggestions
- `trimmedExpression` _{string}_: Expression after whitespace trimming

## Expression Autocomplete

This modernized version of Jexl includes a powerful `Autocomplete` engine that provides intelligent suggestions while users write Jexl expressions. The autocomplete system analyzes the expression at a given cursor position and returns context-aware suggestions with rich metadata.

### Key Features

- 🔍 **Context-aware suggestions**: Suggests available variables, functions, transforms based on current context and cursor position
- 🎯 **Syntax-aware suggestions**: Suggests operators, keywords, closing brackets/parens
- 🏷️ **Type-aware suggestions**: Suggests properties based on inferred types from context objects
- 📝 **Rich metadata**: Each suggestion includes type, description, signature, and documentation
- 🎨 **Namespace support**: Full support for namespace functions and transforms
- 🚀 **Performance optimized**: Fast suggestions even with large context objects

### Basic Usage

```typescript
import { Jexl, Autocomplete } from '@pawel-up/jexl'

const jexl = new Jexl()

// Get suggestions for dot notation
const result = jexl.autocomplete('user.', 5, {
  user: { name: 'John', age: 30 },
})
console.log(result.suggestions)
// [{ label: 'name', value: 'name', type: 'property', description: 'Property of type string' }, ...]

// Get suggestions for transforms
jexl.addTransform('upper', (val: string) => val.toUpperCase())
const transformResult = jexl.autocomplete('value|', 6)
console.log(transformResult.suggestions)
// [{ label: 'upper', value: 'upper', type: 'transform', description: 'Transform function' }, ...]
```

### Autocomplete Result Structure

The `autocomplete` method returns an `AutocompleteResult` object:

```typescript
interface AutocompleteResult {
  suggestions: AutocompleteSuggestion[] // Array of suggestions
  triggerCharacter?: string // What triggered autocomplete (., |, etc.)
  replaceRange?: { start: number; end: number } // Range to replace when suggestion is selected
}

interface AutocompleteSuggestion {
  label: string // Display text shown to user
  value: string // Text to insert when suggestion is selected
  type: SuggestionType // 'property', 'function', 'transform', 'operator', etc.
  description?: string // Human-readable description
  signature?: string // Function/transform signature
  documentation?: string // Detailed documentation
  detail?: string // Additional context information
  sortText?: string // Custom sort order (lower = higher priority)
  filterText?: string // Custom filter text for matching
}
```

### Context-Aware Suggestions

#### Dot Notation Property Access

```typescript
const context = {
  user: {
    profile: {
      personal: { firstName: 'John', lastName: 'Doe' },
      contact: { email: 'john@example.com', phone: '123-456-7890' },
    },
  },
}

// Get property suggestions
const result = jexl.autocomplete('user.profile.', 13, context)
console.log(result.suggestions)
// [
//   { label: 'personal', value: 'personal', type: 'property', description: 'Property of type object' },
//   { label: 'contact', value: 'contact', type: 'property', description: 'Property of type object' }
// ]

// Deep nested property access
const deepResult = jexl.autocomplete('user.profile.personal.', 22, context)
console.log(deepResult.suggestions)
// [
//   { label: 'firstName', value: 'firstName', type: 'property', description: 'Property of type string' },
//   { label: 'lastName', value: 'lastName', type: 'property', description: 'Property of type string' }
// ]
```

#### Transform Suggestions

```typescript
// Register transforms
jexl.addTransform('upper', (val: string) => val.toUpperCase())
jexl.addTransform('lower', (val: string) => val.toLowerCase())
jexl.addTransform('trim', (val: string) => val.trim())
jexl.addTransform('String.upper', (val: string) => val.toUpperCase())
jexl.addTransform('String.lower', (val: string) => val.toLowerCase())

// Get transform suggestions after pipe operator
const result = jexl.autocomplete('value|', 6)
console.log(result.suggestions)
// [
//   { label: 'upper', value: 'upper', type: 'transform', description: 'Transform function' },
//   { label: 'lower', value: 'lower', type: 'transform', description: 'Transform function' },
//   { label: 'trim', value: 'trim', type: 'transform', description: 'Transform function' },
//   { label: 'String.upper', value: 'String.upper', type: 'transform', description: 'Transform function' },
//   { label: 'String.lower', value: 'String.lower', type: 'transform', description: 'Transform function' }
// ]
```

#### Namespace Transform Suggestions

```typescript
// Get suggestions for namespace transforms
const namespaceResult = jexl.autocomplete('String.', 7)
console.log(namespaceResult.suggestions)
// [
//   { label: 'upper', value: 'upper', type: 'transform', description: 'Transform function in String namespace' },
//   { label: 'lower', value: 'lower', type: 'transform', description: 'Transform function in String namespace' }
// ]
```

#### Function Suggestions

```typescript
// Register functions
jexl.addFunction('max', (...args: number[]) => Math.max(...args))
jexl.addFunction('min', (...args: number[]) => Math.min(...args))
jexl.addFunction('Math.max', (a: number, b: number) => Math.max(a, b))
jexl.addFunction('Utils.String.slugify', (text: string) =>
  text
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-|-$/g, '')
)

// Get function suggestions
const result = jexl.autocomplete('', 0)
console.log(result.suggestions)
// [
//   { label: 'max', value: 'max', type: 'function', description: 'Function' },
//   { label: 'min', value: 'min', type: 'function', description: 'Function' },
//   { label: 'Math.max', value: 'Math.max', type: 'function', description: 'Function' },
//   { label: 'Utils.String.slugify', value: 'Utils.String.slugify', type: 'function', description: 'Function' }
// ]

// Get namespace function suggestions
const namespaceResult = jexl.autocomplete('Math.', 5)
console.log(namespaceResult.suggestions)
// [
//   { label: 'max', value: 'max', type: 'function', description: 'Function in Math namespace' }
// ]
```

### Advanced Features

#### Partial Input Filtering

The autocomplete engine automatically filters suggestions based on partial input:

```typescript
const context = { name: 'John', age: 30, active: true }

// Filter suggestions that start with 'na'
const result = jexl.autocomplete('na', 2, context)
console.log(result.suggestions)
// [{ label: 'name', value: 'name', type: 'identifier', description: 'Variable of type string' }]
// Note: 'age' and 'active' are filtered out
```

#### Replace Range Detection

The autocomplete engine provides the exact range of text to replace when a suggestion is selected:

```typescript
const result = jexl.autocomplete('na', 2, context)
console.log(result.replaceRange)
// { start: 0, end: 2 } - Replace characters 0-2 with the selected suggestion
```

#### Trigger Character Detection

The engine identifies what character triggered the autocomplete:

```typescript
// Dot notation
const dotResult = jexl.autocomplete('user.', 5, context)
console.log(dotResult.triggerCharacter) // '.'

// Pipe operator
const pipeResult = jexl.autocomplete('value|', 6)
console.log(pipeResult.triggerCharacter) // '|'
```

### Standalone Autocomplete Class

You can also use the `Autocomplete` class independently:

```typescript
import { Autocomplete } from '@pawel-up/jexl'

const jexl = new Jexl()
jexl.addFunction('test', () => 'test')
jexl.addTransform('upper', (val: string) => val.toUpperCase())

const autocomplete = new Autocomplete(jexl.grammar)
const result = autocomplete.getSuggestions('', 0)

console.log(result.suggestions)
// Get suggestions without needing a Jexl instance
```

### Error Handling

The autocomplete engine gracefully handles errors and edge cases:

```typescript
// Invalid cursor positions
const result1 = jexl.autocomplete('name', 10, context) // Cursor beyond expression length
console.log(result1.suggestions) // []

// Malformed expressions
const result2 = jexl.autocomplete('user..', 6, context) // Double dots
console.log(result2.suggestions) // [] (gracefully handled)

// Inside string literals
const result3 = jexl.autocomplete('"hello ', 7, context)
console.log(result3.suggestions) // [] (no suggestions inside strings)
```

### Integration with IDEs and Editors

The autocomplete engine is designed to integrate seamlessly with code editors and IDEs:

```typescript
// Example integration with a text editor
function setupAutocomplete(editor: TextEditor, context: any) {
  editor.onCursorChange((position) => {
    const expression = editor.getText()
    const result = jexl.autocomplete(expression, position, context)

    if (result.suggestions.length > 0) {
      showSuggestions(result.suggestions, result.replaceRange)
    }
  })
}

function showSuggestions(suggestions: AutocompleteSuggestion[], replaceRange?: { start: number; end: number }) {
  // Display suggestions in editor's autocomplete popup
  // Use replaceRange to determine what text to replace
  // Use suggestion metadata for rich display (icons, descriptions, etc.)
}
```

### Performance Considerations

- The autocomplete engine is optimized for fast suggestions even with large context objects
- Suggestions are generated on-demand and cached internally
- String literal detection prevents unnecessary processing inside quoted strings
- Namespace detection is efficient and only checks relevant functions/transforms

## Other implementations

- [PyJEXL](https://github.com/mozilla/pyjexl) - A Python-based JEXL parser and evaluator.
- [JexlApex](https://github.com/the-last-byte/JexlApex) - A Salesforce Apex JEXL parser and evaluator.

## License

Jexl is licensed under the MIT license. Please see `LICENSE.txt` for full details.

## Credits

**Current Maintainer:** [Pawel Uchida-Psztyc (@jarrodek)](https://github.com/jarrodek) - Modernized the library with TypeScript, enhanced type safety, and modern tooling.

**Original Creator:** [Tom Shawver (@TomFrost)](https://github.com/TomFrost) - Created the original Jexl library in 2015.

**Contributors:** Thanks to [all the contributors](https://github.com/TomFrost/Jexl/graphs/contributors) who helped make the original Jexl library great.

The original Jexl was created at [TechnologyAdvice](http://technologyadvice.com) in Nashville, TN.