UNPKG

expression-language

Version:

Javascript implementation of symfony/expression-language

328 lines (252 loc) 13.9 kB
# Javascript implementation of the Symfony/ExpressionLanguage The idea is to be able to evaluate the same expressions client-side (in Javascript with this library) and server-side (in PHP with the Symfony/ExpressionLanguage). [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ## Feature parity Below is the current parity of this library with Symfony's ExpressionLanguage features. All items default to supported status. | Category | Feature | Supported | |-------------------------|------------------------------------------------------------------------------|-----------| | Literals | Strings (single and double quotes) | ✅ | | Literals | Numbers (integers, decimals, decimals without leading zero) with underscores | ✅ | | Literals | Arrays (JSON-like [ ... ]) | ✅ | | Literals | Hashes/Objects (JSON-like { key: value }) | ✅ | | Literals | Booleans (true/false) | ✅ | | Literals | null | ✅ | | Literals | Exponential/scientific notation | ✅ | | Literals | Block comments /* ... */ inside expressions | ✅ | | Escapes | Backslash escaping in strings and regexes | ✅ | | Escapes | Control characters need escaping (e.g., \n) | ✅ | | Objects | Access public properties with dot syntax (obj.prop) | ✅ | | Objects | Call methods with dot syntax (obj.method(...)) | ✅ | | Objects | Null-safe operator (obj?.prop / obj?.method()) | ✅ | | Nullish | Null-coalescing operator (a ?? b) | ✅ | | Functions | constant() | ✅ | | Functions | enum() | ✅ | | Functions | min() | ✅ | | Functions | max() | ✅ | | Arrays | Access array items with bracket syntax (arr[...]) | ✅ | | Operators: Arithmetic | +, -, *, /, %, ** | ✅ | | Operators: Bitwise | &, \| , ^ | ✅ | | Operators: Bitwise | ~ (not), <<, >> | ✅ | | Operators: Comparison | ==, ===, !=, !==, <, >, <=, >= | ✅ | | Operators: Comparison | matches (regex) | ✅ | | Operators: String tests | contains, starts with, ends with | ✅ | | Operators: Logical | not/!, and/&&, or/\|\|, xor | ✅ | | Operators: String | ~ (concatenation) | ✅ | | Operators: Array | in, not in (strict comparison) | ✅ | | Operators: Numeric | .. (range) | ✅ | | Operators: Ternary | a ? b : c, a ?: b, a ? b | ✅ | | Other | Null-safe operator (?.) | ✅ | | Other | Null-coalescing operator (??) | ✅ | | Precedence | Operator precedence as per Symfony docs | ✅ | | fromPhp() | Supported as fromJavascript() | ✅ | | Symfony Built-ins | Security expression variables | ⛔️ | | Symfony Built-ins | Service container expression variables | ⛔️ | | Symfony Built-ins | Routing expression variables | ⛔️ | > Notes: Symfony Built-ins are not supported in the javascript environment ## Installation ### NPM/Yarn ```bash npm install expression-language # or yarn add expression-language ``` ### Browser You can also use this library directly in the browser by including it via a script tag: ```html <!-- Unminified version for development --> <script src="https://unpkg.com/expression-language/dist/expression-language.js"></script> <!-- or minified version for production --> <script src="https://unpkg.com/expression-language/dist/expression-language.min.js"></script> ``` ## Examples ### NPM/Yarn Setup ```javascript import {ExpressionLanguage} from "expression-language"; let expressionLanguage = new ExpressionLanguage(); ``` ### Browser Setup ```html <script src="https://unpkg.com/expression-language/dist/expression-language.min.js"></script> <script> // The library is available as a global ExpressionLanguage object const expressionLanguage = new ExpressionLanguage.ExpressionLanguage(); </script> ``` A complete browser example is available in the [examples/browser-usage.html](examples/browser-usage.html) file. #### Basic ```javascript let result = expressionLanguage.evaluate('1 + 1'); // result is 2. ``` #### Multiple clauses ```javascript let result = expressionLanguage.evaluate( 'a > 0 && b != a', { a: 1, b: 2 } ); // result is true ``` #### Object and Array access ```javascript let expression = 'a[2] === "three" and b.myMethod(a[1]) === "bar two"'; let values = { a: ["one", "two", "three"], b: { myProperty: "foo", myMethod: function (word) { return "bar " + word; } } }; let result = expressionLanguage.evaluate(expression, values); // result is true ``` #### Registering custom functions You can register functions in two main ways. Make sure to register functions before calling evaluate(), compile(), or parse(); otherwise a LogicException will be thrown. - Using register(name, compiler, evaluator): ```javascript import { ExpressionLanguage } from 'expression-language'; const el = new ExpressionLanguage(); // Define how the function should compile to JavaScript and how it should evaluate at runtime. el.register( 'double', // compiler: receives the compiled argument strings and must return JS source (x) => `((+${x}) * 2)`, // evaluator: receives (values, ...args) and returns the result (values, x) => Number(x) * 2 ); console.log(el.evaluate('double(21)')); // 42 console.log(el.compile('double(a)', ['a'])); // '((+a) * 2)' ``` - Using addFunction with an ExpressionFunction instance: ```javascript import { ExpressionLanguage, ExpressionFunction } from 'expression-language'; const el = new ExpressionLanguage(); const timesFn = new ExpressionFunction( 'times', (a, b) => `(${a} * ${b})`, (values, a, b) => a * b ); el.addFunction(timesFn); console.log(el.evaluate('times(6, 7)')); // 42 ``` #### Using providers Providers are a convenient way to bundle and register multiple functions. A provider exposes a getFunctions() method that returns an array of ExpressionFunction instances. You can register providers via the constructor or with registerProvider(). - Built-in providers you can use out of the box: - BasicProvider: isset() - StringProvider: strtolower, strtoupper, explode, strlen, strstr, stristr, substr - ArrayProvider: implode, count, array_intersect - DateProvider: date, strtotime - Registering built-in providers: ```javascript import { ExpressionLanguage, StringProvider, ArrayProvider, DateProvider, BasicProvider } from 'expression-language'; // Pass providers in the constructor (array or any iterable) const el = new ExpressionLanguage(null, [ new StringProvider(), new ArrayProvider(), new DateProvider(), new BasicProvider(), ]); console.log(el.evaluate('strtoupper("hello")')); // 'HELLO' console.log(el.evaluate('count([1,2,3])')); // 3 console.log(el.evaluate('isset(foo.bar)', { foo: { bar: 1 } })); // true ``` - Creating your own provider: ```javascript import { ExpressionLanguage, ExpressionFunction } from 'expression-language'; class MathProvider { getFunctions() { return [ new ExpressionFunction( 'clamp', (x, min, max) => `Math.min(${max}, Math.max(${min}, ${x}))`, (values, x, min, max) => Math.min(max, Math.max(min, x)) ), new ExpressionFunction( 'pct', (value, total) => `(((${value}) / (${total})) * 100)`, (values, value, total) => (value / total) * 100 ) ]; } } const el = new ExpressionLanguage(); el.registerProvider(new MathProvider()); console.log(el.evaluate('clamp(150, 0, 100)')); // 100 console.log(el.evaluate('pct(2, 8)')); // 25 ``` #### Using ExpressionFunction.fromJavascript() Use this helper to wrap an existing JavaScript function (resolved from the global object) as an ExpressionFunction. Rules and tips: - If you pass a namespaced/dotted path like 'Math.max', you must also provide an explicit expression function name (e.g., 'max'). - For non-namespaced global functions (e.g., 'myFn'), the expression function name defaults to the same name. - The function must exist on globalThis (window in browsers, global in Node). If it does not exist, an error is thrown. Examples: ```javascript import { ExpressionLanguage, ExpressionFunction } from 'expression-language'; const el = new ExpressionLanguage(); // 1) Non-namespaced global function globalThis.mySum = (a, b) => a + b; // or window.mySum in browser const sumFn = ExpressionFunction.fromJavascript('mySum'); el.addFunction(sumFn); console.log(el.evaluate('mySum(20, 22)')); // 42 // 2) Namespaced (dotted) function requires an explicit expression name const maxFn = ExpressionFunction.fromJavascript('Math.max', 'max'); el.addFunction(maxFn); console.log(el.evaluate('max(1, 3, 2)')); // 3 // Note: min/max are already built-in and compile to Math.min/Math.max. ``` > Note: Register functions or providers before calling evaluate(), compile(), or parse(); late registration will throw a LogicException. #### Using IGNORE_* flags These flags let you relax strict validation when parsing expressions via the high-level API. They are useful for linting or building tools where variables/functions may be unknown at parse time. - IGNORE_UNKNOWN_VARIABLES: allows names that are not provided in the names list. - IGNORE_UNKNOWN_FUNCTIONS: allows calling functions that are not registered. - You can combine flags with bitwise OR (|). Examples: ```javascript import { ExpressionLanguage, IGNORE_UNKNOWN_VARIABLES, IGNORE_UNKNOWN_FUNCTIONS } from 'expression-language'; const el = new ExpressionLanguage(); // 1) Allow unknown variables when parsing via ExpressionLanguage el.parse('foo.bar', [], IGNORE_UNKNOWN_VARIABLES); // 2) Allow unknown functions when parsing via ExpressionLanguage el.parse('myFn()', [], IGNORE_UNKNOWN_FUNCTIONS); // 3) Allow both unknown functions and variables el.parse('myFn(foo)', [], IGNORE_UNKNOWN_FUNCTIONS | IGNORE_UNKNOWN_VARIABLES); ``` Linting: ```javascript import { ExpressionLanguage, IGNORE_UNKNOWN_VARIABLES, IGNORE_UNKNOWN_FUNCTIONS } from 'expression-language'; const el = new ExpressionLanguage(); // Validate expressions without executing them el.lint('a > 0 && myFn(foo)', ['a'], IGNORE_UNKNOWN_FUNCTIONS | IGNORE_UNKNOWN_VARIABLES); // By default (flags = 0), unknowns throw: try { el.lint('myFn(foo)'); } catch (e) { console.warn('Lint failed as expected:', e.message); } ``` Notes: - Passing null for the names parameter is deprecated; use IGNORE_UNKNOWN_VARIABLES instead when you want to allow unknown variables. ## Continuous Integration and Deployment This package uses GitHub Actions for automated workflows: 1. **NPM Publishing**: Automatically publishes to npm when the package version changes 2. **GitHub Releases**: Automatically creates GitHub releases with changelogs and distribution files ### For Maintainers If you're maintaining this package, you'll need to set up the following: #### NPM Publishing Set up trusted publisher in npmjs.org by following the instructions [here](https://docs.npmjs.com/trusted-publishers). #### GitHub Releases The GitHub release workflow automatically: - Checks if the package version has changed - Builds the project to generate distribution files - Creates a GitHub release with the new version tag - Generates a changelog based on commit messages - Attaches the distribution files to the release No additional setup is required for GitHub releases as it uses the default `GITHUB_TOKEN`. Once set up, any push to the main branch will trigger these workflows when the package version changes.