# 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!)
```

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

### 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']`. However, the `==` operator is used
behind-the-scenes to search arrays, so it should not be used with arrays of
objects. The following expression returns false: `{a: 'b'} in [{a: 'b'}]`.

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

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

```javascript
jexl.addTransform('split', (val, char) => val.split(char))
jexl.addTransform('lower', (val) => 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](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 |

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

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