Coder Social home page Coder Social logo

here-be / snapdragon Goto Github PK

View Code? Open in Web Editor NEW
221.0 7.0 25.0 212 KB

snapdragon is an extremely pluggable, powerful and easy-to-use parser-renderer factory.

Home Page: https://github.com/here-be

License: MIT License

JavaScript 100.00%
parser compiler ast parse compile tokenize render lexer token source-map

snapdragon's Introduction

snapdragon NPM version NPM monthly downloads NPM total downloads Linux Build Status

Easy-to-use plugin system for creating powerful, fast and versatile parsers and compilers, with built-in source-map support.

Please consider following this project's author, Jon Schlinkert, and consider starring the project to show your ❤️ and support.

Table of Contents

Details

Install

Install with npm:

$ npm install --save snapdragon

Created by jonschlinkert and doowb.

Features

  • Bootstrap your own parser, get sourcemap support for free
  • All parsing and compiling is handled by simple, reusable middleware functions
  • Inspired by the parsers in pug and css.

Quickstart example

All of the examples in this document assume the following two lines of setup code exist first:

var Snapdragon = require('snapdragon');
var snapdragon = new Snapdragon();

Parse a string

var ast = snapdragon.parser
  // parser handlers (essentially middleware)
  // used for parsing substrings to create tokens
  .set('foo', function () {})
  .set('bar', function () {})
  .parse('some string', options);

Compile an AST returned from .parse()

var result = snapdragon.compiler
  // compiler handlers (essentially middleware), 
  // called on a node when the `node.type` matches
  // the name of the handler
  .set('foo', function () {})
  .set('bar', function () {})
  // pass the `ast` from the parse method
  .compile(ast)

// the compiled string
console.log(result.output);

See the examples.

Parsing

Parser handlers

Parser handlers are middleware functions responsible for matching substrings to create tokens:

Example handler

var ast = snapdragon.parser
  .set('dot', function() {
    var pos = this.position();
    var m = this.match(/^\./);
    if (!m) return;
    return pos({
      // the "type" will be used by the compiler later on,
      // we'll go over this in the compiler docs
      type: 'dot',
      // "val" is the string captured by ".match",
      // in this case that would be '.'
      val: m[0]
    });
  })
  .parse('.'[, options])

As a side node, it's not scrictly required to set the type on the token, since the parser will add it to the token if it's undefined, based on the name of the handler. But it's good practice since tokens aren't always returned.

Example token

And the resulting tokens look something like this:

{ 
  type: 'dot',
  val: '.' 
}

Position

Next, pos() is called on the token as it's returned, which patches the token with the position of the string that was captured:

{ type: 'dot',
  val: '.',
  position:
   { start: { lineno: 1, column: 1 },
     end: { lineno: 1, column: 2 } }}

Life as an AST node

When the token is returned, the parser pushes it onto the nodes array of the "previous" node (since we're in a tree, the "previous" node might be literally the last node that was created, or it might be the "parent" node inside a nested context, like when parsing brackets or something with an open or close), at which point the token begins its life as an AST node.

Wrapping up

In the parser calls all handlers and cannot find a match for a substring, an error is thrown.

Assuming the parser finished parsing the entire string, an AST is returned.

Compiling

The compiler's job is to take the AST created by the parser and convert it to a new string. It does this by iterating over each node on the AST and calling a function on the node based on its type.

This function is called a "handler".

Compiler handlers

Handlers are named middleware functions that are called on a node when node.type matches the name of a registered handler.

var result = snapdragon.compiler
  .set('dot', function (node) {
    console.log(node.val)
    //=> '.'
    return this.emit(node.val);
  })

If node.type does not match a registered handler, an error is thrown.

Source maps

If you want source map support, make sure to emit the entire node as the second argument as well (this allows the compiler to get the node.position).

var res = snapdragon.compiler
  .set('dot', function (node) {
    return this.emit(node.val, node);
  })

All together

This is a very basic example, but it shows how to parse a dot, then compile it as an escaped dot.

var Snapdragon = require('..');
var snapdragon = new Snapdragon();

var ast = snapdragon.parser
  .set('dot', function () {
    var pos = this.position();
    var m = this.match(/^\./);
    if (!m) return;
    return pos({
      type: 'dot',
      val: m[0]
    })
  })
  .parse('.')

var result = snapdragon.compiler
  .set('dot', function (node) {
    return this.emit('\\' + node.val);
  })
  .compile(ast)

console.log(result.output);
//=> '\.'

API

Create a new Parser with the given input and options.

Params

  • input {String}
  • options {Object}

Example

var Snapdragon = require('snapdragon');
var Parser = Snapdragon.Parser;
var parser = new Parser();

Throw a formatted error message with details including the cursor position.

Params

  • msg {String}: Message to use in the Error.
  • node {Object}
  • returns {undefined}

Example

parser.set('foo', function(node) {
  if (node.val !== 'foo') {
    throw this.error('expected node.val to be "foo"', node);
  }
});

Define a non-enumberable property on the Parser instance. This is useful in plugins, for exposing methods inside handlers.

Params

  • key {String}: propery name
  • val {any}: property value
  • returns {Object}: Returns the Parser instance for chaining.

Example

parser.define('foo', 'bar');

Create a new Node with the given val and type.

Params

  • val {Object}
  • type {String}
  • returns {Object}: returns the Node instance.

Example

parser.node('/', 'slash');

Mark position and patch node.position.

  • returns {Function}: Returns a function that takes a node

Example

parser.set('foo', function(node) {
  var pos = this.position();
  var match = this.match(/foo/);
  if (match) {
    // call `pos` with the node
    return pos(this.node(match[0]));
  }
});

Add parser type with the given visitor fn.

Params

  • type {String}
  • fn {Function}

Example

 parser.set('all', function() {
   var match = this.match(/^./);
   if (match) {
     return this.node(match[0]);
   }
 });

Get parser type.

Params

  • type {String}

Example

var fn = parser.get('slash');

Push a node onto the stack for the given type.

Params

  • type {String}
  • returns {Object} token

Example

parser.set('all', function() {
  var match = this.match(/^./);
  if (match) {
    var node = this.node(match[0]);
    this.push(node);
    return node;
  }
});

Pop a token off of the stack of the given type.

Params

  • type {String}
  • returns {Object}: Returns a token

Example

parser.set('close', function() {
  var match = this.match(/^\}/);
  if (match) {
    var node = this.node({
      type: 'close',
      val: match[0]
    });

    this.pop(node.type);
    return node;
  }
});

Return true if inside a "set" of the given type. Sets are created manually by adding a type to parser.sets. A node is "inside" a set when an *.open node for the given type was previously pushed onto the set. The type is removed from the set by popping it off when the *.close node for the given type is reached.

Params

  • type {String}
  • returns {Boolean}

Example

parser.set('close', function() {
  var pos = this.position();
  var m = this.match(/^\}/);
  if (!m) return;
  if (!this.isInside('bracket')) {
    throw new Error('missing opening bracket');
  }
});

Return true if node is the given type.

Params

  • node {Object}
  • type {String}
  • returns {Boolean}

Example

parser.isType(node, 'brace');

Get the previous AST node from the parser.stack (when inside a nested context) or parser.nodes.

  • returns {Object}

Example

var prev = this.prev();

Match regex, return captures, and update the cursor position by match[0] length.

Params

  • regex {RegExp}
  • returns {Object}

Example

// make sure to use the starting regex boundary: "^"
var match = this.match(/^\./);

Params

  • input {String}
  • returns {Object}: Returns an AST with ast.nodes

Example

var ast = parser.parse('foo/bar');

Create a new Compiler with the given options.

Params

  • options {Object}
  • state {Object}: Optionally pass a "state" object to use inside visitor functions.

Example

var Snapdragon = require('snapdragon');
var Compiler = Snapdragon.Compiler;
var compiler = new Compiler();

Throw a formatted error message with details including the cursor position.

Params

  • msg {String}: Message to use in the Error.
  • node {Object}
  • returns {undefined}

Example

compiler.set('foo', function(node) {
  if (node.val !== 'foo') {
    throw this.error('expected node.val to be "foo"', node);
  }
});

Concat the given string to compiler.output.

Params

  • string {String}
  • node {Object}: Optionally pass the node to use for position if source maps are enabled.
  • returns {String}: returns the string

Example

compiler.set('foo', function(node) {
  this.emit(node.val, node);
});

Emit an empty string to effectively "skip" the string for the given node, but still emit the position and node type.

Params

  • {Object}: node

Example

// example: do nothing for beginning-of-string
snapdragon.compiler.set('bos', compiler.noop);

Define a non-enumberable property on the Compiler instance. This is useful in plugins, for exposing methods inside handlers.

Params

  • key {String}: propery name
  • val {any}: property value
  • returns {Object}: Returns the Compiler instance for chaining.

Example

compiler.define('customMethod', function() {
  // do stuff
});

Add a compiler fn for the given type. Compilers are called when the .compile method encounters a node of the given type to generate the output string.

Params

  • type {String}
  • fn {Function}

Example

compiler
  .set('comma', function(node) {
    this.emit(',');
  })
  .set('dot', function(node) {
    this.emit('.');
  })
  .set('slash', function(node) {
    this.emit('/');
  });

Get the compiler of the given type.

Params

  • type {String}

Example

var fn = compiler.get('slash');

Visit node using the registered compiler function associated with the node.type.

Params

  • node {Object}
  • returns {Object}: returns the node

Example

compiler
  .set('i', function(node) {
    this.visit(node);
  })

Iterate over node.nodes, calling visit on each node.

Params

  • node {Object}
  • returns {Object}: returns the node

Example

compiler
  .set('i', function(node) {
    utils.mapVisit(node);
  })

Compile the given AST and return a string. Iterates over ast.nodes with mapVisit.

Params

  • ast {Object}
  • options {Object}: Compiler options
  • returns {Object}: returns the node

Example

var ast = parser.parse('foo');
var str = compiler.compile(ast);

Snapdragon in the wild

A few of the libraries that use snapdragon:

  • braces: Bash-like brace expansion, implemented in JavaScript. Safer than other brace expansion libs, with complete support… more | homepage
  • breakdance: Breakdance is a node.js library for converting HTML to markdown. Highly pluggable, flexible and easy… more | homepage
  • expand-brackets: Expand POSIX bracket expressions (character classes) in glob patterns. | homepage
  • extglob: Extended glob support for JavaScript. Adds (almost) the expressive power of regular expressions to glob… more | homepage
  • micromatch: Glob matching for javascript/node.js. A drop-in replacement and faster alternative to minimatch and multimatch. | homepage
  • nanomatch: Fast, minimal glob matcher for node.js. Similar to micromatch, minimatch and multimatch, but complete Bash… more | homepage

History

v0.9.0

Breaking changes!

In an attempt to make snapdragon lighter, more versatile, and more pluggable, some major changes were made in this release.

v0.5.0

Breaking changes!

Substantial breaking changes were made in v0.5.0! Most of these changes are part of a larger refactor that will be finished in 0.6.0, including the introduction of a Lexer class.

  • Renderer was renamed to Compiler
  • the .render method was renamed to .compile

About

Contributing

Pull requests and stars are always welcome. For bugs and feature requests, please create an issue.

Running Tests

Running and reviewing unit tests is a great way to get familiarized with a library and its API. You can install dependencies and run tests with the following command:

$ npm install && npm test
Building docs

(This project's readme.md is generated by verb, please don't edit the readme directly. Any changes to the readme must be made in the .verb.md readme template.)

To generate the readme, run the following command:

$ npm install -g verbose/verb#dev verb-generate-readme && verb

Related projects

A few of the libraries that use snapdragon:

Contributors

Commits Contributor
156 jonschlinkert
3 doowb
2 danez
1 EdwardBetts

Author

Jon Schlinkert

License

Copyright © 2018, Jon Schlinkert. Released under the MIT License.


This file was generated by verb-generate-readme, v0.6.0, on March 20, 2018.

snapdragon's People

Contributors

danez avatar doowb avatar edwardbetts avatar jonschlinkert avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

snapdragon's Issues

Please upgrade source-map to 0.7.2 to get speed improvements

The latest source-map which is v0.7.2 got support for WebAssembly and it is 10 times faster than the old one. More info here:

https://github.com/mozilla/source-map/blob/master/CHANGELOG.md#072
http://fitzgeraldnick.com/2018/02/26/speed-without-wizardry.html
https://hacks.mozilla.org/2018/01/oxidizing-source-maps-with-rust-and-webassembly/
firefox-devtools/debugger#5598

Please update so you would get 10 times speed improvements for your library in Firefox, 9 times in Safari and 5 times in Chrome.

Monorepo

It is nice that snapdragon as well as micromatch are nicely separated into small pieces, but I really think that having all of them in separate repositories make maintaining them a big pain, especially when making a breaking change in one of them that other rely one and then trying to update the whole ecosystem.

I'm also maintaining some projects that use monorepos with yarn workspaces and lerna and it gives you the ability to have small packages, but makes bigger changes super easy as they are all in one repository and one PR. So in my opinion setting up a monorepo would be a nice addition for snapdragon and micromatch.

So I wonder what your opinions are on this? And also for the micromatch org.

I don't want to come into a new project and change everything :) but I think it could make things a lot easier for everyone.

Security Vulnerability pick up by Fortify Scan (snapdragon)

Good Afternoon snapdragon team,

We were performing a scan on the snapdragon Javascript library and the Fortify application has picked up the following items that are discovered as vulnerabilities.
(Reason being our project has imported this library as a dependency and part of our deliverable would requires the scanning of the full source code and identify/remove the vulnerable.)

(Low) Password Management: Password in Comment
No of items picked by the Fortify Scan: Less than 10
Kindly refer to the following image, scanImage1.png
scanImage1

Do give us a heads up on when the team is planning to fix these vulnerabilities and/or the possible workarounds to prevent the same items to be picked up by the Fortify Scans again.

TypeError: expected an object, function, or array

I got this error using latest version of snapdragon

/home/remi/app/node_modules/snapdragon/node_modules/define-property/index.js:18
    throw new TypeError('expected an object, function, or array');
    ^

TypeError: expected an object, function, or array
    at defineProperty (/home/remi/app/node_modules/snapdragon/node_modules/define-property/index.js:18:11)
    at Parser.eos (/home/remi/app/node_modules/snapdragon/lib/parser.js:434:9)
    at Parser.parse (/home/remi/app/node_modules/snapdragon/lib/parser.js:536:20)
    at Object.<anonymous> (/home/remi/app/src/lib/index.js:15:20)
    at Module._compile (module.js:652:30)
    at loader (/home/remi/app/node_modules/babel-register/lib/node.js:144:5)
    at Object.require.extensions.(anonymous function) [as .js] (/home/remi/app/node_modules/babel-register/lib/node.js:154:7)
    at Module.load (module.js:565:32)
    at tryModuleLoad (module.js:505:12)
    at Function.Module._load (module.js:497:3)

using this parser:

function parser () {
  const pos = this.position()
  const match = this.match(/^\s*(if)\s*\(\s*(.*?)\s*\)\s*(?=then)/)
  const prev = this.prev()

  if (!match) return

  const ifNode = pos(this.node({
    type: 'ConditionnalState.if',
    val: match[1],
    condition: match[2]
  }))

  const conditionnalStateNode = pos(this.node({
    type: 'ConditionnalState',
    nodes: [ifNode]
  }))

  this.push('ConditionnalState', conditionnalStateNode)
  prev.nodes.push(conditionnalStateNode)
}

but if I change it for the following

function parser () {
  const pos = this.position()
  const match = this.match(/^\s*(if)\s*\(\s*(.*?)\s*\)\s*(?=then)/)
    // const prev = this.prev()

    if (!match) return

    const ifNode = pos(this.node({
      type: IfConditionnalStateParser.type,
      val: match[1],
      condition: match[2]
    }))

    // const conditionnalStateNode = pos(this.node({
    //   type: 'ConditionnalState',
    //   nodes: [ifNode]
    // }))

    // this.push('ConditionnalState', conditionnalStateNode)
    // prev.nodes.push(conditionnalStateNode)

    return ifNode
}

Here, there is no error.

I can't understand why that error.

It is the bundled eos() parser that throw TypeError because prev.parent is undefined.

define(prev.parent, 'escaped', true);

But I don't know why/when node should/must have a parent property.

I see that the condition for that line is hasOpenAndClose(prev) but I dont know hat to do with that.

I'm trying to create an AST to parse plantuml files (for activity diagrams only for now).

Here is the actual tree I get (when I use the second version of the mentionned parser, the first make eos() throw

Node {
  type: 'root',
  errors: [],
  nodes:
   [ Node { type: 'bos', val: '' },
     Node {
       type: 'Uml',
       title: 'Activité Eolienne',
       nodes:
        [ Node {
            type: 'Activity',
            nodes:
             [ Node { type: 'StaticState', val: 'Je veux installer une éolienne' },
               Node {
                 type: 'ConditionnalState.if',
                 val: 'if',
                 condition: 'Hauteur >= 12m (mât + nacelle)' },
               Node { type: 'ConditionnalState.then', val: 'then' },
               Node { type: 'Space', val: ' ' },
               Node { type: 'ConditionnalState.condition.open', val: '(' },
               Node { type: 'All', val: 'OUI)' },
               Node { type: 'NewLine', val: '\n' },
               Node { type: 'StaticState', val: 'CERFA = PC' },
               Node { type: 'Space', val: '  ' },
               Node { type: 'Activity.stop', val: 'stop' },
               Node { type: 'NewLine', val: '\n' },
               Node { type: 'ConditionnalState.else', val: 'else' },
               Node { type: 'Space', val: ' ' },
               Node { type: 'ConditionnalState.condition.open', val: '(' },
               Node { type: 'All', val: 'NON)' },
               Node { type: 'NewLine', val: '\n' },
               Node {
                 type: 'ConditionnalState.if',
                 val: 'if',
                 condition: 'Secteur protégé' },
               Node { type: 'ConditionnalState.then', val: 'then' },
               Node { type: 'Space', val: ' ' },
               Node { type: 'ConditionnalState.condition.open', val: '(' },
               Node { type: 'All', val: 'OUI)' },
               Node { type: 'NewLine', val: '\n' },
               Node { type: 'StaticState', val: 'CERFA = DP' },
               Node { type: 'Space', val: '    ' },
               Node { type: 'Activity.stop', val: 'stop' },
               Node { type: 'NewLine', val: '\n' },
               Node { type: 'Space', val: '  ' },
               Node { type: 'ConditionnalState.else', val: 'else' },
               Node { type: 'Space', val: ' ' },
               Node { type: 'ConditionnalState.condition.open', val: '(' },
               Node { type: 'All', val: 'NON)' },
               Node { type: 'NewLine', val: '\n' },
               Node { type: 'StaticState', val: 'Aucune autorisation nécessaire' },
               Node { type: 'StaticState', val: 'Retour au début de l\'assistant' },
               Node { type: 'Space', val: '    ' },
               Node { type: 'DetachPuml', val: 'detach' },
               Node { type: 'NewLine', val: '\n' },
               Node { type: 'All', val: 'endif' },
               Node { type: 'NewLine', val: '\n' } ],
            val: '\\undefined' },
          Node { type: 'All', val: '' } ] },
     Node { type: 'eos', val: '' } ] }

Prev / next, or offset

app
  .use(function any () {
    var pos = this.position();
    var m = this.match(/^./);
    if (!m) return;
    return pos({
      type: 'any',
      val: m[0]
    });
  })

Idea how from one renderer to get prev and next char? I think offset in position would work for me.

edit: And btw, should mention in readme, that you should be careful with parser plugins order (will show example in other issue).

upgrade source-map-resolve to 0.6.0

source-map-resolve 0.50 uses two deprecated libs urix and resolve-url. so nstalling snapdragon via npm complains about the deprecated warning.

[readme] - explain more for new-comers

Current iteration of Readme doesn't tell what exactly does snapdragon do.

It is stated it is "a factory", but there's no link to explanation what "a factory" is.

I wrote a few libraries that work with parsed HTML AST's and I'm somewhat familiar with the subject, yet I find the Readme file too steep and unfriendly.

Here are the simple questions that are still unanswered by the current Readme:

  • What does this library achieve by compiling and parsing?
  • Does snapdragon work on HTML (parsed or raw strings)? Or external CSS file's (parsed or raw string)?
  • How does snapdragon compare to, let's say a combination of PostHTML-parser and PostHTML-render? Can all be used together?

If you would lower the barrier for new-comers you would attract more contributors and win promoters for this library.

Dedupe dependencies.

Currently, there are multiple versions of several dependencies in the dependency tree. This issue is meant to organize the deduplication of these.

_

_

Please tag release release 0.11.0

According to github, the last release was 0.8.0... but there are commits saying otherwise : perhaps you forgot to tag them?

Thanks!

Browser support?

I'm trying to work with this for building a parser for use in the browser, and it's failing on the source-maps inclusion, specifically webpack is dying because it's calling in fs and path modules.

Is there a configuration that will work for in the browser? Or is that something you're looking at adding in the future? Or maybe is that something I could help with adding?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.