snapdragon-parser
Version:
Easily parse a string to create an AST.
698 lines (601 loc) • 15.7 kB
JavaScript
'use strict';
const assert = require('assert');
const Emitter = require('events');
const Lexer = require('snapdragon-lexer');
const Node = require('snapdragon-node');
/**
* Create a new `Parser` with the given `input` and `options`.
*
* ```js
* const Parser = require('snapdragon-parser');
* const parser = new Parser();
* ```
* @name Parser
* @param {String} `input`
* @param {Object} `options`
* @api public
*/
class Parser extends Emitter {
constructor(input, options = {}) {
super();
if (typeof input !== 'string') {
options = input || {};
input = '';
}
this.options = Object.assign({ type: 'root' }, options);
this.sep = this.options.sep || '.';
this.node = this.node.bind(this);
this.types = new Set();
this.handlers = new Map();
if (options.handlers) {
for (const [type, handler] of options.handlers) {
this.handler(type, handler);
}
}
this.lexer = new Lexer(options);
this.init(input);
}
/**
* Initialize parser properties
*/
init(input) {
this.lexer.state = new Lexer.State(input, this.options);
this.bos = this.node({ type: 'bos', value: '' });
this.eos = this.node({ type: 'eos', value: '' });
this.ast = this.node({ type: this.options.type, nodes: [] });
this.stack = [this.ast];
}
/**
* Create a new [Node](#node) with the given `value` and `type`.
*
* ```js
* const node = parser.node({type: 'slash', value: '/'});
* // sugar for
* const Node = require('snapdragon-node');
* const node = Node({type: 'slash', value: '/'});
* ```
* @name .node
* @param {Object} `node` The object to use to create a node
* @return {Object} returns the created [Node](#node) instance.
* @api public
*/
node(node, value = '') {
assert(node, 'expected a string or object');
if (typeof node === 'string') {
return this.node({ type: node, value });
}
if (!this.isNode(node)) {
node = new Node(node);
}
if (Array.isArray(node.nodes) && node.nodes.length) {
node.nodes = node.nodes.map(this.node.bind(this));
}
this.emit('node', node);
return node;
}
/**
* Returns true if the given value is an instance of [snapdragon-node][].
*
* @name .isNode
* @param {Object} `node`
* @return {Boolean}
* @api public
*/
isNode(node) {
return Node.isNode(node);
}
/**
* Register handler of the given `type`.
*
* ```js
* parser.set('all', function(tok) {
* // do stuff to tok
* return tok;
* });
* ```
* @name .set
* @param {String} `type`
* @param {Function} `handler`
* @api public
*/
set(type, handler = tok => tok) {
assert.equal(typeof type, 'string', 'expected a string');
this.types.add(type);
// can't do fat arrow, we need to ensure that the
// handler context is always correct (in case
// handlers are called directly, or re-registered
// on a new instance, etc.)
let parser = this;
this.handlers.set(type, function(...args) {
let ctx = this || parser;
let node = handler.call(ctx, ...args);
if (isObject(node)) node = ctx.node(node);
if (!ctx.isNode(node)) return;
if (!node.type) node.type = type;
ctx.emit('handled', node);
ctx.emit(node.type, node);
return node;
});
return this;
}
/**
* Get a registered handler function.
*
* ```js
* handlers.set('star', function() {
* // do parser, lexer, or compiler stuff
* });
* const star = handlers.get('star');
* ```
* @name .get
* @param {String} `type`
* @param {Function} `fn` The handler function to register.
* @api public
*/
get(type) {
let handler = this.handlers.get(type) || this.handlers.get('default');
assert.equal(typeof handler, 'function', `expected handler "${type}" to be a function`);
return handler;
}
/**
* Returns true if the parser has a registered handler of the given `type`.
*
* ```js
* parser.set('star', function() {});
* console.log(parser.has('star')); // true
* ```
* @name .has
* @param {String} type
* @return {Boolean}
* @api public
*/
has(type) {
return this.handlers.has(type);
}
/**
* Get the previously/last lexed token.
*/
last() {
return this.lexer.lookbehind(1);
}
/**
* Get the last node from the stack
*/
prev() {
return this.stack[this.stack.length - 1];
}
/**
* Get the last child node of the current node on the stack
*/
lastChild() {
let last = this.prev();
while (last && last.nodes && last.nodes.length) {
last = last.nodes[last.nodes.length - 1];
}
return last;
}
/**
* Look ahead to the next lexer token.
*/
peek() {
return this.lexer.lookahead(1);
}
/**
* Capture a node of the given `type`.
*
* @name .capture
* @param {String} `type` (required)
* @param {RegExp} `regex` (optional)
* @param {Function} `lexerFn` (optional)
* @param {Function} `parserFn` (optional)
* @return {Object} Returns the Parser instance for chaining.
* @api public
*/
capture(type, regex, lexerFn, parserFn) {
if (!isRegex(regex)) {
return this.set(type, regex);
}
if (typeof parserFn === 'undefined') {
parserFn = lexerFn;
lexerFn = void 0;
}
this.lexer.capture(type, regex, lexerFn);
return this.set(type, parserFn);
}
/**
* Returns true if the parser is currently "inside" a node of the given `type`.
*
* @name .isInside
* @param {String} `type`
* @return {Boolean}
* @api public
*/
isInside(types) {
if (!types) return this.stack.length > 1;
types = [].concat(types);
for (let i = this.stack.length - 1; i > 0; i--) {
if (types.includes(this.stack[i].type)) {
return true;
}
}
return false;
}
/**
* Returns true if `node` is a "block" node. Block nodes have a
* `nodes` array for keeping child nodes.
*
* ```js
* parser.isBlock(new Node()); //=> false
* parser.isBlock(new Node({ nodes: [] })); //=> true
* ```
* @name .isBlock
* @param {Object} `node`
* @return {Boolean}
* @api public
*/
isBlock(node) {
return Array.isArray(node.nodes);
}
/**
* Returns true if `node` is a new "block" node, with either no child nodes,
* or only an open node.
*
* ```js
* parser.isBlock(new Node()); //=> false
* parser.isBlock(new Node({ nodes: [] })); //=> true
* ```
* @name .isBlock
* @param {Object} `node`
* @return {Boolean}
* @api public
*/
isNewBlock(node) {
return this.isBlock(node) && (!node.nodes[0] || this.isOpen(node.nodes[0], node));
}
/**
* Returns true if the given `node` is an "open" node.
*
* @name .isOpen
* @param {Object} `node`
* @return {Object} `parentNode`
* @api public
*/
isOpen(node, parent = this.prev()) {
if (typeof this.options.isOpen === 'function') {
return this.options.isOpen.call(this, node, parent);
}
if (typeof parent.isOpen === 'function') {
return parent.isOpen(node);
}
let segs = node.type.split(this.sep);
if (segs[0] === parent.type) {
return segs[1] === 'open';
}
return false;
}
/**
* Returns true if the given `node` is a "close" node.
*
* @name .isClose
* @param {Object} `node`
* @return {Object} `parentNode`
* @api public
*/
isClose(node, parent = this.prev()) {
if (typeof this.options.isClose === 'function') {
return this.options.isClose.call(this, node, parent);
}
if (typeof parent.isClose === 'function') {
return parent.isClose(node);
}
let segs = node.type.split(this.sep);
if (segs[0] === parent.type) {
return segs[1] === 'close';
}
return false;
}
/**
* Push a child node onto the `node.nodes` array of the current node
* on the `stack`.
*
* ```js
* parser.set('default', function(tok) {
* return this.push(this.node(tok));
* });
* ```
* @name .push
* @emits push
* @param {Object} `node` (required)
* @return {Object} `node`
* @api public
*/
push(node) {
if (!node) return;
if (isObject(node) && !this.isNode(node)) {
node = this.node(node);
}
this.emit('push', node);
// get previous node added to the stack
let parent = this.prev();
// allow users to do this with custom code in handlers
if (node.handleBlock !== false) {
if (this.isNewBlock(node)) {
// if this is a new block node, push it onto the stack
this.stack.push(node);
} else if (this.isBlock(parent) && this.isClose(node, parent)) {
// otherwise if this is a closing node and the parent node is
// a block node, call .pop to pop the parent node from the stack
this.pop(node);
}
}
if (!parent.nodes.includes(node)) {
parent.push(node);
}
return node;
}
/**
* Pop the last node from the `stack`. If a
*
* ```js
* parser.set('default', function(tok) {
* return this.push(this.node(tok));
* });
* ```
* @name .pop
* @emits pop
* @param {Object} `node` (optional)
* @return {Object} `node`
* @api public
*/
pop(closingNode) {
let parent = this.stack.pop();
assert(Node.isNode(parent), 'expected node to be an instance of Node');
assert(Array.isArray(parent.nodes), 'expected node.nodes to be an array');
this.emit('pop', parent, closingNode);
// if a closing node is passed, throw if it's not valid
if (!this.isClose(closingNode, parent)) {
this.error(`expected "${parent.type}.close", received: "${closingNode.type}"`);
}
return parent;
}
/**
* Get the next token from the lexer, then calls the registered
* parser handler for `token.type` on the token.
*
* @name .next
* @return {Any} Returns whatever value the handler returns.
* @api public
*/
next() {
let token = this.lexer.next();
let handler;
if (!token) return;
this.lexer.push(token);
try {
handler = this.get(token.type);
return handler.call(this, token);
} catch (err) {
err.name = 'ParserError';
this.error(err);
}
}
/**
* Expect the given `type`, or throw an exception.
*
* @param {String} `type`
* @api public
*/
expect(type) {
let next = this.peek();
assert(next.type === type, `expected "${type}", but received "${next.type}"`);
return this.next();
}
/**
* Accept the given `type`.
*
* @param {String} type
* @api public
*/
accept(type) {
let node = this.peek();
if (node.type === type) {
return this.next();
}
}
/**
* Parses the given `input` string and returns an AST object.
*
* ```js
* const ast = parser.parse('foo/bar');
* ```
* @name .parse
* @param {String} `input`
* @return {Object} Returns an AST (abstract syntax tree).
* @api public
*/
parse(input, options) {
assert.equal(typeof input, 'string', 'expected input to be a string');
this.init(input);
let bos = this.handlers.get('bos');
let eos = this.handlers.get('eos');
this.push(bos ? bos.call(this, this.bos) : this.bos);
while (!this.lexer.eos()) this.push(this.next());
this.push(eos ? eos.call(this, this.eos) : this.eos);
this.emit('parsed', this.ast);
this.fail();
return this.ast;
}
/**
* Creates a new Parser instance with the given options, and copy
* the handlers from the current instance to the new instance.
*
* @param {Object} `options`
* @param {Object} `parent` Optionally pass a different parser instance to copy handlers from.
* @return {Object} Returns a new Parser instance
* @api public
*/
create(options, parent = this) {
let parser = new parent.constructor(options);
parser.lexer.handlers = parent.lexer.handlers;
parser.lexer.types = parent.lexer.types;
parser.handlers = parent.handlers;
parser.types = parent.types;
parser.input = '';
return parser;
}
/**
* Concat nodes from another AST to `node.nodes`.
*
* @param {Object} `node`
* @param {Object} `ast`
* @return {Object}
* @api public
*/
concat(node, ast) {
for (let child of ast.nodes) {
if (child.type !== 'bos' && child.type !== 'eos') {
node.push(child);
}
}
return node;
}
/**
* Returns true if listeners are registered for even `name`.
*
* @name .hasListeners
* @param {String} `name`
* @return {Boolean}
* @api public
*/
hasListeners(name) {
if (typeof super.hasListeners === 'function') {
return super.hasListeners(name);
}
return this.listenerCount(name) > 0;
}
/**
* Call a plugin function on the parser instance.
*
* ```js
* const myParser = new Parser();
* const plugin = parser => {
* // do stuff to parser instance
* };
* myParser.use(plugin);
* ```
* @param {Function} `fn`
* @return {Parser} Returns the Parser instance.
* @api public
*/
use(fn) {
fn.call(this, this);
return this;
}
/**
* Throw a formatted error message with details including the cursor position.
*
* ```js
* parser.set('foo', function(tok) {
* if (tok.value !== 'foo') {
* throw this.error('expected token.value to be "foo"', tok);
* }
* });
* ```
* @name .error
* @param {String} `msg` Message to use in the Error.
* @param {Object} `node`
* @return {Undefined}
* @api public
*/
error(err) {
if (typeof err === 'string') {
err = new Error(err);
}
if (this.hasListeners('error')) {
this.emit('error', err);
} else {
throw err;
}
}
/**
* Fail when a closing delimiter is missing.
*/
fail() {
let node = this.stack.pop();
if (this.options.ignoreSyntaxErrors !== true && node.type !== 'root') {
this.error(new SyntaxError(`unclosed: "${node.type}"`));
}
}
get tokens() {
return this.lexer.state.tokens;
}
set current(value) {
this.lexer.state.current = value;
}
get current() {
return this.lexer.state.current;
}
set string(value) {
this.lexer.state.string = value;
}
get string() {
return this.lexer.state.string;
}
set input(value) {
this.lexer.state.input = value;
}
get input() {
return this.lexer.state.input;
}
/**
* Get the part of the input string has has already been parsed.
* @return {String}
* @api public
*/
get parsed() {
return this.lexer.state.consumed;
}
get consumed() {
return this.parsed;
}
static get Node() {
return Node;
}
/**
* Returns true if the given value is an instance of snapdragon `Parser`.
*
* ```js
* const Parser = require('parser');
* const parser = new Parser();
* console.log(Parser.isParser(parser)); //=> true
* console.log(Parser.isParser({})); //=> false
* ```
* @param {Object} `parser`
* @returns {Boolean}
* @api public
*/
static isParser(parser) {
return isObject(parser) && parser instanceof Parser;
}
}
/**
* Returns true if value is an object
* @param {Any} value
* @return {Boolean}
*/
function isObject(val) {
return val && typeof val === 'object' && !Array.isArray(val);
}
/**
* Returns true if value is a RegExp
* @param {Any} value
* @return {Boolean}
*/
function isRegex(val) {
return val instanceof RegExp;
}
/**
* Expose `Parser`
* @type {Function}
*/
module.exports = Parser;