utils.js
  1  'use strict';
  2  
  3  var utils = module.exports;
  4  var path = require('path');
  5  
  6  /**
  7   * Module dependencies
  8   */
  9  
 10  var isWindows = require('is-windows')();
 11  var Snapdragon = require('snapdragon');
 12  utils.define = require('define-property');
 13  utils.diff = require('arr-diff');
 14  utils.extend = require('extend-shallow');
 15  utils.pick = require('object.pick');
 16  utils.typeOf = require('kind-of');
 17  utils.unique = require('array-unique');
 18  
 19  /**
 20   * Returns true if the given value is effectively an empty string
 21   */
 22  
 23  utils.isEmptyString = function(val) {
 24    return String(val) === '' || String(val) === './';
 25  };
 26  
 27  /**
 28   * Returns true if the platform is windows, or `path.sep` is `\\`.
 29   * This is defined as a function to allow `path.sep` to be set in unit tests,
 30   * or by the user, if there is a reason to do so.
 31   * @return {Boolean}
 32   */
 33  
 34  utils.isWindows = function() {
 35    return path.sep === '\\' || isWindows === true;
 36  };
 37  
 38  /**
 39   * Return the last element from an array
 40   */
 41  
 42  utils.last = function(arr, n) {
 43    return arr[arr.length - (n || 1)];
 44  };
 45  
 46  /**
 47   * Get the `Snapdragon` instance to use
 48   */
 49  
 50  utils.instantiate = function(ast, options) {
 51    var snapdragon;
 52    // if an instance was created by `.parse`, use that instance
 53    if (utils.typeOf(ast) === 'object' && ast.snapdragon) {
 54      snapdragon = ast.snapdragon;
 55    // if the user supplies an instance on options, use that instance
 56    } else if (utils.typeOf(options) === 'object' && options.snapdragon) {
 57      snapdragon = options.snapdragon;
 58    // create a new instance
 59    } else {
 60      snapdragon = new Snapdragon(options);
 61    }
 62  
 63    utils.define(snapdragon, 'parse', function(str, options) {
 64      var parsed = Snapdragon.prototype.parse.call(this, str, options);
 65      parsed.input = str;
 66  
 67      // escape unmatched brace/bracket/parens
 68      var last = this.parser.stack.pop();
 69      if (last && this.options.strictErrors !== true) {
 70        var open = last.nodes[0];
 71        var inner = last.nodes[1];
 72        if (last.type === 'bracket') {
 73          if (inner.val.charAt(0) === '[') {
 74            inner.val = '\\' + inner.val;
 75          }
 76  
 77        } else {
 78          open.val = '\\' + open.val;
 79          var sibling = open.parent.nodes[1];
 80          if (sibling.type === 'star') {
 81            sibling.loose = true;
 82          }
 83        }
 84      }
 85  
 86      // add non-enumerable parser reference
 87      utils.define(parsed, 'parser', this.parser);
 88      return parsed;
 89    });
 90  
 91    return snapdragon;
 92  };
 93  
 94  /**
 95   * Create the key to use for memoization. The key is generated
 96   * by iterating over the options and concatenating key-value pairs
 97   * to the pattern string.
 98   */
 99  
100  utils.createKey = function(pattern, options) {
101    if (typeof options === 'undefined') {
102      return pattern;
103    }
104    var key = pattern;
105    for (var prop in options) {
106      if (options.hasOwnProperty(prop)) {
107        key += ';' + prop + '=' + String(options[prop]);
108      }
109    }
110    return key;
111  };
112  
113  /**
114   * Cast `val` to an array
115   * @return {Array}
116   */
117  
118  utils.arrayify = function(val) {
119    if (typeof val === 'string') return [val];
120    return val ? (Array.isArray(val) ? val : [val]) : [];
121  };
122  
123  /**
124   * Return true if `val` is a non-empty string
125   */
126  
127  utils.isString = function(val) {
128    return typeof val === 'string';
129  };
130  
131  /**
132   * Return true if `val` is a non-empty string
133   */
134  
135  utils.isRegex = function(val) {
136    return utils.typeOf(val) === 'regexp';
137  };
138  
139  /**
140   * Return true if `val` is a non-empty string
141   */
142  
143  utils.isObject = function(val) {
144    return utils.typeOf(val) === 'object';
145  };
146  
147  /**
148   * Escape regex characters in the given string
149   */
150  
151  utils.escapeRegex = function(str) {
152    return str.replace(/[-[\]{}()^$|*+?.\\/\s]/g, '\\$&');
153  };
154  
155  /**
156   * Combines duplicate characters in the provided `input` string.
157   * @param {String} `input`
158   * @returns {String}
159   */
160  
161  utils.combineDupes = function(input, patterns) {
162    patterns = utils.arrayify(patterns).join('|').split('|');
163    patterns = patterns.map(function(s) {
164      return s.replace(/\\?([+*\\/])/g, '\\$1');
165    });
166    var substr = patterns.join('|');
167    var regex = new RegExp('(' + substr + ')(?=\\1)', 'g');
168    return input.replace(regex, '');
169  };
170  
171  /**
172   * Returns true if the given `str` has special characters
173   */
174  
175  utils.hasSpecialChars = function(str) {
176    return /(?:(?:(^|\/)[!.])|[*?+()|[\]{}]|[+@]\()/.test(str);
177  };
178  
179  /**
180   * Normalize slashes in the given filepath.
181   *
182   * @param {String} `filepath`
183   * @return {String}
184   */
185  
186  utils.toPosixPath = function(str) {
187    return str.replace(/\\+/g, '/');
188  };
189  
190  /**
191   * Strip backslashes before special characters in a string.
192   *
193   * @param {String} `str`
194   * @return {String}
195   */
196  
197  utils.unescape = function(str) {
198    return utils.toPosixPath(str.replace(/\\(?=[*+?!.])/g, ''));
199  };
200  
201  /**
202   * Strip the drive letter from a windows filepath
203   * @param {String} `fp`
204   * @return {String}
205   */
206  
207  utils.stripDrive = function(fp) {
208    return utils.isWindows() ? fp.replace(/^[a-z]:[\\/]+?/i, '/') : fp;
209  };
210  
211  /**
212   * Strip the prefix from a filepath
213   * @param {String} `fp`
214   * @return {String}
215   */
216  
217  utils.stripPrefix = function(str) {
218    if (str.charAt(0) === '.' && (str.charAt(1) === '/' || str.charAt(1) === '\\')) {
219      return str.slice(2);
220    }
221    return str;
222  };
223  
224  /**
225   * Returns true if `str` is a common character that doesn't need
226   * to be processed to be used for matching.
227   * @param {String} `str`
228   * @return {Boolean}
229   */
230  
231  utils.isSimpleChar = function(str) {
232    return str.trim() === '' || str === '.';
233  };
234  
235  /**
236   * Returns true if the given str is an escaped or
237   * unescaped path character
238   */
239  
240  utils.isSlash = function(str) {
241    return str === '/' || str === '\\/' || str === '\\' || str === '\\\\';
242  };
243  
244  /**
245   * Returns a function that returns true if the given
246   * pattern matches or contains a `filepath`
247   *
248   * @param {String} `pattern`
249   * @return {Function}
250   */
251  
252  utils.matchPath = function(pattern, options) {
253    return (options && options.contains)
254      ? utils.containsPattern(pattern, options)
255      : utils.equalsPattern(pattern, options);
256  };
257  
258  /**
259   * Returns true if the given (original) filepath or unixified path are equal
260   * to the given pattern.
261   */
262  
263  utils._equals = function(filepath, unixPath, pattern) {
264    return pattern === filepath || pattern === unixPath;
265  };
266  
267  /**
268   * Returns true if the given (original) filepath or unixified path contain
269   * the given pattern.
270   */
271  
272  utils._contains = function(filepath, unixPath, pattern) {
273    return filepath.indexOf(pattern) !== -1 || unixPath.indexOf(pattern) !== -1;
274  };
275  
276  /**
277   * Returns a function that returns true if the given
278   * pattern is the same as a given `filepath`
279   *
280   * @param {String} `pattern`
281   * @return {Function}
282   */
283  
284  utils.equalsPattern = function(pattern, options) {
285    var unixify = utils.unixify(options);
286    options = options || {};
287  
288    return function fn(filepath) {
289      var equal = utils._equals(filepath, unixify(filepath), pattern);
290      if (equal === true || options.nocase !== true) {
291        return equal;
292      }
293      var lower = filepath.toLowerCase();
294      return utils._equals(lower, unixify(lower), pattern);
295    };
296  };
297  
298  /**
299   * Returns a function that returns true if the given
300   * pattern contains a `filepath`
301   *
302   * @param {String} `pattern`
303   * @return {Function}
304   */
305  
306  utils.containsPattern = function(pattern, options) {
307    var unixify = utils.unixify(options);
308    options = options || {};
309  
310    return function(filepath) {
311      var contains = utils._contains(filepath, unixify(filepath), pattern);
312      if (contains === true || options.nocase !== true) {
313        return contains;
314      }
315      var lower = filepath.toLowerCase();
316      return utils._contains(lower, unixify(lower), pattern);
317    };
318  };
319  
320  /**
321   * Returns a function that returns true if the given
322   * regex matches the `filename` of a file path.
323   *
324   * @param {RegExp} `re` Matching regex
325   * @return {Function}
326   */
327  
328  utils.matchBasename = function(re) {
329    return function(filepath) {
330      return re.test(filepath) || re.test(path.basename(filepath));
331    };
332  };
333  
334  /**
335   * Returns the given value unchanced.
336   * @return {any}
337   */
338  
339  utils.identity = function(val) {
340    return val;
341  };
342  
343  /**
344   * Determines the filepath to return based on the provided options.
345   * @return {any}
346   */
347  
348  utils.value = function(str, unixify, options) {
349    if (options && options.unixify === false) {
350      return str;
351    }
352    if (options && typeof options.unixify === 'function') {
353      return options.unixify(str);
354    }
355    return unixify(str);
356  };
357  
358  /**
359   * Returns a function that normalizes slashes in a string to forward
360   * slashes, strips `./` from beginning of paths, and optionally unescapes
361   * special characters.
362   * @return {Function}
363   */
364  
365  utils.unixify = function(options) {
366    var opts = options || {};
367    return function(filepath) {
368      if (opts.stripPrefix !== false) {
369        filepath = utils.stripPrefix(filepath);
370      }
371      if (opts.unescape === true) {
372        filepath = utils.unescape(filepath);
373      }
374      if (opts.unixify === true || utils.isWindows()) {
375        filepath = utils.toPosixPath(filepath);
376      }
377      return filepath;
378    };
379  };