1 // A simple implementation of make-array
2 function make_array (subject) {
3 return Array.isArray(subject)
8 const REGEX_BLANK_LINE = /^\s+$/
9 const REGEX_LEADING_EXCAPED_EXCLAMATION = /^\\!/
10 const REGEX_LEADING_EXCAPED_HASH = /^\\#/
12 const KEY_IGNORE = typeof Symbol !== 'undefined'
13 ? Symbol.for('node-ignore')
14 /* istanbul ignore next */
17 const define = (object, key, value) =>
18 Object.defineProperty(object, key, {value})
20 const REGEX_REGEXP_RANGE = /([0-z])-([0-z])/g
22 // Sanitize the range of a regular expression
23 // The cases are complicated, see test cases for details
24 const sanitizeRange = range => range.replace(
26 (match, from, to) => from.charCodeAt(0) <= to.charCodeAt(0)
28 // Invalid range (out of order) which is ok for gitignore rules but
29 // fatal for JavaScript regular expression, so eliminate it.
33 // > If the pattern ends with a slash,
34 // > it is removed for the purpose of the following description,
35 // > but it would only find a match with a directory.
36 // > In other words, foo/ will match a directory foo and paths underneath it,
37 // > but will not match a regular file or a symbolic link foo
38 // > (this is consistent with the way how pathspec works in general in Git).
39 // '`foo/`' will not match regular file '`foo`' or symbolic link '`foo`'
40 // -> ignore-rules will not deal with it, because it costs extra `fs.stat` call
41 // you could use option `mark: true` with `glob`
43 // '`foo/`' should not continue with the '`..`'
44 const DEFAULT_REPLACER_PREFIX = [
46 // > Trailing spaces are ignored unless they are quoted with backslash ("\")
52 match => match.indexOf('\\') === 0
57 // replace (\ ) with ' '
63 // Escape metacharacters
64 // which is written down by users but means special for regular expressions.
66 // > There are 12 characters with special meanings:
67 // > - the backslash \,
69 // > - the dollar sign $,
70 // > - the period or dot .,
71 // > - the vertical bar or pipe symbol |,
72 // > - the question mark ?,
73 // > - the asterisk or star *,
74 // > - the plus sign +,
75 // > - the opening parenthesis (,
76 // > - the closing parenthesis ),
77 // > - and the opening square bracket [,
78 // > - the opening curly brace {,
79 // > These special characters are often called "metacharacters".
86 // > [abc] matches any character inside the brackets
87 // > (in this case a, b, or c);
89 (match, p1, p2) => p2 === ']'
90 ? `[${sanitizeRange(p1)}]`
95 // > a question mark (?) matches a single character
103 // > A leading slash matches the beginning of the pathname.
104 // > For example, "/*.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c".
105 // A leading slash matches the beginning of the pathname
110 // replace special metacharacter slash after the leading slash
117 // > A leading "**" followed by a slash means match in all directories.
118 // > For example, "**/foo" matches file or directory "foo" anywhere,
119 // > the same as pattern "foo".
120 // > "**/foo/bar" matches file or directory "bar" anywhere that is directly
121 // > under directory "foo".
122 // Notice that the '*'s have been replaced as '\\*'
125 // '**/foo' <-> 'foo'
130 const DEFAULT_REPLACER_SUFFIX = [
133 // there will be no leading '/'
134 // (which has been replaced by section "leading slash")
135 // If starts with '**', adding a '^' to the regular expression also works
137 function startingReplacer () {
138 return !/\/(?!$)/.test(this)
139 // > If the pattern does not contain a slash /,
140 // > Git treats it as a shell glob pattern
141 // Actually, if there is only a trailing slash,
142 // git also treats it as a shell glob pattern
145 // > Otherwise, Git treats the pattern as a shell glob suitable for
146 // > consumption by fnmatch(3)
153 // Use lookahead assertions so that we could match more than one `'/**'`
154 /\\\/\\\*\\\*(?=\\\/|$)/g,
156 // Zero, one or several directories
157 // should not use '*', or it will be replaced by the next replacer
159 // Check if it is not the last `'/**'`
160 (match, index, str) => index + 6 < str.length
163 // > A slash followed by two consecutive asterisks then a slash matches
164 // > zero or more directories.
165 // > For example, "a/**/b" matches "a/b", "a/x/b", "a/x/y/b" and so on.
170 // > A trailing `"/**"` matches everything inside.
172 // #21: everything inside but it should not include the current folder
176 // intermediate wildcards
178 // Never replace escaped '*'
179 // ignore rule '\*' will match the path '*'
182 // 'abc.*' -> skip this rule
183 /(^|[^\\]+)\\\*(?=.+)/g,
185 // '*.js' matches '.js'
186 // '*.js' doesn't match 'abc'
187 (match, p1) => `${p1}[^\\/]*`
196 // '/*' does not match ''
197 // '/*' does not match everything
200 // 'abc/*' does not match 'abc/'
207 return `${prefix}(?=$|\\/$)`
218 const POSITIVE_REPLACERS = [
219 ...DEFAULT_REPLACER_PREFIX,
235 // 'js' will not match 'js.'
236 // 'ab' will not match 'abc'
239 // 'js*' will not match 'a.js'
240 // 'js/' will not match 'a.js'
241 // 'js' will match 'a.js' and 'a.js/'
242 match => `${match}(?=$|\\/)`
245 ...DEFAULT_REPLACER_SUFFIX
248 const NEGATIVE_REPLACERS = [
249 ...DEFAULT_REPLACER_PREFIX,
252 // The MISSING rule of [gitignore docs](https://git-scm.com/docs/gitignore)
253 // A negative pattern without a trailing wildcard should not
254 // re-include the things inside that directory.
257 // ['node_modules/*', '!node_modules']
258 // should ignore `node_modules/a.js`
261 match => `${match}(?=$|\\/$)`
264 ...DEFAULT_REPLACER_SUFFIX
267 // A simple cache, because an ignore rule only has only one certain meaning
268 const cache = Object.create(null)
271 const make_regex = (pattern, negative, ignorecase) => {
272 const r = cache[pattern]
277 const replacers = negative
281 const source = replacers.reduce(
282 (prev, current) => prev.replace(current[0], current[1].bind(pattern)),
286 return cache[pattern] = ignorecase
287 ? new RegExp(source, 'i')
291 // > A blank line matches no files, so it can serve as a separator for readability.
292 const checkPattern = pattern => pattern
293 && typeof pattern === 'string'
294 && !REGEX_BLANK_LINE.test(pattern)
296 // > A line starting with # serves as a comment.
297 && pattern.indexOf('#') !== 0
299 const createRule = (pattern, ignorecase) => {
300 const origin = pattern
303 // > An optional prefix "!" which negates the pattern;
304 if (pattern.indexOf('!') === 0) {
306 pattern = pattern.substr(1)
310 // > Put a backslash ("\") in front of the first "!" for patterns that
311 // > begin with a literal "!", for example, `"\!important!.txt"`.
312 .replace(REGEX_LEADING_EXCAPED_EXCLAMATION, '!')
313 // > Put a backslash ("\") in front of the first hash for patterns that
314 // > begin with a hash.
315 .replace(REGEX_LEADING_EXCAPED_HASH, '#')
317 const regex = make_regex(pattern, negative, ignorecase)
332 this._ignorecase = ignorecase
333 define(this, KEY_IGNORE, true)
338 this._cache = Object.create(null)
341 // @param {Array.<string>|string|Ignore} pattern
345 if (typeof pattern === 'string') {
346 pattern = pattern.split(/\r?\n/g)
349 make_array(pattern).forEach(this._addPattern, this)
351 // Some rules have just added to the ignore,
352 // making the behavior changed.
361 addPattern (pattern) {
362 return this.add(pattern)
365 _addPattern (pattern) {
367 if (pattern && pattern[KEY_IGNORE]) {
368 this._rules = this._rules.concat(pattern._rules)
373 if (checkPattern(pattern)) {
374 const rule = createRule(pattern, this._ignorecase)
376 this._rules.push(rule)
381 return make_array(paths).filter(path => this._filter(path))
385 return path => this._filter(path)
389 return !this._filter(path)
392 // @returns `Boolean` true if the `path` is NOT ignored
393 _filter (path, slices) {
398 if (path in this._cache) {
399 return this._cache[path]
404 // ['path', 'to', 'a.js']
405 slices = path.split(SLASH)
410 return this._cache[path] = slices.length
411 // > It is not possible to re-include a file if a parent directory of
412 // > that file is excluded.
413 // If the path contains a parent directory, check the parent first
414 ? this._filter(slices.join(SLASH) + SLASH, slices)
417 // Or only test the path
421 // @returns {Boolean} true if a file is NOT ignored
423 // Explicitly define variable type by setting matched to `0`
426 this._rules.forEach(rule => {
427 // if matched = true, then we only test negative rules
428 // if matched = false, then we test non-negative rules
429 if (!(matched ^ rule.negative)) {
430 matched = rule.negative ^ rule.regex.test(path)
439 // --------------------------------------------------------------
440 /* istanbul ignore if */
442 // Detect `process` so that it can run in browsers.
443 typeof process !== 'undefined'
445 process.env && process.env.IGNORE_TEST_WIN32
446 || process.platform === 'win32'
449 const filter = IgnoreBase.prototype._filter
451 /* eslint no-control-regex: "off" */
452 const make_posix = str => /^\\\\\?\\/.test(str)
453 || /[^\x00-\x80]+/.test(str)
455 : str.replace(/\\/g, '/')
457 IgnoreBase.prototype._filter = function filterWin32 (path, slices) {
458 path = make_posix(path)
459 return filter.call(this, path, slices)
463 module.exports = options => new IgnoreBase(options)