'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;