utils.js
  1  'use strict';
  2  
  3  var splitString = require('split-string');
  4  var utils = module.exports;
  5  
  6  /**
  7   * Module dependencies
  8   */
  9  
 10  utils.extend = require('extend-shallow');
 11  utils.flatten = require('arr-flatten');
 12  utils.isObject = require('isobject');
 13  utils.fillRange = require('fill-range');
 14  utils.repeat = require('repeat-element');
 15  utils.unique = require('array-unique');
 16  
 17  utils.define = function(obj, key, val) {
 18    Object.defineProperty(obj, key, {
 19      writable: true,
 20      configurable: true,
 21      enumerable: false,
 22      value: val
 23    });
 24  };
 25  
 26  /**
 27   * Returns true if the given string contains only empty brace sets.
 28   */
 29  
 30  utils.isEmptySets = function(str) {
 31    return /^(?:\{,\})+$/.test(str);
 32  };
 33  
 34  /**
 35   * Returns true if the given string contains only empty brace sets.
 36   */
 37  
 38  utils.isQuotedString = function(str) {
 39    var open = str.charAt(0);
 40    if (open === '\'' || open === '"' || open === '`') {
 41      return str.slice(-1) === open;
 42    }
 43    return false;
 44  };
 45  
 46  /**
 47   * Create the key to use for memoization. The unique key is generated
 48   * by iterating over the options and concatenating key-value pairs
 49   * to the pattern string.
 50   */
 51  
 52  utils.createKey = function(pattern, options) {
 53    var id = pattern;
 54    if (typeof options === 'undefined') {
 55      return id;
 56    }
 57    var keys = Object.keys(options);
 58    for (var i = 0; i < keys.length; i++) {
 59      var key = keys[i];
 60      id += ';' + key + '=' + String(options[key]);
 61    }
 62    return id;
 63  };
 64  
 65  /**
 66   * Normalize options
 67   */
 68  
 69  utils.createOptions = function(options) {
 70    var opts = utils.extend.apply(null, arguments);
 71    if (typeof opts.expand === 'boolean') {
 72      opts.optimize = !opts.expand;
 73    }
 74    if (typeof opts.optimize === 'boolean') {
 75      opts.expand = !opts.optimize;
 76    }
 77    if (opts.optimize === true) {
 78      opts.makeRe = true;
 79    }
 80    return opts;
 81  };
 82  
 83  /**
 84   * Join patterns in `a` to patterns in `b`
 85   */
 86  
 87  utils.join = function(a, b, options) {
 88    options = options || {};
 89    a = utils.arrayify(a);
 90    b = utils.arrayify(b);
 91  
 92    if (!a.length) return b;
 93    if (!b.length) return a;
 94  
 95    var len = a.length;
 96    var idx = -1;
 97    var arr = [];
 98  
 99    while (++idx < len) {
100      var val = a[idx];
101      if (Array.isArray(val)) {
102        for (var i = 0; i < val.length; i++) {
103          val[i] = utils.join(val[i], b, options);
104        }
105        arr.push(val);
106        continue;
107      }
108  
109      for (var j = 0; j < b.length; j++) {
110        var bval = b[j];
111  
112        if (Array.isArray(bval)) {
113          arr.push(utils.join(val, bval, options));
114        } else {
115          arr.push(val + bval);
116        }
117      }
118    }
119    return arr;
120  };
121  
122  /**
123   * Split the given string on `,` if not escaped.
124   */
125  
126  utils.split = function(str, options) {
127    var opts = utils.extend({sep: ','}, options);
128    if (typeof opts.keepQuotes !== 'boolean') {
129      opts.keepQuotes = true;
130    }
131    if (opts.unescape === false) {
132      opts.keepEscaping = true;
133    }
134    return splitString(str, opts, utils.escapeBrackets(opts));
135  };
136  
137  /**
138   * Expand ranges or sets in the given `pattern`.
139   *
140   * @param {String} `str`
141   * @param {Object} `options`
142   * @return {Object}
143   */
144  
145  utils.expand = function(str, options) {
146    var opts = utils.extend({rangeLimit: 10000}, options);
147    var segs = utils.split(str, opts);
148    var tok = { segs: segs };
149  
150    if (utils.isQuotedString(str)) {
151      return tok;
152    }
153  
154    if (opts.rangeLimit === true) {
155      opts.rangeLimit = 10000;
156    }
157  
158    if (segs.length > 1) {
159      if (opts.optimize === false) {
160        tok.val = segs[0];
161        return tok;
162      }
163  
164      tok.segs = utils.stringifyArray(tok.segs);
165    } else if (segs.length === 1) {
166      var arr = str.split('..');
167  
168      if (arr.length === 1) {
169        tok.val = tok.segs[tok.segs.length - 1] || tok.val || str;
170        tok.segs = [];
171        return tok;
172      }
173  
174      if (arr.length === 2 && arr[0] === arr[1]) {
175        tok.escaped = true;
176        tok.val = arr[0];
177        tok.segs = [];
178        return tok;
179      }
180  
181      if (arr.length > 1) {
182        if (opts.optimize !== false) {
183          opts.optimize = true;
184          delete opts.expand;
185        }
186  
187        if (opts.optimize !== true) {
188          var min = Math.min(arr[0], arr[1]);
189          var max = Math.max(arr[0], arr[1]);
190          var step = arr[2] || 1;
191  
192          if (opts.rangeLimit !== false && ((max - min) / step >= opts.rangeLimit)) {
193            throw new RangeError('expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.');
194          }
195        }
196  
197        arr.push(opts);
198        tok.segs = utils.fillRange.apply(null, arr);
199  
200        if (!tok.segs.length) {
201          tok.escaped = true;
202          tok.val = str;
203          return tok;
204        }
205  
206        if (opts.optimize === true) {
207          tok.segs = utils.stringifyArray(tok.segs);
208        }
209  
210        if (tok.segs === '') {
211          tok.val = str;
212        } else {
213          tok.val = tok.segs[0];
214        }
215        return tok;
216      }
217    } else {
218      tok.val = str;
219    }
220    return tok;
221  };
222  
223  /**
224   * Ensure commas inside brackets and parens are not split.
225   * @param {Object} `tok` Token from the `split-string` module
226   * @return {undefined}
227   */
228  
229  utils.escapeBrackets = function(options) {
230    return function(tok) {
231      if (tok.escaped && tok.val === 'b') {
232        tok.val = '\\b';
233        return;
234      }
235  
236      if (tok.val !== '(' && tok.val !== '[') return;
237      var opts = utils.extend({}, options);
238      var brackets = [];
239      var parens = [];
240      var stack = [];
241      var val = tok.val;
242      var str = tok.str;
243      var i = tok.idx - 1;
244  
245      while (++i < str.length) {
246        var ch = str[i];
247  
248        if (ch === '\\') {
249          val += (opts.keepEscaping === false ? '' : ch) + str[++i];
250          continue;
251        }
252  
253        if (ch === '(') {
254          parens.push(ch);
255          stack.push(ch);
256        }
257  
258        if (ch === '[') {
259          brackets.push(ch);
260          stack.push(ch);
261        }
262  
263        if (ch === ')') {
264          parens.pop();
265          stack.pop();
266          if (!stack.length) {
267            val += ch;
268            break;
269          }
270        }
271  
272        if (ch === ']') {
273          brackets.pop();
274          stack.pop();
275          if (!stack.length) {
276            val += ch;
277            break;
278          }
279        }
280        val += ch;
281      }
282  
283      tok.split = false;
284      tok.val = val.slice(1);
285      tok.idx = i;
286    };
287  };
288  
289  /**
290   * Returns true if the given string looks like a regex quantifier
291   * @return {Boolean}
292   */
293  
294  utils.isQuantifier = function(str) {
295    return /^(?:[0-9]?,[0-9]|[0-9],)$/.test(str);
296  };
297  
298  /**
299   * Cast `val` to an array.
300   * @param {*} `val`
301   */
302  
303  utils.stringifyArray = function(arr) {
304    return [utils.arrayify(arr).join('|')];
305  };
306  
307  /**
308   * Cast `val` to an array.
309   * @param {*} `val`
310   */
311  
312  utils.arrayify = function(arr) {
313    if (typeof arr === 'undefined') {
314      return [];
315    }
316    if (typeof arr === 'string') {
317      return [arr];
318    }
319    return arr;
320  };
321  
322  /**
323   * Returns true if the given `str` is a non-empty string
324   * @return {Boolean}
325   */
326  
327  utils.isString = function(str) {
328    return str != null && typeof str === 'string';
329  };
330  
331  /**
332   * Get the last element from `array`
333   * @param {Array} `array`
334   * @return {*}
335   */
336  
337  utils.last = function(arr, n) {
338    return arr[arr.length - (n || 1)];
339  };
340  
341  utils.escapeRegex = function(str) {
342    return str.replace(/\\?([!^*?()[\]{}+?/])/g, '\\$1');
343  };