Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

463 linhas
11 KiB

4 anos atrás
  1. // A simple implementation of make-array
  2. function make_array (subject) {
  3. return Array.isArray(subject)
  4. ? subject
  5. : [subject]
  6. }
  7. const REGEX_BLANK_LINE = /^\s+$/
  8. const REGEX_LEADING_EXCAPED_EXCLAMATION = /^\\!/
  9. const REGEX_LEADING_EXCAPED_HASH = /^\\#/
  10. const SLASH = '/'
  11. const KEY_IGNORE = typeof Symbol !== 'undefined'
  12. ? Symbol.for('node-ignore')
  13. /* istanbul ignore next */
  14. : 'node-ignore'
  15. const define = (object, key, value) =>
  16. Object.defineProperty(object, key, {value})
  17. const REGEX_REGEXP_RANGE = /([0-z])-([0-z])/g
  18. // Sanitize the range of a regular expression
  19. // The cases are complicated, see test cases for details
  20. const sanitizeRange = range => range.replace(
  21. REGEX_REGEXP_RANGE,
  22. (match, from, to) => from.charCodeAt(0) <= to.charCodeAt(0)
  23. ? match
  24. // Invalid range (out of order) which is ok for gitignore rules but
  25. // fatal for JavaScript regular expression, so eliminate it.
  26. : ''
  27. )
  28. // > If the pattern ends with a slash,
  29. // > it is removed for the purpose of the following description,
  30. // > but it would only find a match with a directory.
  31. // > In other words, foo/ will match a directory foo and paths underneath it,
  32. // > but will not match a regular file or a symbolic link foo
  33. // > (this is consistent with the way how pathspec works in general in Git).
  34. // '`foo/`' will not match regular file '`foo`' or symbolic link '`foo`'
  35. // -> ignore-rules will not deal with it, because it costs extra `fs.stat` call
  36. // you could use option `mark: true` with `glob`
  37. // '`foo/`' should not continue with the '`..`'
  38. const DEFAULT_REPLACER_PREFIX = [
  39. // > Trailing spaces are ignored unless they are quoted with backslash ("\")
  40. [
  41. // (a\ ) -> (a )
  42. // (a ) -> (a)
  43. // (a \ ) -> (a )
  44. /\\?\s+$/,
  45. match => match.indexOf('\\') === 0
  46. ? ' '
  47. : ''
  48. ],
  49. // replace (\ ) with ' '
  50. [
  51. /\\\s/g,
  52. () => ' '
  53. ],
  54. // Escape metacharacters
  55. // which is written down by users but means special for regular expressions.
  56. // > There are 12 characters with special meanings:
  57. // > - the backslash \,
  58. // > - the caret ^,
  59. // > - the dollar sign $,
  60. // > - the period or dot .,
  61. // > - the vertical bar or pipe symbol |,
  62. // > - the question mark ?,
  63. // > - the asterisk or star *,
  64. // > - the plus sign +,
  65. // > - the opening parenthesis (,
  66. // > - the closing parenthesis ),
  67. // > - and the opening square bracket [,
  68. // > - the opening curly brace {,
  69. // > These special characters are often called "metacharacters".
  70. [
  71. /[\\^$.|*+(){]/g,
  72. match => `\\${match}`
  73. ],
  74. [
  75. // > [abc] matches any character inside the brackets
  76. // > (in this case a, b, or c);
  77. /\[([^\]/]*)($|\])/g,
  78. (match, p1, p2) => p2 === ']'
  79. ? `[${sanitizeRange(p1)}]`
  80. : `\\${match}`
  81. ],
  82. [
  83. // > a question mark (?) matches a single character
  84. /(?!\\)\?/g,
  85. () => '[^/]'
  86. ],
  87. // leading slash
  88. [
  89. // > A leading slash matches the beginning of the pathname.
  90. // > For example, "/*.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c".
  91. // A leading slash matches the beginning of the pathname
  92. /^\//,
  93. () => '^'
  94. ],
  95. // replace special metacharacter slash after the leading slash
  96. [
  97. /\//g,
  98. () => '\\/'
  99. ],
  100. [
  101. // > A leading "**" followed by a slash means match in all directories.
  102. // > For example, "**/foo" matches file or directory "foo" anywhere,
  103. // > the same as pattern "foo".
  104. // > "**/foo/bar" matches file or directory "bar" anywhere that is directly
  105. // > under directory "foo".
  106. // Notice that the '*'s have been replaced as '\\*'
  107. /^\^*\\\*\\\*\\\//,
  108. // '**/foo' <-> 'foo'
  109. () => '^(?:.*\\/)?'
  110. ]
  111. ]
  112. const DEFAULT_REPLACER_SUFFIX = [
  113. // starting
  114. [
  115. // there will be no leading '/'
  116. // (which has been replaced by section "leading slash")
  117. // If starts with '**', adding a '^' to the regular expression also works
  118. /^(?=[^^])/,
  119. function startingReplacer () {
  120. return !/\/(?!$)/.test(this)
  121. // > If the pattern does not contain a slash /,
  122. // > Git treats it as a shell glob pattern
  123. // Actually, if there is only a trailing slash,
  124. // git also treats it as a shell glob pattern
  125. ? '(?:^|\\/)'
  126. // > Otherwise, Git treats the pattern as a shell glob suitable for
  127. // > consumption by fnmatch(3)
  128. : '^'
  129. }
  130. ],
  131. // two globstars
  132. [
  133. // Use lookahead assertions so that we could match more than one `'/**'`
  134. /\\\/\\\*\\\*(?=\\\/|$)/g,
  135. // Zero, one or several directories
  136. // should not use '*', or it will be replaced by the next replacer
  137. // Check if it is not the last `'/**'`
  138. (match, index, str) => index + 6 < str.length
  139. // case: /**/
  140. // > A slash followed by two consecutive asterisks then a slash matches
  141. // > zero or more directories.
  142. // > For example, "a/**/b" matches "a/b", "a/x/b", "a/x/y/b" and so on.
  143. // '/**/'
  144. ? '(?:\\/[^\\/]+)*'
  145. // case: /**
  146. // > A trailing `"/**"` matches everything inside.
  147. // #21: everything inside but it should not include the current folder
  148. : '\\/.+'
  149. ],
  150. // intermediate wildcards
  151. [
  152. // Never replace escaped '*'
  153. // ignore rule '\*' will match the path '*'
  154. // 'abc.*/' -> go
  155. // 'abc.*' -> skip this rule
  156. /(^|[^\\]+)\\\*(?=.+)/g,
  157. // '*.js' matches '.js'
  158. // '*.js' doesn't match 'abc'
  159. (match, p1) => `${p1}[^\\/]*`
  160. ],
  161. // trailing wildcard
  162. [
  163. /(\^|\\\/)?\\\*$/,
  164. (match, p1) => {
  165. const prefix = p1
  166. // '\^':
  167. // '/*' does not match ''
  168. // '/*' does not match everything
  169. // '\\\/':
  170. // 'abc/*' does not match 'abc/'
  171. ? `${p1}[^/]+`
  172. // 'a*' matches 'a'
  173. // 'a*' matches 'aa'
  174. : '[^/]*'
  175. return `${prefix}(?=$|\\/$)`
  176. }
  177. ],
  178. [
  179. // unescape
  180. /\\\\\\/g,
  181. () => '\\'
  182. ]
  183. ]
  184. const POSITIVE_REPLACERS = [
  185. ...DEFAULT_REPLACER_PREFIX,
  186. // 'f'
  187. // matches
  188. // - /f(end)
  189. // - /f/
  190. // - (start)f(end)
  191. // - (start)f/
  192. // doesn't match
  193. // - oof
  194. // - foo
  195. // pseudo:
  196. // -> (^|/)f(/|$)
  197. // ending
  198. [
  199. // 'js' will not match 'js.'
  200. // 'ab' will not match 'abc'
  201. /(?:[^*/])$/,
  202. // 'js*' will not match 'a.js'
  203. // 'js/' will not match 'a.js'
  204. // 'js' will match 'a.js' and 'a.js/'
  205. match => `${match}(?=$|\\/)`
  206. ],
  207. ...DEFAULT_REPLACER_SUFFIX
  208. ]
  209. const NEGATIVE_REPLACERS = [
  210. ...DEFAULT_REPLACER_PREFIX,
  211. // #24, #38
  212. // The MISSING rule of [gitignore docs](https://git-scm.com/docs/gitignore)
  213. // A negative pattern without a trailing wildcard should not
  214. // re-include the things inside that directory.
  215. // eg:
  216. // ['node_modules/*', '!node_modules']
  217. // should ignore `node_modules/a.js`
  218. [
  219. /(?:[^*])$/,
  220. match => `${match}(?=$|\\/$)`
  221. ],
  222. ...DEFAULT_REPLACER_SUFFIX
  223. ]
  224. // A simple cache, because an ignore rule only has only one certain meaning
  225. const cache = Object.create(null)
  226. // @param {pattern}
  227. const make_regex = (pattern, negative, ignorecase) => {
  228. const r = cache[pattern]
  229. if (r) {
  230. return r
  231. }
  232. const replacers = negative
  233. ? NEGATIVE_REPLACERS
  234. : POSITIVE_REPLACERS
  235. const source = replacers.reduce(
  236. (prev, current) => prev.replace(current[0], current[1].bind(pattern)),
  237. pattern
  238. )
  239. return cache[pattern] = ignorecase
  240. ? new RegExp(source, 'i')
  241. : new RegExp(source)
  242. }
  243. // > A blank line matches no files, so it can serve as a separator for readability.
  244. const checkPattern = pattern => pattern
  245. && typeof pattern === 'string'
  246. && !REGEX_BLANK_LINE.test(pattern)
  247. // > A line starting with # serves as a comment.
  248. && pattern.indexOf('#') !== 0
  249. const createRule = (pattern, ignorecase) => {
  250. const origin = pattern
  251. let negative = false
  252. // > An optional prefix "!" which negates the pattern;
  253. if (pattern.indexOf('!') === 0) {
  254. negative = true
  255. pattern = pattern.substr(1)
  256. }
  257. pattern = pattern
  258. // > Put a backslash ("\") in front of the first "!" for patterns that
  259. // > begin with a literal "!", for example, `"\!important!.txt"`.
  260. .replace(REGEX_LEADING_EXCAPED_EXCLAMATION, '!')
  261. // > Put a backslash ("\") in front of the first hash for patterns that
  262. // > begin with a hash.
  263. .replace(REGEX_LEADING_EXCAPED_HASH, '#')
  264. const regex = make_regex(pattern, negative, ignorecase)
  265. return {
  266. origin,
  267. pattern,
  268. negative,
  269. regex
  270. }
  271. }
  272. class IgnoreBase {
  273. constructor ({
  274. ignorecase = true
  275. } = {}) {
  276. this._rules = []
  277. this._ignorecase = ignorecase
  278. define(this, KEY_IGNORE, true)
  279. this._initCache()
  280. }
  281. _initCache () {
  282. this._cache = Object.create(null)
  283. }
  284. // @param {Array.<string>|string|Ignore} pattern
  285. add (pattern) {
  286. this._added = false
  287. if (typeof pattern === 'string') {
  288. pattern = pattern.split(/\r?\n/g)
  289. }
  290. make_array(pattern).forEach(this._addPattern, this)
  291. // Some rules have just added to the ignore,
  292. // making the behavior changed.
  293. if (this._added) {
  294. this._initCache()
  295. }
  296. return this
  297. }
  298. // legacy
  299. addPattern (pattern) {
  300. return this.add(pattern)
  301. }
  302. _addPattern (pattern) {
  303. // #32
  304. if (pattern && pattern[KEY_IGNORE]) {
  305. this._rules = this._rules.concat(pattern._rules)
  306. this._added = true
  307. return
  308. }
  309. if (checkPattern(pattern)) {
  310. const rule = createRule(pattern, this._ignorecase)
  311. this._added = true
  312. this._rules.push(rule)
  313. }
  314. }
  315. filter (paths) {
  316. return make_array(paths).filter(path => this._filter(path))
  317. }
  318. createFilter () {
  319. return path => this._filter(path)
  320. }
  321. ignores (path) {
  322. return !this._filter(path)
  323. }
  324. // @returns `Boolean` true if the `path` is NOT ignored
  325. _filter (path, slices) {
  326. if (!path) {
  327. return false
  328. }
  329. if (path in this._cache) {
  330. return this._cache[path]
  331. }
  332. if (!slices) {
  333. // path/to/a.js
  334. // ['path', 'to', 'a.js']
  335. slices = path.split(SLASH)
  336. }
  337. slices.pop()
  338. return this._cache[path] = slices.length
  339. // > It is not possible to re-include a file if a parent directory of
  340. // > that file is excluded.
  341. // If the path contains a parent directory, check the parent first
  342. ? this._filter(slices.join(SLASH) + SLASH, slices)
  343. && this._test(path)
  344. // Or only test the path
  345. : this._test(path)
  346. }
  347. // @returns {Boolean} true if a file is NOT ignored
  348. _test (path) {
  349. // Explicitly define variable type by setting matched to `0`
  350. let matched = 0
  351. this._rules.forEach(rule => {
  352. // if matched = true, then we only test negative rules
  353. // if matched = false, then we test non-negative rules
  354. if (!(matched ^ rule.negative)) {
  355. matched = rule.negative ^ rule.regex.test(path)
  356. }
  357. })
  358. return !matched
  359. }
  360. }
  361. // Windows
  362. // --------------------------------------------------------------
  363. /* istanbul ignore if */
  364. if (
  365. // Detect `process` so that it can run in browsers.
  366. typeof process !== 'undefined'
  367. && (
  368. process.env && process.env.IGNORE_TEST_WIN32
  369. || process.platform === 'win32'
  370. )
  371. ) {
  372. const filter = IgnoreBase.prototype._filter
  373. /* eslint no-control-regex: "off" */
  374. const make_posix = str => /^\\\\\?\\/.test(str)
  375. || /[^\x00-\x80]+/.test(str)
  376. ? str
  377. : str.replace(/\\/g, '/')
  378. IgnoreBase.prototype._filter = function filterWin32 (path, slices) {
  379. path = make_posix(path)
  380. return filter.call(this, path, slices)
  381. }
  382. }
  383. module.exports = options => new IgnoreBase(options)