Initial commit
[yaffs-website] / node_modules / fill-range / index.js
1 /*!
2  * fill-range <https://github.com/jonschlinkert/fill-range>
3  *
4  * Copyright (c) 2014-2015, Jon Schlinkert.
5  * Licensed under the MIT License.
6  */
7
8 'use strict';
9
10 var isObject = require('isobject');
11 var isNumber = require('is-number');
12 var randomize = require('randomatic');
13 var repeatStr = require('repeat-string');
14 var repeat = require('repeat-element');
15
16 /**
17  * Expose `fillRange`
18  */
19
20 module.exports = fillRange;
21
22 /**
23  * Return a range of numbers or letters.
24  *
25  * @param  {String} `a` Start of the range
26  * @param  {String} `b` End of the range
27  * @param  {String} `step` Increment or decrement to use.
28  * @param  {Function} `fn` Custom function to modify each element in the range.
29  * @return {Array}
30  */
31
32 function fillRange(a, b, step, options, fn) {
33   if (a == null || b == null) {
34     throw new Error('fill-range expects the first and second args to be strings.');
35   }
36
37   if (typeof step === 'function') {
38     fn = step; options = {}; step = null;
39   }
40
41   if (typeof options === 'function') {
42     fn = options; options = {};
43   }
44
45   if (isObject(step)) {
46     options = step; step = '';
47   }
48
49   var expand, regex = false, sep = '';
50   var opts = options || {};
51
52   if (typeof opts.silent === 'undefined') {
53     opts.silent = true;
54   }
55
56   step = step || opts.step;
57
58   // store a ref to unmodified arg
59   var origA = a, origB = b;
60
61   b = (b.toString() === '-0') ? 0 : b;
62
63   if (opts.optimize || opts.makeRe) {
64     step = step ? (step += '~') : step;
65     expand = true;
66     regex = true;
67     sep = '~';
68   }
69
70   // handle special step characters
71   if (typeof step === 'string') {
72     var match = stepRe().exec(step);
73
74     if (match) {
75       var i = match.index;
76       var m = match[0];
77
78       // repeat string
79       if (m === '+') {
80         return repeat(a, b);
81
82       // randomize a, `b` times
83       } else if (m === '?') {
84         return [randomize(a, b)];
85
86       // expand right, no regex reduction
87       } else if (m === '>') {
88         step = step.substr(0, i) + step.substr(i + 1);
89         expand = true;
90
91       // expand to an array, or if valid create a reduced
92       // string for a regex logic `or`
93       } else if (m === '|') {
94         step = step.substr(0, i) + step.substr(i + 1);
95         expand = true;
96         regex = true;
97         sep = m;
98
99       // expand to an array, or if valid create a reduced
100       // string for a regex range
101       } else if (m === '~') {
102         step = step.substr(0, i) + step.substr(i + 1);
103         expand = true;
104         regex = true;
105         sep = m;
106       }
107     } else if (!isNumber(step)) {
108       if (!opts.silent) {
109         throw new TypeError('fill-range: invalid step.');
110       }
111       return null;
112     }
113   }
114
115   if (/[.&*()[\]^%$#@!]/.test(a) || /[.&*()[\]^%$#@!]/.test(b)) {
116     if (!opts.silent) {
117       throw new RangeError('fill-range: invalid range arguments.');
118     }
119     return null;
120   }
121
122   // has neither a letter nor number, or has both letters and numbers
123   // this needs to be after the step logic
124   if (!noAlphaNum(a) || !noAlphaNum(b) || hasBoth(a) || hasBoth(b)) {
125     if (!opts.silent) {
126       throw new RangeError('fill-range: invalid range arguments.');
127     }
128     return null;
129   }
130
131   // validate arguments
132   var isNumA = isNumber(zeros(a));
133   var isNumB = isNumber(zeros(b));
134
135   if ((!isNumA && isNumB) || (isNumA && !isNumB)) {
136     if (!opts.silent) {
137       throw new TypeError('fill-range: first range argument is incompatible with second.');
138     }
139     return null;
140   }
141
142   // by this point both are the same, so we
143   // can use A to check going forward.
144   var isNum = isNumA;
145   var num = formatStep(step);
146
147   // is the range alphabetical? or numeric?
148   if (isNum) {
149     // if numeric, coerce to an integer
150     a = +a; b = +b;
151   } else {
152     // otherwise, get the charCode to expand alpha ranges
153     a = a.charCodeAt(0);
154     b = b.charCodeAt(0);
155   }
156
157   // is the pattern descending?
158   var isDescending = a > b;
159
160   // don't create a character class if the args are < 0
161   if (a < 0 || b < 0) {
162     expand = false;
163     regex = false;
164   }
165
166   // detect padding
167   var padding = isPadded(origA, origB);
168   var res, pad, arr = [];
169   var ii = 0;
170
171   // character classes, ranges and logical `or`
172   if (regex) {
173     if (shouldExpand(a, b, num, isNum, padding, opts)) {
174       // make sure the correct separator is used
175       if (sep === '|' || sep === '~') {
176         sep = detectSeparator(a, b, num, isNum, isDescending);
177       }
178       return wrap([origA, origB], sep, opts);
179     }
180   }
181
182   while (isDescending ? (a >= b) : (a <= b)) {
183     if (padding && isNum) {
184       pad = padding(a);
185     }
186
187     // custom function
188     if (typeof fn === 'function') {
189       res = fn(a, isNum, pad, ii++);
190
191     // letters
192     } else if (!isNum) {
193       if (regex && isInvalidChar(a)) {
194         res = null;
195       } else {
196         res = String.fromCharCode(a);
197       }
198
199     // numbers
200     } else {
201       res = formatPadding(a, pad);
202     }
203
204     // add result to the array, filtering any nulled values
205     if (res !== null) arr.push(res);
206
207     // increment or decrement
208     if (isDescending) {
209       a -= num;
210     } else {
211       a += num;
212     }
213   }
214
215   // now that the array is expanded, we need to handle regex
216   // character classes, ranges or logical `or` that wasn't
217   // already handled before the loop
218   if ((regex || expand) && !opts.noexpand) {
219     // make sure the correct separator is used
220     if (sep === '|' || sep === '~') {
221       sep = detectSeparator(a, b, num, isNum, isDescending);
222     }
223     if (arr.length === 1 || a < 0 || b < 0) { return arr; }
224     return wrap(arr, sep, opts);
225   }
226
227   return arr;
228 }
229
230 /**
231  * Wrap the string with the correct regex
232  * syntax.
233  */
234
235 function wrap(arr, sep, opts) {
236   if (sep === '~') { sep = '-'; }
237   var str = arr.join(sep);
238   var pre = opts && opts.regexPrefix;
239
240   // regex logical `or`
241   if (sep === '|') {
242     str = pre ? pre + str : str;
243     str = '(' + str + ')';
244   }
245
246   // regex character class
247   if (sep === '-') {
248     str = (pre && pre === '^')
249       ? pre + str
250       : str;
251     str = '[' + str + ']';
252   }
253   return [str];
254 }
255
256 /**
257  * Check for invalid characters
258  */
259
260 function isCharClass(a, b, step, isNum, isDescending) {
261   if (isDescending) { return false; }
262   if (isNum) { return a <= 9 && b <= 9; }
263   if (a < b) { return step === 1; }
264   return false;
265 }
266
267 /**
268  * Detect the correct separator to use
269  */
270
271 function shouldExpand(a, b, num, isNum, padding, opts) {
272   if (isNum && (a > 9 || b > 9)) { return false; }
273   return !padding && num === 1 && a < b;
274 }
275
276 /**
277  * Detect the correct separator to use
278  */
279
280 function detectSeparator(a, b, step, isNum, isDescending) {
281   var isChar = isCharClass(a, b, step, isNum, isDescending);
282   if (!isChar) {
283     return '|';
284   }
285   return '~';
286 }
287
288 /**
289  * Correctly format the step based on type
290  */
291
292 function formatStep(step) {
293   return Math.abs(step >> 0) || 1;
294 }
295
296 /**
297  * Format padding, taking leading `-` into account
298  */
299
300 function formatPadding(ch, pad) {
301   var res = pad ? pad + ch : ch;
302   if (pad && ch.toString().charAt(0) === '-') {
303     res = '-' + pad + ch.toString().substr(1);
304   }
305   return res.toString();
306 }
307
308 /**
309  * Check for invalid characters
310  */
311
312 function isInvalidChar(str) {
313   var ch = toStr(str);
314   return ch === '\\'
315     || ch === '['
316     || ch === ']'
317     || ch === '^'
318     || ch === '('
319     || ch === ')'
320     || ch === '`';
321 }
322
323 /**
324  * Convert to a string from a charCode
325  */
326
327 function toStr(ch) {
328   return String.fromCharCode(ch);
329 }
330
331
332 /**
333  * Step regex
334  */
335
336 function stepRe() {
337   return /\?|>|\||\+|\~/g;
338 }
339
340 /**
341  * Return true if `val` has either a letter
342  * or a number
343  */
344
345 function noAlphaNum(val) {
346   return /[a-z0-9]/i.test(val);
347 }
348
349 /**
350  * Return true if `val` has both a letter and
351  * a number (invalid)
352  */
353
354 function hasBoth(val) {
355   return /[a-z][0-9]|[0-9][a-z]/i.test(val);
356 }
357
358 /**
359  * Normalize zeros for checks
360  */
361
362 function zeros(val) {
363   if (/^-*0+$/.test(val.toString())) {
364     return '0';
365   }
366   return val;
367 }
368
369 /**
370  * Return true if `val` has leading zeros,
371  * or a similar valid pattern.
372  */
373
374 function hasZeros(val) {
375   return /[^.]\.|^-*0+[0-9]/.test(val);
376 }
377
378 /**
379  * If the string is padded, returns a curried function with
380  * the a cached padding string, or `false` if no padding.
381  *
382  * @param  {*} `origA` String or number.
383  * @return {String|Boolean}
384  */
385
386 function isPadded(origA, origB) {
387   if (hasZeros(origA) || hasZeros(origB)) {
388     var alen = length(origA);
389     var blen = length(origB);
390
391     var len = alen >= blen
392       ? alen
393       : blen;
394
395     return function (a) {
396       return repeatStr('0', len - length(a));
397     };
398   }
399   return false;
400 }
401
402 /**
403  * Get the string length of `val`
404  */
405
406 function length(val) {
407   return val.toString().length;
408 }