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