source: imaps-frontend/node_modules/ignore/index.js

main
Last change on this file was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

  • Property mode set to 100644
File size: 15.8 KB
Line 
1// A simple implementation of make-array
2function makeArray (subject) {
3 return Array.isArray(subject)
4 ? subject
5 : [subject]
6}
7
8const EMPTY = ''
9const SPACE = ' '
10const ESCAPE = '\\'
11const REGEX_TEST_BLANK_LINE = /^\s+$/
12const REGEX_INVALID_TRAILING_BACKSLASH = /(?:[^\\]|^)\\$/
13const REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION = /^\\!/
14const REGEX_REPLACE_LEADING_EXCAPED_HASH = /^\\#/
15const REGEX_SPLITALL_CRLF = /\r?\n/g
16// /foo,
17// ./foo,
18// ../foo,
19// .
20// ..
21const REGEX_TEST_INVALID_PATH = /^\.*\/|^\.+$/
22
23const SLASH = '/'
24
25// Do not use ternary expression here, since "istanbul ignore next" is buggy
26let TMP_KEY_IGNORE = 'node-ignore'
27/* istanbul ignore else */
28if (typeof Symbol !== 'undefined') {
29 TMP_KEY_IGNORE = Symbol.for('node-ignore')
30}
31const KEY_IGNORE = TMP_KEY_IGNORE
32
33const define = (object, key, value) =>
34 Object.defineProperty(object, key, {value})
35
36const REGEX_REGEXP_RANGE = /([0-z])-([0-z])/g
37
38const RETURN_FALSE = () => false
39
40// Sanitize the range of a regular expression
41// The cases are complicated, see test cases for details
42const sanitizeRange = range => range.replace(
43 REGEX_REGEXP_RANGE,
44 (match, from, to) => from.charCodeAt(0) <= to.charCodeAt(0)
45 ? match
46 // Invalid range (out of order) which is ok for gitignore rules but
47 // fatal for JavaScript regular expression, so eliminate it.
48 : EMPTY
49)
50
51// See fixtures #59
52const cleanRangeBackSlash = slashes => {
53 const {length} = slashes
54 return slashes.slice(0, length - length % 2)
55}
56
57// > If the pattern ends with a slash,
58// > it is removed for the purpose of the following description,
59// > but it would only find a match with a directory.
60// > In other words, foo/ will match a directory foo and paths underneath it,
61// > but will not match a regular file or a symbolic link foo
62// > (this is consistent with the way how pathspec works in general in Git).
63// '`foo/`' will not match regular file '`foo`' or symbolic link '`foo`'
64// -> ignore-rules will not deal with it, because it costs extra `fs.stat` call
65// you could use option `mark: true` with `glob`
66
67// '`foo/`' should not continue with the '`..`'
68const REPLACERS = [
69
70 [
71 // remove BOM
72 // TODO:
73 // Other similar zero-width characters?
74 /^\uFEFF/,
75 () => EMPTY
76 ],
77
78 // > Trailing spaces are ignored unless they are quoted with backslash ("\")
79 [
80 // (a\ ) -> (a )
81 // (a ) -> (a)
82 // (a \ ) -> (a )
83 /\\?\s+$/,
84 match => match.indexOf('\\') === 0
85 ? SPACE
86 : EMPTY
87 ],
88
89 // replace (\ ) with ' '
90 [
91 /\\\s/g,
92 () => SPACE
93 ],
94
95 // Escape metacharacters
96 // which is written down by users but means special for regular expressions.
97
98 // > There are 12 characters with special meanings:
99 // > - the backslash \,
100 // > - the caret ^,
101 // > - the dollar sign $,
102 // > - the period or dot .,
103 // > - the vertical bar or pipe symbol |,
104 // > - the question mark ?,
105 // > - the asterisk or star *,
106 // > - the plus sign +,
107 // > - the opening parenthesis (,
108 // > - the closing parenthesis ),
109 // > - and the opening square bracket [,
110 // > - the opening curly brace {,
111 // > These special characters are often called "metacharacters".
112 [
113 /[\\$.|*+(){^]/g,
114 match => `\\${match}`
115 ],
116
117 [
118 // > a question mark (?) matches a single character
119 /(?!\\)\?/g,
120 () => '[^/]'
121 ],
122
123 // leading slash
124 [
125
126 // > A leading slash matches the beginning of the pathname.
127 // > For example, "/*.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c".
128 // A leading slash matches the beginning of the pathname
129 /^\//,
130 () => '^'
131 ],
132
133 // replace special metacharacter slash after the leading slash
134 [
135 /\//g,
136 () => '\\/'
137 ],
138
139 [
140 // > A leading "**" followed by a slash means match in all directories.
141 // > For example, "**/foo" matches file or directory "foo" anywhere,
142 // > the same as pattern "foo".
143 // > "**/foo/bar" matches file or directory "bar" anywhere that is directly
144 // > under directory "foo".
145 // Notice that the '*'s have been replaced as '\\*'
146 /^\^*\\\*\\\*\\\//,
147
148 // '**/foo' <-> 'foo'
149 () => '^(?:.*\\/)?'
150 ],
151
152 // starting
153 [
154 // there will be no leading '/'
155 // (which has been replaced by section "leading slash")
156 // If starts with '**', adding a '^' to the regular expression also works
157 /^(?=[^^])/,
158 function startingReplacer () {
159 // If has a slash `/` at the beginning or middle
160 return !/\/(?!$)/.test(this)
161 // > Prior to 2.22.1
162 // > If the pattern does not contain a slash /,
163 // > Git treats it as a shell glob pattern
164 // Actually, if there is only a trailing slash,
165 // git also treats it as a shell glob pattern
166
167 // After 2.22.1 (compatible but clearer)
168 // > If there is a separator at the beginning or middle (or both)
169 // > of the pattern, then the pattern is relative to the directory
170 // > level of the particular .gitignore file itself.
171 // > Otherwise the pattern may also match at any level below
172 // > the .gitignore level.
173 ? '(?:^|\\/)'
174
175 // > Otherwise, Git treats the pattern as a shell glob suitable for
176 // > consumption by fnmatch(3)
177 : '^'
178 }
179 ],
180
181 // two globstars
182 [
183 // Use lookahead assertions so that we could match more than one `'/**'`
184 /\\\/\\\*\\\*(?=\\\/|$)/g,
185
186 // Zero, one or several directories
187 // should not use '*', or it will be replaced by the next replacer
188
189 // Check if it is not the last `'/**'`
190 (_, index, str) => index + 6 < str.length
191
192 // case: /**/
193 // > A slash followed by two consecutive asterisks then a slash matches
194 // > zero or more directories.
195 // > For example, "a/**/b" matches "a/b", "a/x/b", "a/x/y/b" and so on.
196 // '/**/'
197 ? '(?:\\/[^\\/]+)*'
198
199 // case: /**
200 // > A trailing `"/**"` matches everything inside.
201
202 // #21: everything inside but it should not include the current folder
203 : '\\/.+'
204 ],
205
206 // normal intermediate wildcards
207 [
208 // Never replace escaped '*'
209 // ignore rule '\*' will match the path '*'
210
211 // 'abc.*/' -> go
212 // 'abc.*' -> skip this rule,
213 // coz trailing single wildcard will be handed by [trailing wildcard]
214 /(^|[^\\]+)(\\\*)+(?=.+)/g,
215
216 // '*.js' matches '.js'
217 // '*.js' doesn't match 'abc'
218 (_, p1, p2) => {
219 // 1.
220 // > An asterisk "*" matches anything except a slash.
221 // 2.
222 // > Other consecutive asterisks are considered regular asterisks
223 // > and will match according to the previous rules.
224 const unescaped = p2.replace(/\\\*/g, '[^\\/]*')
225 return p1 + unescaped
226 }
227 ],
228
229 [
230 // unescape, revert step 3 except for back slash
231 // For example, if a user escape a '\\*',
232 // after step 3, the result will be '\\\\\\*'
233 /\\\\\\(?=[$.|*+(){^])/g,
234 () => ESCAPE
235 ],
236
237 [
238 // '\\\\' -> '\\'
239 /\\\\/g,
240 () => ESCAPE
241 ],
242
243 [
244 // > The range notation, e.g. [a-zA-Z],
245 // > can be used to match one of the characters in a range.
246
247 // `\` is escaped by step 3
248 /(\\)?\[([^\]/]*?)(\\*)($|\])/g,
249 (match, leadEscape, range, endEscape, close) => leadEscape === ESCAPE
250 // '\\[bar]' -> '\\\\[bar\\]'
251 ? `\\[${range}${cleanRangeBackSlash(endEscape)}${close}`
252 : close === ']'
253 ? endEscape.length % 2 === 0
254 // A normal case, and it is a range notation
255 // '[bar]'
256 // '[bar\\\\]'
257 ? `[${sanitizeRange(range)}${endEscape}]`
258 // Invalid range notaton
259 // '[bar\\]' -> '[bar\\\\]'
260 : '[]'
261 : '[]'
262 ],
263
264 // ending
265 [
266 // 'js' will not match 'js.'
267 // 'ab' will not match 'abc'
268 /(?:[^*])$/,
269
270 // WTF!
271 // https://git-scm.com/docs/gitignore
272 // changes in [2.22.1](https://git-scm.com/docs/gitignore/2.22.1)
273 // which re-fixes #24, #38
274
275 // > If there is a separator at the end of the pattern then the pattern
276 // > will only match directories, otherwise the pattern can match both
277 // > files and directories.
278
279 // 'js*' will not match 'a.js'
280 // 'js/' will not match 'a.js'
281 // 'js' will match 'a.js' and 'a.js/'
282 match => /\/$/.test(match)
283 // foo/ will not match 'foo'
284 ? `${match}$`
285 // foo matches 'foo' and 'foo/'
286 : `${match}(?=$|\\/$)`
287 ],
288
289 // trailing wildcard
290 [
291 /(\^|\\\/)?\\\*$/,
292 (_, p1) => {
293 const prefix = p1
294 // '\^':
295 // '/*' does not match EMPTY
296 // '/*' does not match everything
297
298 // '\\\/':
299 // 'abc/*' does not match 'abc/'
300 ? `${p1}[^/]+`
301
302 // 'a*' matches 'a'
303 // 'a*' matches 'aa'
304 : '[^/]*'
305
306 return `${prefix}(?=$|\\/$)`
307 }
308 ],
309]
310
311// A simple cache, because an ignore rule only has only one certain meaning
312const regexCache = Object.create(null)
313
314// @param {pattern}
315const makeRegex = (pattern, ignoreCase) => {
316 let source = regexCache[pattern]
317
318 if (!source) {
319 source = REPLACERS.reduce(
320 (prev, current) => prev.replace(current[0], current[1].bind(pattern)),
321 pattern
322 )
323 regexCache[pattern] = source
324 }
325
326 return ignoreCase
327 ? new RegExp(source, 'i')
328 : new RegExp(source)
329}
330
331const isString = subject => typeof subject === 'string'
332
333// > A blank line matches no files, so it can serve as a separator for readability.
334const checkPattern = pattern => pattern
335 && isString(pattern)
336 && !REGEX_TEST_BLANK_LINE.test(pattern)
337 && !REGEX_INVALID_TRAILING_BACKSLASH.test(pattern)
338
339 // > A line starting with # serves as a comment.
340 && pattern.indexOf('#') !== 0
341
342const splitPattern = pattern => pattern.split(REGEX_SPLITALL_CRLF)
343
344class IgnoreRule {
345 constructor (
346 origin,
347 pattern,
348 negative,
349 regex
350 ) {
351 this.origin = origin
352 this.pattern = pattern
353 this.negative = negative
354 this.regex = regex
355 }
356}
357
358const createRule = (pattern, ignoreCase) => {
359 const origin = pattern
360 let negative = false
361
362 // > An optional prefix "!" which negates the pattern;
363 if (pattern.indexOf('!') === 0) {
364 negative = true
365 pattern = pattern.substr(1)
366 }
367
368 pattern = pattern
369 // > Put a backslash ("\") in front of the first "!" for patterns that
370 // > begin with a literal "!", for example, `"\!important!.txt"`.
371 .replace(REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION, '!')
372 // > Put a backslash ("\") in front of the first hash for patterns that
373 // > begin with a hash.
374 .replace(REGEX_REPLACE_LEADING_EXCAPED_HASH, '#')
375
376 const regex = makeRegex(pattern, ignoreCase)
377
378 return new IgnoreRule(
379 origin,
380 pattern,
381 negative,
382 regex
383 )
384}
385
386const throwError = (message, Ctor) => {
387 throw new Ctor(message)
388}
389
390const checkPath = (path, originalPath, doThrow) => {
391 if (!isString(path)) {
392 return doThrow(
393 `path must be a string, but got \`${originalPath}\``,
394 TypeError
395 )
396 }
397
398 // We don't know if we should ignore EMPTY, so throw
399 if (!path) {
400 return doThrow(`path must not be empty`, TypeError)
401 }
402
403 // Check if it is a relative path
404 if (checkPath.isNotRelative(path)) {
405 const r = '`path.relative()`d'
406 return doThrow(
407 `path should be a ${r} string, but got "${originalPath}"`,
408 RangeError
409 )
410 }
411
412 return true
413}
414
415const isNotRelative = path => REGEX_TEST_INVALID_PATH.test(path)
416
417checkPath.isNotRelative = isNotRelative
418checkPath.convert = p => p
419
420class Ignore {
421 constructor ({
422 ignorecase = true,
423 ignoreCase = ignorecase,
424 allowRelativePaths = false
425 } = {}) {
426 define(this, KEY_IGNORE, true)
427
428 this._rules = []
429 this._ignoreCase = ignoreCase
430 this._allowRelativePaths = allowRelativePaths
431 this._initCache()
432 }
433
434 _initCache () {
435 this._ignoreCache = Object.create(null)
436 this._testCache = Object.create(null)
437 }
438
439 _addPattern (pattern) {
440 // #32
441 if (pattern && pattern[KEY_IGNORE]) {
442 this._rules = this._rules.concat(pattern._rules)
443 this._added = true
444 return
445 }
446
447 if (checkPattern(pattern)) {
448 const rule = createRule(pattern, this._ignoreCase)
449 this._added = true
450 this._rules.push(rule)
451 }
452 }
453
454 // @param {Array<string> | string | Ignore} pattern
455 add (pattern) {
456 this._added = false
457
458 makeArray(
459 isString(pattern)
460 ? splitPattern(pattern)
461 : pattern
462 ).forEach(this._addPattern, this)
463
464 // Some rules have just added to the ignore,
465 // making the behavior changed.
466 if (this._added) {
467 this._initCache()
468 }
469
470 return this
471 }
472
473 // legacy
474 addPattern (pattern) {
475 return this.add(pattern)
476 }
477
478 // | ignored : unignored
479 // negative | 0:0 | 0:1 | 1:0 | 1:1
480 // -------- | ------- | ------- | ------- | --------
481 // 0 | TEST | TEST | SKIP | X
482 // 1 | TESTIF | SKIP | TEST | X
483
484 // - SKIP: always skip
485 // - TEST: always test
486 // - TESTIF: only test if checkUnignored
487 // - X: that never happen
488
489 // @param {boolean} whether should check if the path is unignored,
490 // setting `checkUnignored` to `false` could reduce additional
491 // path matching.
492
493 // @returns {TestResult} true if a file is ignored
494 _testOne (path, checkUnignored) {
495 let ignored = false
496 let unignored = false
497
498 this._rules.forEach(rule => {
499 const {negative} = rule
500 if (
501 unignored === negative && ignored !== unignored
502 || negative && !ignored && !unignored && !checkUnignored
503 ) {
504 return
505 }
506
507 const matched = rule.regex.test(path)
508
509 if (matched) {
510 ignored = !negative
511 unignored = negative
512 }
513 })
514
515 return {
516 ignored,
517 unignored
518 }
519 }
520
521 // @returns {TestResult}
522 _test (originalPath, cache, checkUnignored, slices) {
523 const path = originalPath
524 // Supports nullable path
525 && checkPath.convert(originalPath)
526
527 checkPath(
528 path,
529 originalPath,
530 this._allowRelativePaths
531 ? RETURN_FALSE
532 : throwError
533 )
534
535 return this._t(path, cache, checkUnignored, slices)
536 }
537
538 _t (path, cache, checkUnignored, slices) {
539 if (path in cache) {
540 return cache[path]
541 }
542
543 if (!slices) {
544 // path/to/a.js
545 // ['path', 'to', 'a.js']
546 slices = path.split(SLASH)
547 }
548
549 slices.pop()
550
551 // If the path has no parent directory, just test it
552 if (!slices.length) {
553 return cache[path] = this._testOne(path, checkUnignored)
554 }
555
556 const parent = this._t(
557 slices.join(SLASH) + SLASH,
558 cache,
559 checkUnignored,
560 slices
561 )
562
563 // If the path contains a parent directory, check the parent first
564 return cache[path] = parent.ignored
565 // > It is not possible to re-include a file if a parent directory of
566 // > that file is excluded.
567 ? parent
568 : this._testOne(path, checkUnignored)
569 }
570
571 ignores (path) {
572 return this._test(path, this._ignoreCache, false).ignored
573 }
574
575 createFilter () {
576 return path => !this.ignores(path)
577 }
578
579 filter (paths) {
580 return makeArray(paths).filter(this.createFilter())
581 }
582
583 // @returns {TestResult}
584 test (path) {
585 return this._test(path, this._testCache, true)
586 }
587}
588
589const factory = options => new Ignore(options)
590
591const isPathValid = path =>
592 checkPath(path && checkPath.convert(path), path, RETURN_FALSE)
593
594factory.isPathValid = isPathValid
595
596// Fixes typescript
597factory.default = factory
598
599module.exports = factory
600
601// Windows
602// --------------------------------------------------------------
603/* istanbul ignore if */
604if (
605 // Detect `process` so that it can run in browsers.
606 typeof process !== 'undefined'
607 && (
608 process.env && process.env.IGNORE_TEST_WIN32
609 || process.platform === 'win32'
610 )
611) {
612 /* eslint no-control-regex: "off" */
613 const makePosix = str => /^\\\\\?\\/.test(str)
614 || /["<>|\u0000-\u001F]+/u.test(str)
615 ? str
616 : str.replace(/\\/g, '/')
617
618 checkPath.convert = makePosix
619
620 // 'C:\\foo' <- 'C:\\foo' has been converted to 'C:/'
621 // 'd:\\foo'
622 const REGIX_IS_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i
623 checkPath.isNotRelative = path =>
624 REGIX_IS_WINDOWS_PATH_ABSOLUTE.test(path)
625 || isNotRelative(path)
626}
Note: See TracBrowser for help on using the repository browser.