npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@rosell/js-expression

v0.3.1

Published

Evaluate javascript expressions. No eval. No dependencies. Only 6kb.

Downloads

3

Readme

js-expression

Evaluate javascript expressions. No eval. No dependencies. Only 6kb.

Includes a tokenizer, a parser and an evaluator.

Usage:

Install:

npm i @rosell/js-expression

Use

import { JsExpression } from '@rosell/js-expression';
// or one of the following:
// const { JsExpression } = require('@rosell/js-expression');   // CJS
// import JsExpression from '@rosell/js-expression';            // ESM, just one class
// import { JsExpression, Tokenizer, Evaluator, Parse } from '@rosell/js-expression';    // ESM, all four classes

let e = new JsExpression('(1+1)*3');
let result = e.evaluate();   // evaluates to 6

Context (variables and functions) can be set like this:

let e = new JsExpression('add(shoeSize,1)');
let localContext = {
  'shoeSize': 10,
  'add' => (a,b) => a+b
}
e.setLocalContext(localContext);
let result = e.evaluate();   // evaluates to 11

Revaluating is cheap, as there is no need for reparsing.

localContext['shoeSize'] = 20;
let result2 = e.evaluate(localContext);   // Now evaluates to 21

If you have several evaluations that are to run in the same context, you set a global context like this:

let globalContext = {
  'substract', (a,b) => a-b),
  'PI', Math.PI
}
JsExpression.setGlobalContext(globalContext);

let e = new JsExpression('substract(PI,PI)');
let result = e.evaluate();   // evaluates to 0

If you provide a name for the global context, you can easily switch:

let sweden = {
  lang: 'swedish'
}
let denmark = {
  lang: 'danish'
}
JsExpression.setGlobalContext(sweden, 'se');
JsExpression.setGlobalContext(denmark, 'dk');
let e = new JsExpression("'they speak: ' + lang");
e.evaluate('se')    // evaluates to "they speak swedish"
e.evaluate('dk')    // evaluates to "they speak danish"

Security:

console.log(new JsExpression('window').evaluate());
// undefined

console.log(new JsExpression('global').evaluate());
// undefined

JsExpression.setGlobalContext({'myString': 'hello'});
new JsExpression('myString.toString()').evaluate());
// throws "Function does not exist: toString"

To only tokenize or parse, use the tokenize() and parse() methods. Example:

let e = new JsExpression('1+2');
let tokens = e.tokenize();  // tokens: [[LITERAL, '1'], [INFIX_OP, '+'], [LITERAL, '2']]
let tokenValues = tokens.map(function(a) {return a[1]});    // result: [1, '+', 2]
let rpnTokens = e.parse();  // Reorders tokens to rpn (and removes parenthesises)
let rpnTokenValues = rpnTokens.map(function(a) {return a[1]});    // result: [1, 2, '+']

How it works

The library contains three engines: (Tokenizer, Parser and Evaluator), each in a class of its own, and the JsExpression class, which is for convenience.

Tokenizer

Converts a string to tokens. A token consists of type information and value.

Examples:

| in | out | | ------- | ----------------- | | 7 | [LITERAL, 7] | | * | [INFIX_OP, "*"] | | ! | [PREFIX_OP, "!"] |

Tokonization time is O(n)

Parser

The parser parses tokens into a rpn list (reverse polish notation). Such a list is very suited for being evaluated.

Examples: (not showing the token types for simplicity)

| in | out | | ------- | ------------------- | | 7+1 | [7, 1, '+'] | | 1+23 | [1, 2, 3, '', '+'] | | (1+2)3 | [1, 2, +, 3, ''] | | (1+2)3 | [1, 2, +, 3, ''] | | -73 | [7, '+/-', 3, ''] | | 1+(-7) | [1, 7, '+/-', '+'] |

Note 1: Actual input must be array of tokens, not a string. For example [[LITERAL, 7], [INFIX_OP, "+"], [LITERAL, 1]] rather than "7+1"
Note 2: Actual output is array of tokens, not array of string.

The parser also gets rid of parenthesis. And it also handles unary minus (which it transposes to "+/-") and unary plus (which it removes)

Parsing time is O(n*n)

Evaluator

The evaluator evaluates the tokens and operators in the rpn list.

Examples: (again, for simplicity, not showing token types) | in | out | | ------------ | ------------------- | | [7, 1, '+'] | 8 |

Note: actual input must be array of tokens, not array of string, as the example could lead you to think

Evaluation time is O(n)

JsExpression

Pulls it all together. It takes care of parsing before evaluating and makes sure not to parse again upon subsequent evaluations.

Support

Operators

The following operators are supported:
,, ?, ??, ||, &&, |, ^, &, ==, !=, ===, <, >, <=, >=, >>, <<, >>>, +, -, *, /, %, **, !, ~, typeof, void, .

The following are not:
yield, new, ?. and operators that makes assignments (=, ++, --, etc) in and instanceof (forgot about those - will be added monday)

Literals

The following literals are supported:
numbers, strings, true, false, Arrays - ie [1,2], Objects - ie {one: 1} or {'one': 1}, undefined, null, NaN

The following are not:
Regular expressions

Other features

The following features are supported:
Grouping with parenthesis, unary plus, unary minus, member access - ie obj.color and object['colo' + 'r']

The following are not:

  • running other functions than those you add
  • accessing other variables than those you set
  • changing supplied variables "from within" (no operators that makes assignments)
  • function constructors
  • semicolon
  • comments

Known bugs

  • Mixing of dynamic and static property accessors in some cases fails. For now, avoid ie obj["address"].street and instead go with obj["address"]["street"] or obj.address.street.
  • Array constructors cannot take empty places, like: [1,,2]
  • Literal numbers not fully supported (only decimal base, no exp). number spec here

Whats out there, besides this?

Why did I create this?

I needed something like this, but couldn't find exactly what I needed out there. And the challenge seemed like fun (and turned out to be). Had the basic engines running in three days and spent another three days implementing unary plus/minus, object/array literals, object accessors and the ternary operator.