Modular expression parser & evaluator
import subscript from 'subscript' let fn = subscript('a + b * 2') fn({ a: 1, b: 3 }) // 7
- Modular — 40+ pluggable syntax features, see playground
- Universal — minimal syntax tree, see spec
- Fast — efficient parser, see benchmarks
- Small — ~2KB core, runs in browser/node
- Safe — sandboxed, no prototype access
- Metacircular — parses and compiles itself
Useful for: templates, calculators, sandboxes, safe eval, language subsets, custom DSLs, preprocessors.
Subscript – common expressions:
import subscript from 'subscript' subscript('a.b + c * 2')({ a: { b: 1 }, c: 3 }) // 7
Justin – JSON + expressions + templates + arrows:
import justin from 'subscript/justin.js' justin('{ x: a?.b ?? 0, y: [1, ...rest] }')({ a: null, rest: [2, 3] }) // { x: 0, y: [1, 2, 3] }
Jessie – JSON + expressions + statements, functions (JS subset):
import jessie from 'subscript/jessie.js' let fn = jessie(` function factorial(n) { if (n <= 1) return 1 return n * factorial(n - 1) } factorial(5) `) fn({}) // 120
Each preset has a parse-only entry — subscript/feature/{subscript,justin,jessie} — that drops the runtime (~50% smaller) for consumers that only need the AST. See docs for full description.
Expressions parse to minimal JSON-compatible tree structure:
import { parse } from 'subscript' parse('a + b * 2') // ['+', 'a', ['*', 'b', [, 2]]] // node kinds 'x' // identifier — resolve from context [, value] // literal — return as-is (empty slot = data) [op, ...args] // operation — apply operator
See spec.md.
Add operators, literals or custom syntax. binary/unary/token/keyword register parsers; operator registers compilers — pair them as needed:
import justin, { binary, operator, compile } from 'subscript/justin.js' // add intersection operator binary('∩', 80) // register parser operator('∩', (a, b) => ( // register compiler a = compile(a), b = compile(b), ctx => a(ctx).filter(x => b(ctx).includes(x)) )) justin('[1,2,3] ∩ [2,3,4]')({}) // [2, 3]
For parse-only consumers, register only the parse half:
import { binary } from 'subscript/feature/justin.js' binary('∩', 80)
See docs.md for full API.
Blocked by default:
__proto__and any other__*propertyconstructor,prototype- Globals outside the supplied context
subscript('constructor')({}) // undefined subscript('constructor.constructor("alert(1)")')({}) // undefined
Context values are capabilities. Do not pass reflection helpers such as Reflect.get or Object.getOwnPropertyDescriptor to untrusted expressions.
Parsing a + b * c - d / e + f.g[0](h) + i.j, 30k iterations — each iteration parses an unseen source, so source-keyed compile caches (new Function, angular-expressions) don't help:
Parse:
subscript 26ms
justin 35ms
cel-js 45ms
jsep 46ms
jessie 51ms ← JS subset (statements + functions)
expr-eval 74ms
oxc 86ms ← full JS parser (native Rust)
mathjs 156ms
new Function 166ms
jexl 304ms
angular-expr 48s
Eval:
new Function 0.2ms
subscript 1.7ms
cel-js 10ms
mathjs 10ms
expression-eval 24ms
angular-expr 44ms
Run via node --import ./test/https-loader.js test/benchmark.js.
Convert tree back to code:
import { codegen } from 'subscript/util/stringify.js' codegen(['+', ['*', 'min', [,60]], [,'sec']]) // 'min * 60 + "sec"'
Bundle imports into a single file:
// Node.js import { bundleFile } from 'subscript/util/bundle.js' console.log(await bundleFile('jessie.js')) // Browser / custom sources import { bundle } from 'subscript/util/bundle.js' console.log(await bundle('main.js', { 'main.js': `import { x } from './lib.js'; export default x * 2`, 'lib.js': `export const x = 21` })) // → "const x = 21;\nexport { x as default }"
cel-js, jsep, jexl, expr-eval, math.js, mozjexl, jexpr, expression-eval, string-math, nerdamer, math-codegen, math-parser, nx-compile, built-in-math-eval