Peg.js in AngularJS webapp

593 views Asked by At

I have an AngularJS web application.

I'd like to use peg.js in my application. I've just written a peg.js grammar: CriteriaValue.pegjs and generated the parser with the command line: pegjs CriteriaValue.pegjs, which generated CriteriaValue.js.

Could someone explain to me how to use the parser ?

var result = parser.parse('my string'); doesn't work.

I've created a plunker: http://plnkr.co/edit/Ae05SeZAjKOQ75B3lvLc?p=preview

1

There are 1 answers

2
tavnab On BEST ANSWER

Short Answer

  • In CriteriaValue.js, change module.exports in the first line to parser
  • In index.html, swap the <script> tags so that CriteriaValue.js comes first
  • (Optional) In script.js, output the results of your parse as a formatted JSON string, in order to see the actual values

Here's the plunker: http://plnkr.co/edit/kiBp2Na9s4PXpenCzQjx?p=preview

Long Answer

Run your original plunker and check the console logs; you'll notice 2 errors:

  • ReferenceError: Can't find variable: parser (script.js:3)
  • ReferenceError: Can't find variable: error (CriteriaValue.js:1)

The first error is due to the fact that no parser object is ever created in global scope, by script.js or by CriteriaValue.js.

Looking at CriteriaValue.js, you can see it's actually assigning the generated parser object to a non-existent modules.export. This is the default behavior of PEG.js, as it assumes you're going to use your parser with node.js. The reason you're seeing the error is that there is no module object, so we can't assign to this non-existent object's export property. Changing the assignment to parser, which is something we can assign to (because PEG.js doesn't use strict mode), avoids this error and makes parser available for use in script.js.

Finally, the parser needs to be created before script.js can use it; hence the reason for the <script> swap.

For future creation of CriteriaValue.js, do it like this:

pegjs --export-var parser CriteriaValue.pegjs

This will generate the file so that the object is assigned to the variable parser instead of module.exports.

Where AngularJS Comes In

As @dirkk said in the comments, defining the parser as a global variable is bad practice and certainly not the AngularJS way, which is to implement your parser as a service.

The quickest (but not necessarily best) way to do that is to take your already generated CriteriaValue.js code and wrap a service around it. e.g.:

angular.module('yourApp.services', [])
.factory('Parser', function() {
  // The generated code, except replace "parser = " with "return "
});

Another option is to fetch the .pegjs file & generate your parser on the client using PEG.buildParser():

angular.module('yourApp.services', [])
.factory('Parser', ['$http', '$q', function($http, $q) {
  var deferred = $q.defer();
  $http.get('path/to/CriteriaValue.pegjs')
  .success(function(grammar) {
    try {
      deferred.resolve(PEG.buildParser(grammar));
    } catch (e) {
      deferred.reject(e);
    }
  })
  .error(function(message) {
    deferred.reject('Unable to load grammar: ' + message);
  });

  return deferred.promise;
}]);

This makes updating your grammar easier, as you won't have to rewrite your service every time, but it adds a delay in loading your app. The feasibility of this depends on how complex your grammar is & how often it actually needs to change.

Despite how you build your parser, you don't necessarily need to expose the generated parser object directly to the rest of your Angular app. Instead, you can implement a higher-level API for what your app will actually do with this parser (e.g. validate(input), getAST(input), etc...). This way, if you decide in the future to switch to a different parser (e.g. Jison), you'll have much less code to change.