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