3 var use = require('use');
4 var util = require('util');
5 var Cache = require('map-cache');
6 var define = require('define-property');
7 var debug = require('debug')('snapdragon:parser');
8 var Position = require('./position');
9 var utils = require('./utils');
12 * Create a new `Parser` with the given `input` and `options`.
13 * @param {String} `input`
14 * @param {Object} `options`
18 function Parser(options) {
19 debug('initializing', __filename);
20 this.options = utils.extend({source: 'string'}, options);
21 this.init(this.options);
32 init: function(options) {
40 this.regex = new Cache();
41 this.errors = this.errors || [];
42 this.parsers = this.parsers || {};
43 this.types = this.types || [];
44 this.sets = this.sets || {};
45 this.fns = this.fns || [];
46 this.currentType = 'root';
48 var pos = this.position();
49 this.bos = pos({type: 'bos', val: ''});
57 define(this.bos, 'parent', this.ast);
58 this.nodes = [this.ast];
66 * Throw a formatted error with the cursor column and `msg`.
67 * @param {String} `msg` Message to use in the Error.
70 error: function(msg, node) {
71 var pos = node.position || {start: {column: 0, line: 0}};
72 var line = pos.start.line;
73 var column = pos.start.column;
74 var source = this.options.source;
76 var message = source + ' <line:' + line + ' column:' + column + '>: ' + msg;
77 var err = new Error(message);
82 if (this.options.silent) {
83 this.errors.push(err);
90 * Define a non-enumberable property on the `Parser` instance.
93 * parser.define('foo', 'bar');
96 * @param {String} `key` propery name
97 * @param {any} `val` property value
98 * @return {Object} Returns the Parser instance for chaining.
102 define: function(key, val) {
103 define(this, key, val);
108 * Mark position and patch `node.position`.
111 position: function() {
112 var start = { line: this.line, column: this.column };
115 return function(node) {
116 define(node, 'position', new Position(start, self));
122 * Set parser `name` with the given `fn`
123 * @param {String} `name`
124 * @param {Function} `fn`
128 set: function(type, fn) {
129 if (this.types.indexOf(type) === -1) {
130 this.types.push(type);
132 this.parsers[type] = fn.bind(this);
138 * @param {String} `name`
142 get: function(name) {
143 return this.parsers[name];
147 * Push a `token` onto the `type` stack.
149 * @param {String} `type`
150 * @return {Object} `token`
154 push: function(type, token) {
155 this.sets[type] = this.sets[type] || [];
157 this.stack.push(token);
158 return this.sets[type].push(token);
162 * Pop a token off of the `type` stack
163 * @param {String} `type`
164 * @returns {Object} Returns a token
168 pop: function(type) {
169 this.sets[type] = this.sets[type] || [];
172 return this.sets[type].pop();
176 * Return true if inside a `stack` node. Types are `braces`, `parens` or `brackets`.
178 * @param {String} `type`
183 isInside: function(type) {
184 this.sets[type] = this.sets[type] || [];
185 return this.sets[type].length > 0;
189 * Return true if `node` is the given `type`.
192 * parser.isType(node, 'brace');
194 * @param {Object} `node`
195 * @param {String} `type`
200 isType: function(node, type) {
201 return node && node.type === type;
205 * Get the previous AST node
210 return this.stack.length > 0
211 ? utils.last(this.stack, n)
212 : utils.last(this.nodes, n);
216 * Update line and column based on `str`.
219 consume: function(len) {
220 this.input = this.input.substr(len);
224 * Update column based on `str`.
227 updatePosition: function(str, len) {
228 var lines = str.match(/\n/g);
229 if (lines) this.line += lines.length;
230 var i = str.lastIndexOf('\n');
231 this.column = ~i ? len - i : this.column + len;
237 * Match `regex`, return captures, and update the cursor position by `match[0]` length.
238 * @param {RegExp} `regex`
242 match: function(regex) {
243 var m = regex.exec(this.input);
245 this.updatePosition(m[0], m[0].length);
251 * Capture `type` with the given regex.
252 * @param {String} `type`
253 * @param {RegExp} `regex`
257 capture: function(type, regex) {
258 if (typeof regex === 'function') {
259 return this.set.apply(this, arguments);
262 this.regex.set(type, regex);
263 this.set(type, function() {
264 var parsed = this.parsed;
265 var pos = this.position();
266 var m = this.match(regex);
267 if (!m || !m[0]) return;
269 var prev = this.prev();
281 define(node, 'inside', this.stack.length > 0);
282 define(node, 'parent', prev);
283 prev.nodes.push(node);
289 * Create a parser with open and close for parens,
293 capturePair: function(type, openRegex, closeRegex, fn) {
294 this.sets[type] = this.sets[type] || [];
300 this.set(type + '.open', function() {
301 var parsed = this.parsed;
302 var pos = this.position();
303 var m = this.match(openRegex);
304 if (!m || !m[0]) return;
308 this.specialChars = true;
310 type: type + '.open',
315 if (typeof m[1] !== 'undefined') {
319 var prev = this.prev();
325 define(node, 'rest', this.input);
326 define(node, 'parsed', parsed);
327 define(node, 'prefix', m[1]);
328 define(node, 'parent', prev);
329 define(open, 'parent', node);
331 if (typeof fn === 'function') {
332 fn.call(this, open, node);
335 this.push(type, node);
336 prev.nodes.push(node);
343 this.set(type + '.close', function() {
344 var pos = this.position();
345 var m = this.match(closeRegex);
346 if (!m || !m[0]) return;
348 var parent = this.pop(type);
350 type: type + '.close',
356 if (!this.isType(parent, type)) {
357 if (this.options.strict) {
358 throw new Error('missing opening "' + type + '"');
366 if (node.suffix === '\\') {
367 parent.escaped = true;
371 parent.nodes.push(node);
372 define(node, 'parent', parent);
379 * Capture end-of-string
383 var pos = this.position();
384 if (this.input) return;
385 var prev = this.prev();
387 while (prev.type !== 'root' && !prev.visited) {
388 if (this.options.strict === true) {
389 throw new SyntaxError('invalid syntax:' + util.inspect(prev, null, 2));
392 if (!hasDelims(prev)) {
393 prev.parent.escaped = true;
397 visit(prev, function(node) {
398 if (!hasDelims(node.parent)) {
399 node.parent.escaped = true;
409 val: this.append || ''
412 define(tok, 'parent', this.ast);
417 * Run parsers to advance the cursor position
421 var parsed = this.parsed;
422 var len = this.types.length;
426 while (++idx < len) {
427 if ((tok = this.parsers[this.types[idx]].call(this))) {
428 define(tok, 'rest', this.input);
429 define(tok, 'parsed', parsed);
437 * Parse the given string.
441 parse: function(input) {
442 if (typeof input !== 'string') {
443 throw new TypeError('expected a string');
446 this.init(this.options);
452 // check input before calling `.next()`
455 // get the next AST ndoe
456 var node = self.next();
458 var prev = self.prev();
460 define(node, 'parent', prev);
462 prev.nodes.push(node);
466 if (self.sets.hasOwnProperty(prev.type)) {
467 self.currentType = prev.type;
471 // if we got here but input is not changed, throw an error
472 if (self.input && input === self.input) {
473 throw new Error('no parsers registered for: "' + self.input.slice(0, 5) + '"');
477 while (this.input) parse();
478 if (this.stack.length && this.options.strict) {
479 var node = this.stack.pop();
480 throw this.error('missing opening ' + node.type + ': "' + this.orig + '"');
483 var eos = this.eos();
484 var tok = this.prev();
485 if (tok.type !== 'eos') {
486 this.ast.nodes.push(eos);
494 * Visit `node` with the given `fn`
497 function visit(node, fn) {
499 define(node, 'visited', true);
500 return node.nodes ? mapVisit(node.nodes, fn) : fn(node);
506 * Map visit over array of `nodes`.
509 function mapVisit(nodes, fn) {
510 var len = nodes.length;
512 while (++idx < len) {
513 visit(nodes[idx], fn);
517 function hasOpen(node) {
518 return node.nodes && node.nodes[0].type === (node.type + '.open');
521 function hasClose(node) {
522 return node.nodes && utils.last(node.nodes).type === (node.type + '.close');
525 function hasDelims(node) {
526 return hasOpen(node) && hasClose(node);
533 module.exports = Parser;