parsers.js
  1  'use strict';
  2  
  3  var Node = require('snapdragon-node');
  4  var utils = require('./utils');
  5  
  6  /**
  7   * Braces parsers
  8   */
  9  
 10  module.exports = function(braces, options) {
 11    braces.parser
 12      .set('bos', function() {
 13        if (!this.parsed) {
 14          this.ast = this.nodes[0] = new Node(this.ast);
 15        }
 16      })
 17  
 18      /**
 19       * Character parsers
 20       */
 21  
 22      .set('escape', function() {
 23        var pos = this.position();
 24        var m = this.match(/^(?:\\(.)|\$\{)/);
 25        if (!m) return;
 26  
 27        var prev = this.prev();
 28        var last = utils.last(prev.nodes);
 29  
 30        var node = pos(new Node({
 31          type: 'text',
 32          multiplier: 1,
 33          val: m[0]
 34        }));
 35  
 36        if (node.val === '\\\\') {
 37          return node;
 38        }
 39  
 40        if (node.val === '${') {
 41          var str = this.input;
 42          var idx = -1;
 43          var ch;
 44  
 45          while ((ch = str[++idx])) {
 46            this.consume(1);
 47            node.val += ch;
 48            if (ch === '\\') {
 49              node.val += str[++idx];
 50              continue;
 51            }
 52            if (ch === '}') {
 53              break;
 54            }
 55          }
 56        }
 57  
 58        if (this.options.unescape !== false) {
 59          node.val = node.val.replace(/\\([{}])/g, '$1');
 60        }
 61  
 62        if (last.val === '"' && this.input.charAt(0) === '"') {
 63          last.val = node.val;
 64          this.consume(1);
 65          return;
 66        }
 67  
 68        return concatNodes.call(this, pos, node, prev, options);
 69      })
 70  
 71      /**
 72       * Brackets: "[...]" (basic, this is overridden by
 73       * other parsers in more advanced implementations)
 74       */
 75  
 76      .set('bracket', function() {
 77        var isInside = this.isInside('brace');
 78        var pos = this.position();
 79        var m = this.match(/^(?:\[([!^]?)([^\]]{2,}|\]-)(\]|[^*+?]+)|\[)/);
 80        if (!m) return;
 81  
 82        var prev = this.prev();
 83        var val = m[0];
 84        var negated = m[1] ? '^' : '';
 85        var inner = m[2] || '';
 86        var close = m[3] || '';
 87  
 88        if (isInside && prev.type === 'brace') {
 89          prev.text = prev.text || '';
 90          prev.text += val;
 91        }
 92  
 93        var esc = this.input.slice(0, 2);
 94        if (inner === '' && esc === '\\]') {
 95          inner += esc;
 96          this.consume(2);
 97  
 98          var str = this.input;
 99          var idx = -1;
100          var ch;
101  
102          while ((ch = str[++idx])) {
103            this.consume(1);
104            if (ch === ']') {
105              close = ch;
106              break;
107            }
108            inner += ch;
109          }
110        }
111  
112        return pos(new Node({
113          type: 'bracket',
114          val: val,
115          escaped: close !== ']',
116          negated: negated,
117          inner: inner,
118          close: close
119        }));
120      })
121  
122      /**
123       * Empty braces (we capture these early to
124       * speed up processing in the compiler)
125       */
126  
127      .set('multiplier', function() {
128        var isInside = this.isInside('brace');
129        var pos = this.position();
130        var m = this.match(/^\{((?:,|\{,+\})+)\}/);
131        if (!m) return;
132  
133        this.multiplier = true;
134        var prev = this.prev();
135        var val = m[0];
136  
137        if (isInside && prev.type === 'brace') {
138          prev.text = prev.text || '';
139          prev.text += val;
140        }
141  
142        var node = pos(new Node({
143          type: 'text',
144          multiplier: 1,
145          match: m,
146          val: val
147        }));
148  
149        return concatNodes.call(this, pos, node, prev, options);
150      })
151  
152      /**
153       * Open
154       */
155  
156      .set('brace.open', function() {
157        var pos = this.position();
158        var m = this.match(/^\{(?!(?:[^\\}]?|,+)\})/);
159        if (!m) return;
160  
161        var prev = this.prev();
162        var last = utils.last(prev.nodes);
163  
164        // if the last parsed character was an extglob character
165        // we need to _not optimize_ the brace pattern because
166        // it might be mistaken for an extglob by a downstream parser
167        if (last && last.val && isExtglobChar(last.val.slice(-1))) {
168          last.optimize = false;
169        }
170  
171        var open = pos(new Node({
172          type: 'brace.open',
173          val: m[0]
174        }));
175  
176        var node = pos(new Node({
177          type: 'brace',
178          nodes: []
179        }));
180  
181        node.push(open);
182        prev.push(node);
183        this.push('brace', node);
184      })
185  
186      /**
187       * Close
188       */
189  
190      .set('brace.close', function() {
191        var pos = this.position();
192        var m = this.match(/^\}/);
193        if (!m || !m[0]) return;
194  
195        var brace = this.pop('brace');
196        var node = pos(new Node({
197          type: 'brace.close',
198          val: m[0]
199        }));
200  
201        if (!this.isType(brace, 'brace')) {
202          if (this.options.strict) {
203            throw new Error('missing opening "{"');
204          }
205          node.type = 'text';
206          node.multiplier = 0;
207          node.escaped = true;
208          return node;
209        }
210  
211        var prev = this.prev();
212        var last = utils.last(prev.nodes);
213        if (last.text) {
214          var lastNode = utils.last(last.nodes);
215          if (lastNode.val === ')' && /[!@*?+]\(/.test(last.text)) {
216            var open = last.nodes[0];
217            var text = last.nodes[1];
218            if (open.type === 'brace.open' && text && text.type === 'text') {
219              text.optimize = false;
220            }
221          }
222        }
223  
224        if (brace.nodes.length > 2) {
225          var first = brace.nodes[1];
226          if (first.type === 'text' && first.val === ',') {
227            brace.nodes.splice(1, 1);
228            brace.nodes.push(first);
229          }
230        }
231  
232        brace.push(node);
233      })
234  
235      /**
236       * Capture boundary characters
237       */
238  
239      .set('boundary', function() {
240        var pos = this.position();
241        var m = this.match(/^[$^](?!\{)/);
242        if (!m) return;
243        return pos(new Node({
244          type: 'text',
245          val: m[0]
246        }));
247      })
248  
249      /**
250       * One or zero, non-comma characters wrapped in braces
251       */
252  
253      .set('nobrace', function() {
254        var isInside = this.isInside('brace');
255        var pos = this.position();
256        var m = this.match(/^\{[^,]?\}/);
257        if (!m) return;
258  
259        var prev = this.prev();
260        var val = m[0];
261  
262        if (isInside && prev.type === 'brace') {
263          prev.text = prev.text || '';
264          prev.text += val;
265        }
266  
267        return pos(new Node({
268          type: 'text',
269          multiplier: 0,
270          val: val
271        }));
272      })
273  
274      /**
275       * Text
276       */
277  
278      .set('text', function() {
279        var isInside = this.isInside('brace');
280        var pos = this.position();
281        var m = this.match(/^((?!\\)[^${}[\]])+/);
282        if (!m) return;
283  
284        var prev = this.prev();
285        var val = m[0];
286  
287        if (isInside && prev.type === 'brace') {
288          prev.text = prev.text || '';
289          prev.text += val;
290        }
291  
292        var node = pos(new Node({
293          type: 'text',
294          multiplier: 1,
295          val: val
296        }));
297  
298        return concatNodes.call(this, pos, node, prev, options);
299      });
300  };
301  
302  /**
303   * Returns true if the character is an extglob character.
304   */
305  
306  function isExtglobChar(ch) {
307    return ch === '!' || ch === '@' || ch === '*' || ch === '?' || ch === '+';
308  }
309  
310  /**
311   * Combine text nodes, and calculate empty sets (`{,,}`)
312   * @param {Function} `pos` Function to calculate node position
313   * @param {Object} `node` AST node
314   * @return {Object}
315   */
316  
317  function concatNodes(pos, node, parent, options) {
318    node.orig = node.val;
319    var prev = this.prev();
320    var last = utils.last(prev.nodes);
321    var isEscaped = false;
322  
323    if (node.val.length > 1) {
324      var a = node.val.charAt(0);
325      var b = node.val.slice(-1);
326  
327      isEscaped = (a === '"' && b === '"')
328        || (a === "'" && b === "'")
329        || (a === '`' && b === '`');
330    }
331  
332    if (isEscaped && options.unescape !== false) {
333      node.val = node.val.slice(1, node.val.length - 1);
334      node.escaped = true;
335    }
336  
337    if (node.match) {
338      var match = node.match[1];
339      if (!match || match.indexOf('}') === -1) {
340        match = node.match[0];
341      }
342  
343      // replace each set with a single ","
344      var val = match.replace(/\{/g, ',').replace(/\}/g, '');
345      node.multiplier *= val.length;
346      node.val = '';
347    }
348  
349    var simpleText = last.type === 'text'
350      && last.multiplier === 1
351      && node.multiplier === 1
352      && node.val;
353  
354    if (simpleText) {
355      last.val += node.val;
356      return;
357    }
358  
359    prev.push(node);
360  }