UNPKG

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