<h1 align="center"><em>sub</em>script</h1>

<p align="center">Modular expression parser & evaluator</p>
<div align="center">

[![build](https://github.com/dy/subscript/actions/workflows/node.js.yml/badge.svg)](https://github.com/dy/subscript/actions/workflows/node.js.yml) [![npm](https://img.shields.io/npm/v/subscript)](http://npmjs.org/subscript) [![size](https://img.shields.io/bundlephobia/minzip/subscript?label=size)](https://bundlephobia.com/package/subscript) [![microjs](https://img.shields.io/badge/µjs-subscript-darkslateblue)](http://microjs.com/#subscript) <!--[![demo](https://img.shields.io/badge/play-%F0%9F%9A%80-white)](https://dy.github.io/subscript/)-->

</div>


```js
import subscript from 'subscript'

let fn = subscript('a + b * 2')
fn({ a: 1, b: 3 })  // 7
```

* **Modular** — 40+ pluggable syntax features, see [playground](https://dy.github.io/subscript/)
* **Universal** — minimal syntax tree, see [spec](./spec.md)
* **Fast** — efficient parser, see [benchmarks](#performance)
* **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.

## Presets

[**Subscript**](./docs.md#subscript) – common expressions:

```js
import subscript from 'subscript'

subscript('a.b + c * 2')({ a: { b: 1 }, c: 3 })  // 7
```

[**Justin**](./docs.md#justin) – JSON + expressions + templates + arrows:

```js
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**](./docs.md#jessie) – JSON + expressions + statements, functions (JS subset):

```js
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](./docs.md#presets) for full description.


## Syntax tree

Expressions parse to minimal JSON-compatible tree structure:

```js
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](./spec.md).



## Extension

Add operators, literals or custom syntax. `binary`/`unary`/`token`/`keyword` register parsers; `operator` registers compilers — pair them as needed:

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

```js
import { binary } from 'subscript/feature/justin.js'
binary('∩', 80)
```

See [docs.md](./docs.md) for full API.


## Safety

Blocked by default:
- `__proto__`, `__defineGetter__`, `__defineSetter__`
- `constructor`, `prototype`
- Global access (only context is visible)

```js
subscript('constructor.constructor("alert(1)")()')({})
// undefined (blocked)
```

## Performance

Parsing `a + b * c - d / e + f.g[0](h) + i.j`, 30k iterations:

```
Parse:
  new Function   8ms
  subscript     34ms
  cel-js        39ms
  angular-expr  42ms
  justin        51ms
  jsep          54ms
  jessie        76ms   ← JS subset (statements + functions)
  expr-eval     81ms
  oxc           84ms   ← full JS parser (native Rust)
  mathjs       185ms
  jexl         403ms

Eval:
  subscript      3ms
  new Function   4ms
  cel-js        13ms
  expression-eval 14ms
  mathjs        17ms
  angular-expr  62ms
```

Run via `node --import ./test/https-loader.js test/benchmark.js`.

## Utils

### Codegen

Convert tree back to code:

```js
import { codegen } from 'subscript/util/stringify.js'

codegen(['+', ['*', 'min', [,60]], [,'sec']])
// 'min * 60 + "sec"'
```

### Bundle

Bundle imports into a single file:

```js
// 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 }"
```


## Used by

* [jz](https://github.com/dy/jz) — JS subset → WASM compiler
* [sprae](https://github.com/dy/jz) — DOM framework
<!-- * [prepr](https://github.com/dy/prepr) -->
<!-- * [glsl-transpiler](https://github.com/stackgl/glsl-transpiler) -->
<!-- * [piezo](https://github.com/dy/piezo) -->


## Alternatives

<sup>[cel-js](https://github.com/marcbachmann/cel-js?tab=readme-ov-file), [jsep](https://github.com/EricSmekens/jsep), [jexl](https://github.com/TomFrost/Jexl), [expr-eval](https://github.com/silentmatt/expr-eval), [math.js](https://mathjs.org/), [mozjexl](https://github.com/mozilla/mozjexl), [jexpr](https://github.com/justinfagnani/jexpr), [expression-eval](https://github.com/donmccurdy/expression-eval), [string-math](https://github.com/devrafalko/string-math), [nerdamer](https://github.com/jiggzson/nerdamer), [math-codegen](https://github.com/mauriciopoppe/math-codegen), [math-parser](https://www.npmjs.com/package/math-parser), [nx-compile](https://github.com/nx-js/compiler-util), [built-in-math-eval](https://github.com/mauriciopoppe/built-in-math-eval)</sup>

<br>

<p align=center><a href="https://github.com/krsnzd/license/">ॐ</a></p>
