Parses logical expressions like REGISTED&(SPECIAL|INVITED) into an AST and evaluates them with your own token checker. Suitable for permissions management. Written in TypeScript, zero runtime dependencies, ships both CommonJS and ESM builds.
| Operator | Meaning | Precedence |
|---|---|---|
! |
Not | highest |
& |
And | middle |
| |
Or | lowest |
(...) |
Grouping | — |
&binds tighter than|:A&B|Cmeans(A&B)|C.&and|are left-associative.- Whitespace between tokens is ignored.
- Literals are any runs of characters other than whitespace and the operators above (unicode included).
- You require permissions such as
REGISTED&(SPECIAL|INVITED). - The parser builds an AST and passes each literal (
REGISTED,SPECIAL,INVITED) to your token checking function. - The AST is evaluated with short-circuiting, so your checker is only called for tokens that can still change the result.
const { parse } = require('logical-expression-parser');
const REQUIREMENTS = 'REGISTED&(SPECIAL|INVITED)';
const LIST_A = ['REGISTED', 'INVITED'];
const LIST_B = ['SPECIAL', 'EXPERT'];
const RESULT_A = parse(REQUIREMENTS, token => LIST_A.includes(token));
const RESULT_B = parse(REQUIREMENTS, token => LIST_B.includes(token));
// RESULT_A: true
// RESULT_B: falseimport { parse } from 'logical-expression-parser';
// same as aboveimport { parseAst, evaluate } from 'logical-expression-parser';
const ast = parseAst('REGISTED&!(BANNED)');
// {
// type: 'and',
// left: { type: 'literal', value: 'REGISTED' },
// right: { type: 'not', operand: { type: 'literal', value: 'BANNED' } }
// }
const allowed = evaluate(ast, token => userPermissions.includes(token));For permission checks requiring database or network calls, use parseAsync or evaluateAsync:
import { parseAsync } from 'logical-expression-parser';
const allowed = await parseAsync('ADMIN | (SPECIAL & !BANNED)', async token => {
return await checkUserPermission(userId, token);
});Like the synchronous API, evaluation short-circuits so subsequent async checks are skipped once the outcome is determined.
You can register custom binary and prefix unary operators with their own precedence, associativity, and evaluation logic using createParser:
import { createParser } from 'logical-expression-parser';
const parser = createParser({
customOperators: [
{
kind: 'binary',
symbol: '^',
precedence: 15, // between OR (10) and AND (20)
associativity: 'left',
evaluate: (left, right) => left !== right(),
evaluateAsync: async (left, right) => left !== (await right()),
},
{
kind: 'binary',
symbol: '->',
precedence: 5, // looser than OR
associativity: 'right',
// Short-circuiting: if left is false, right() is skipped
evaluate: (left, right) => (!left ? true : right()),
evaluateAsync: async (left, right) => (!left ? true : await right()),
},
{
kind: 'prefix',
symbol: '~',
precedence: 30, // same as '!'
evaluate: operand => !operand,
evaluateAsync: async operand => !(await operand),
},
],
});
const allowed = parser.parse('ADMIN ^ (USER -> BETA)', checker);|(OR):10&(AND):20!(NOT):30
Word operators (e.g. XOR, NAND) are also supported with word-boundary safety (they won't split literals like XOR_GATE).
export type TokenChecker = (token: string) => boolean;
export type AsyncTokenChecker = (token: string) => boolean | Promise<boolean>;
export type LiteralNode = { readonly type: 'literal'; readonly value: string };
export type NotNode = { readonly type: 'not'; readonly operand: AstNode };
export type BinaryNode = {
readonly type: 'and' | 'or';
readonly left: AstNode;
readonly right: AstNode;
};
export type CustomBinaryNode = {
readonly type: 'custom_binary';
readonly operator: string;
readonly left: AstNode;
readonly right: AstNode;
};
export type CustomUnaryNode = {
readonly type: 'custom_unary';
readonly operator: string;
readonly operand: AstNode;
};
export type AstNode =
| LiteralNode
| NotNode
| BinaryNode
| CustomBinaryNode
| CustomUnaryNode;Malformed expressions — empty inputs, A&, &A, (), unbalanced parentheses, stray tokens — throw LEPSyntaxError, a SyntaxError subclass carrying the character offset. Invalid argument types (e.g. non-string expression or non-function checker) throw TypeError.
import { parse, LEPSyntaxError } from 'logical-expression-parser';
try {
parse('A&(B|C', checker);
} catch (error) {
if (error instanceof LEPSyntaxError) {
// error.offset === 6
}
}Requires Node >= 18.
npm install
npm test # builds dist/, compiles tests, runs node --test with a c8 coverage report, smoke-checks both dist buildsSources live in src/, tests in test/; npm run build emits dist/cjs and dist/esm.
Releases publish to npm from GitHub Actions when a v-tag is pushed:
- Bump
versioninpackage.jsonand commit. git tag v<version> && git push origin v<version>— the tag must matchversionexactly (enforced by the workflow).- The workflow builds, runs the full test suite, and publishes with provenance using npm Trusted Publishing (OIDC keyless publishing; configure your repository once under Package Settings → Trusted Publishing on npmjs.com).