123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533 |
- 'use strict';
- var use = require('use');
- var util = require('util');
- var Cache = require('map-cache');
- var define = require('define-property');
- var debug = require('debug')('snapdragon:parser');
- var Position = require('./position');
- var utils = require('./utils');
- /**
- * Create a new `Parser` with the given `input` and `options`.
- * @param {String} `input`
- * @param {Object} `options`
- * @api public
- */
- function Parser(options) {
- debug('initializing', __filename);
- this.options = utils.extend({source: 'string'}, options);
- this.init(this.options);
- use(this);
- }
- /**
- * Prototype methods
- */
- Parser.prototype = {
- constructor: Parser,
- init: function(options) {
- this.orig = '';
- this.input = '';
- this.parsed = '';
- this.column = 1;
- this.line = 1;
- this.regex = new Cache();
- this.errors = this.errors || [];
- this.parsers = this.parsers || {};
- this.types = this.types || [];
- this.sets = this.sets || {};
- this.fns = this.fns || [];
- this.currentType = 'root';
- var pos = this.position();
- this.bos = pos({type: 'bos', val: ''});
- this.ast = {
- type: 'root',
- errors: this.errors,
- nodes: [this.bos]
- };
- define(this.bos, 'parent', this.ast);
- this.nodes = [this.ast];
- this.count = 0;
- this.setCount = 0;
- this.stack = [];
- },
- /**
- * Throw a formatted error with the cursor column and `msg`.
- * @param {String} `msg` Message to use in the Error.
- */
- error: function(msg, node) {
- var pos = node.position || {start: {column: 0, line: 0}};
- var line = pos.start.line;
- var column = pos.start.column;
- var source = this.options.source;
- var message = source + ' <line:' + line + ' column:' + column + '>: ' + msg;
- var err = new Error(message);
- err.source = source;
- err.reason = msg;
- err.pos = pos;
- if (this.options.silent) {
- this.errors.push(err);
- } else {
- throw err;
- }
- },
- /**
- * Define a non-enumberable property on the `Parser` instance.
- *
- * ```js
- * parser.define('foo', 'bar');
- * ```
- * @name .define
- * @param {String} `key` propery name
- * @param {any} `val` property value
- * @return {Object} Returns the Parser instance for chaining.
- * @api public
- */
- define: function(key, val) {
- define(this, key, val);
- return this;
- },
- /**
- * Mark position and patch `node.position`.
- */
- position: function() {
- var start = { line: this.line, column: this.column };
- var self = this;
- return function(node) {
- define(node, 'position', new Position(start, self));
- return node;
- };
- },
- /**
- * Set parser `name` with the given `fn`
- * @param {String} `name`
- * @param {Function} `fn`
- * @api public
- */
- set: function(type, fn) {
- if (this.types.indexOf(type) === -1) {
- this.types.push(type);
- }
- this.parsers[type] = fn.bind(this);
- return this;
- },
- /**
- * Get parser `name`
- * @param {String} `name`
- * @api public
- */
- get: function(name) {
- return this.parsers[name];
- },
- /**
- * Push a `token` onto the `type` stack.
- *
- * @param {String} `type`
- * @return {Object} `token`
- * @api public
- */
- push: function(type, token) {
- this.sets[type] = this.sets[type] || [];
- this.count++;
- this.stack.push(token);
- return this.sets[type].push(token);
- },
- /**
- * Pop a token off of the `type` stack
- * @param {String} `type`
- * @returns {Object} Returns a token
- * @api public
- */
- pop: function(type) {
- this.sets[type] = this.sets[type] || [];
- this.count--;
- this.stack.pop();
- return this.sets[type].pop();
- },
- /**
- * Return true if inside a `stack` node. Types are `braces`, `parens` or `brackets`.
- *
- * @param {String} `type`
- * @return {Boolean}
- * @api public
- */
- isInside: function(type) {
- this.sets[type] = this.sets[type] || [];
- return this.sets[type].length > 0;
- },
- /**
- * Return true if `node` is the given `type`.
- *
- * ```js
- * parser.isType(node, 'brace');
- * ```
- * @param {Object} `node`
- * @param {String} `type`
- * @return {Boolean}
- * @api public
- */
- isType: function(node, type) {
- return node && node.type === type;
- },
- /**
- * Get the previous AST node
- * @return {Object}
- */
- prev: function(n) {
- return this.stack.length > 0
- ? utils.last(this.stack, n)
- : utils.last(this.nodes, n);
- },
- /**
- * Update line and column based on `str`.
- */
- consume: function(len) {
- this.input = this.input.substr(len);
- },
- /**
- * Update column based on `str`.
- */
- updatePosition: function(str, len) {
- var lines = str.match(/\n/g);
- if (lines) this.line += lines.length;
- var i = str.lastIndexOf('\n');
- this.column = ~i ? len - i : this.column + len;
- this.parsed += str;
- this.consume(len);
- },
- /**
- * Match `regex`, return captures, and update the cursor position by `match[0]` length.
- * @param {RegExp} `regex`
- * @return {Object}
- */
- match: function(regex) {
- var m = regex.exec(this.input);
- if (m) {
- this.updatePosition(m[0], m[0].length);
- return m;
- }
- },
- /**
- * Capture `type` with the given regex.
- * @param {String} `type`
- * @param {RegExp} `regex`
- * @return {Function}
- */
- capture: function(type, regex) {
- if (typeof regex === 'function') {
- return this.set.apply(this, arguments);
- }
- this.regex.set(type, regex);
- this.set(type, function() {
- var parsed = this.parsed;
- var pos = this.position();
- var m = this.match(regex);
- if (!m || !m[0]) return;
- var prev = this.prev();
- var node = pos({
- type: type,
- val: m[0],
- parsed: parsed,
- rest: this.input
- });
- if (m[1]) {
- node.inner = m[1];
- }
- define(node, 'inside', this.stack.length > 0);
- define(node, 'parent', prev);
- prev.nodes.push(node);
- }.bind(this));
- return this;
- },
- /**
- * Create a parser with open and close for parens,
- * brackets or braces
- */
- capturePair: function(type, openRegex, closeRegex, fn) {
- this.sets[type] = this.sets[type] || [];
- /**
- * Open
- */
- this.set(type + '.open', function() {
- var parsed = this.parsed;
- var pos = this.position();
- var m = this.match(openRegex);
- if (!m || !m[0]) return;
- var val = m[0];
- this.setCount++;
- this.specialChars = true;
- var open = pos({
- type: type + '.open',
- val: val,
- rest: this.input
- });
- if (typeof m[1] !== 'undefined') {
- open.inner = m[1];
- }
- var prev = this.prev();
- var node = pos({
- type: type,
- nodes: [open]
- });
- define(node, 'rest', this.input);
- define(node, 'parsed', parsed);
- define(node, 'prefix', m[1]);
- define(node, 'parent', prev);
- define(open, 'parent', node);
- if (typeof fn === 'function') {
- fn.call(this, open, node);
- }
- this.push(type, node);
- prev.nodes.push(node);
- });
- /**
- * Close
- */
- this.set(type + '.close', function() {
- var pos = this.position();
- var m = this.match(closeRegex);
- if (!m || !m[0]) return;
- var parent = this.pop(type);
- var node = pos({
- type: type + '.close',
- rest: this.input,
- suffix: m[1],
- val: m[0]
- });
- if (!this.isType(parent, type)) {
- if (this.options.strict) {
- throw new Error('missing opening "' + type + '"');
- }
- this.setCount--;
- node.escaped = true;
- return node;
- }
- if (node.suffix === '\\') {
- parent.escaped = true;
- node.escaped = true;
- }
- parent.nodes.push(node);
- define(node, 'parent', parent);
- });
- return this;
- },
- /**
- * Capture end-of-string
- */
- eos: function() {
- var pos = this.position();
- if (this.input) return;
- var prev = this.prev();
- while (prev.type !== 'root' && !prev.visited) {
- if (this.options.strict === true) {
- throw new SyntaxError('invalid syntax:' + util.inspect(prev, null, 2));
- }
- if (!hasDelims(prev)) {
- prev.parent.escaped = true;
- prev.escaped = true;
- }
- visit(prev, function(node) {
- if (!hasDelims(node.parent)) {
- node.parent.escaped = true;
- node.escaped = true;
- }
- });
- prev = prev.parent;
- }
- var tok = pos({
- type: 'eos',
- val: this.append || ''
- });
- define(tok, 'parent', this.ast);
- return tok;
- },
- /**
- * Run parsers to advance the cursor position
- */
- next: function() {
- var parsed = this.parsed;
- var len = this.types.length;
- var idx = -1;
- var tok;
- while (++idx < len) {
- if ((tok = this.parsers[this.types[idx]].call(this))) {
- define(tok, 'rest', this.input);
- define(tok, 'parsed', parsed);
- this.last = tok;
- return tok;
- }
- }
- },
- /**
- * Parse the given string.
- * @return {Array}
- */
- parse: function(input) {
- if (typeof input !== 'string') {
- throw new TypeError('expected a string');
- }
- this.init(this.options);
- this.orig = input;
- this.input = input;
- var self = this;
- function parse() {
- // check input before calling `.next()`
- input = self.input;
- // get the next AST ndoe
- var node = self.next();
- if (node) {
- var prev = self.prev();
- if (prev) {
- define(node, 'parent', prev);
- if (prev.nodes) {
- prev.nodes.push(node);
- }
- }
- if (self.sets.hasOwnProperty(prev.type)) {
- self.currentType = prev.type;
- }
- }
- // if we got here but input is not changed, throw an error
- if (self.input && input === self.input) {
- throw new Error('no parsers registered for: "' + self.input.slice(0, 5) + '"');
- }
- }
- while (this.input) parse();
- if (this.stack.length && this.options.strict) {
- var node = this.stack.pop();
- throw this.error('missing opening ' + node.type + ': "' + this.orig + '"');
- }
- var eos = this.eos();
- var tok = this.prev();
- if (tok.type !== 'eos') {
- this.ast.nodes.push(eos);
- }
- return this.ast;
- }
- };
- /**
- * Visit `node` with the given `fn`
- */
- function visit(node, fn) {
- if (!node.visited) {
- define(node, 'visited', true);
- return node.nodes ? mapVisit(node.nodes, fn) : fn(node);
- }
- return node;
- }
- /**
- * Map visit over array of `nodes`.
- */
- function mapVisit(nodes, fn) {
- var len = nodes.length;
- var idx = -1;
- while (++idx < len) {
- visit(nodes[idx], fn);
- }
- }
- function hasOpen(node) {
- return node.nodes && node.nodes[0].type === (node.type + '.open');
- }
- function hasClose(node) {
- return node.nodes && utils.last(node.nodes).type === (node.type + '.close');
- }
- function hasDelims(node) {
- return hasOpen(node) && hasClose(node);
- }
- /**
- * Expose `Parser`
- */
- module.exports = Parser;
|