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

@floip/expression-parser

v1.11.3

Published

This project has everything needed to parse and evaluate expressions as defined in [The Flow Expressions Specification](https://floip.gitbooks.io/flow-specification/content/fundamentals/expressions.html)

Downloads

396

Readme

Floip Expression Evaluator

This project has everything needed to parse and evaluate expressions as defined in The Flow Expressions Specification

The expression parser is written in PEG and compiled with PEG.js into both a js target and a php target.

Building

The included makefile should be used to build the parsers:

make, the default task, will build the js and php PEG parsers using docker for the environment by default. You can set the environment variable USE_DOCKER to anything but 'true' to skip using docker -- e.g. make USE_DOCKER=false

Tests via phpunit are included in /tests and should be run with make test These tests will ensure compatibility with Laravel 5.1 and 5.8. These tests will ensure compatibility with PHP 5.5 and PHP 7.1. These tests are run automatically when a push is made to bitbucket.

Releasing

  1. Always rebuild the parsers and run the evaluator tests
  2. Tag the release with the new semantic version (e.g. git tag v1.0.3)
  3. Push the tag (e.g. git push --tags)

Including in a Viamo project

PHP

Add the repository to your composer.json like so:

    "repositories": [
        {
            "type": "vcs",
            "url": "[email protected]:voto/floip-expression-evaluator.git"
        }
    ],

And then add this package as a new dependency: composer require viamo/floip-eval "^1.0"

or add directly to your composer.json and composer update:

    "require": {
        "viamo/floip-eval": "^1.0"
    }

NPM

Add the repository to your package.json like so:

  "dependencies": {
    "floip-eval": "bitbucket:voto/floip-expression-evaluator#v1.0.1"
  }

And then npm install

How to add a new Expression type

To show how the different parts of this project work together, let's go through an example of adding a new expression type to parse and evaluate.

The expression type we will add will look for a string like:

Math is easy 1, 2, @(1 + 1 + 1)

and evaluate it to

Math is easy as 1, 2, 3

1. Adding PEG grammar

The first thing we should do is add our expression to the grammar in src/pegjs/floip.pegjs.

Each definition in this file is built on rules, which themselves can be built on other rules. Ultimately, each rule will match some text.

1. Start = (Expression / $Text+)*

2. Expression = Escaped_Identifier / Closed_Expression / Open_Expression

3. Closed_Expression = id:Expression_Identifier OpenParen ex:Expression_Types cp:(CloseParen {return location() /**<?php return call_user_func($this->_location); ?> **/})

4. Expression_Types = Escaped_Identifier / Logic / Concatenation / Function / Member_Access
  1. We can see that the grammar starts by looking for either an expression or text:
  2. Our grammar is a closed expression since it's enclosed in parens, so we will add it to the rules of Closed_Expression.
  3. We can see here that a Closed_Expression matches Expression_Types:
  4. This is where we want to add our new expression type grammar!

We will add a new rule (Math) to Expression_Types:

Expression_Types = Escaped_Identifier / Math / Logic / Concatenation / Function / Member_Access

And we can then define the rule:

1. Math = lhs:Math_Arg ws* op:$math_chars ws+ rhs:(Math / Math_Arg) ws* {
  return new math(lhs, rhs, op, location())
  /** <?php
    return call_user_func_array($this->_math, [$lhs, $rhs, $op]);
  ?> **/
}

2. Math_Arg = Math_Arg_Inner_Math / Function / Member_Access / $numbers+
3. Math_Arg_Inner_Math = OpenParen child:Math CloseParen { return child; /**<?php return $child; ?>**/}
4. math_chars = [-+*\^/]
  1. In our new rule, we an argument that is followed by a math operator and another argument.
  2. It's a good idea to specify arguments in another rule, so that we can add to them later (like how we did with Expression_Types). Rules in the form foo / bar match from left to right, so it's important to consider precedence!

Building objects in your AST

You may have noticed that there was some object creation happening in our new Math expresion. For our AST, we want expressions to be represented as objects so that we can describe the properties of the expression, such as expression arguments and location in the string.

At the top of our grammar file (src/pegjs/floip.pegjs) we can define variables and functions which we can use in rules:

  var math = function(lhs, rhs, operator, location) {
    return {
      type: 'MATH',
      lhs: lhs,
      rhs: rhs,
      operator: operator,
      location: location
    }
  }

Following every rule, we can create a predicate that does something with the matching text. In our Math rule, we build a Math object and return it into our AST.

We can also use PHP in our predicates by surrounding the code with a special comment syntax:

  /** <?php
  $this->_math = function($lhs, $rhs, $operator) {
    return [
      'type' => 'MATH',
      'lhs' => $lhs,
      'rhs' => $rhs,
      'operator' => $operator,
      'location' => call_user_func($this->_location)
    ];
  };
  ?> **/

When we parse our string now, we should get an AST that looks like this:

[
   "Math is easy 1, 2, ",
   {
      "type": "MATH",
      "lhs": "1",
      "rhs": {
         "type": "MATH",
         "lhs": "1",
         "rhs": "1",
         "operator": "+",
         "location": {
            "start": {
               "offset": 25,
               "line": 1,
               "column": 26
            },
            "end": {
               "offset": 30,
               "line": 1,
               "column": 31
            }
         }
      },
      "operator": "+",
      "location": {
         "start": {
            "offset": 19,
            "line": 1,
            "column": 20
         },
         "end": {
            "offset": 31,
            "line": 1,
            "column": 32
         }
      }
   }
]

Evaluating the new expression type

Now that we have our new grammar, we generate the parsers: make

We now need to handle the new expression type in our evaluator.

src/php/Evaluator.php

Evaluator::evaluate takes a string and a context and produces a string with the evaluated values of each expression contained within the string.

Each expression in the AST is transformed into a Node object, and each Node has a type -- such as "Math".

So, in order to evaluate our new expression type, we need to create a new NodeEvaluator and register it with our Evaluator.

We will create MathNodeEvaluator in the namespace Viamo\Floip\Evaluator\MathNodeEvaluator:

<?php

namespace Viamo\Floip\Evaluator;
use Viamo\Floip\Contract\EvaluatesExpression;

class MathNodeEvaluator implements EvaluatesExpression
{
    public function evaluate(Node $node, $context) {
        // a naive implementation
        $lhs = $node['lhs'];
        $rhs = $node['rhs'];
        return $lhs + $rhs;
    }

    public function handles() {
        return 'MATH';
    }
}

Note that our MathNodeEvaluator implements EvaluatesExpression. To add a new NodeEvaluator to the Evaluator, it must follow this contract.

The Node object we are passed can be accessed just like the plain array it was defined as in our PEG grammar.

Registering the NodeEvaluator

Now, when we are ready to instantiate and use our Evaluator, we will register our Math evaluator before evaluating an expression:

   $evaluator = new Evaluator(new Parser);
   $evaluator->addNodeEvaluator(new MathNodeEvaluator);

   $result = $evaluator->evaluate('Math is easy 1, 2, @(1 + 1 + 1)', []);

We should now get Math is easy 1, 2, 3 as the value of $result.

Writing Tests

You should write tests for your new NodeEvaluator!

Service Provider

Of course, we typically will want all of our NodeEvaluators registered when using the Evaluator in a production environment, so we will provide a ServiceProvider to do so in src/php/Providers/ExpressioEvaluatorServiceProvider.

We can register our new NodeEvaluator in the getNodeEvaluators method, which returns an array of NodeEvaluators:

    protected function getNodeEvaluators()
    {
        return [
            ...
            new MathNodeEvaluator, // Our new class
        ];
    }

Now, after registering our service provider in Laravel, we can get the evaluator instance with NodeEvaluators already registered by typehinting it in the constructor of the class in which we want to use it:

class EvaluationController {

    protected $evaluator;
    protected $user;

    public function __construct(Evaluator $evaluator, User $user) {
        $this->evaluator = $evaluator;
        $this->user = $user;
    }

    public function evaluate(Request $request) {
        return $this->evaluator->evaluate($request->get('expression'), $this->user->getContext());
    }
}