You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					1092 lines
				
				27 KiB
			
		
		
			
		
	
	
					1092 lines
				
				27 KiB
			| 
											3 years ago
										 | 'use strict'; | ||
|  | 
 | ||
|  | const constants = require('./constants'); | ||
|  | const utils = require('./utils'); | ||
|  | 
 | ||
|  | /** | ||
|  |  * Constants | ||
|  |  */ | ||
|  | 
 | ||
|  | const { | ||
|  |   MAX_LENGTH, | ||
|  |   POSIX_REGEX_SOURCE, | ||
|  |   REGEX_NON_SPECIAL_CHARS, | ||
|  |   REGEX_SPECIAL_CHARS_BACKREF, | ||
|  |   REPLACEMENTS | ||
|  | } = constants; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Helpers | ||
|  |  */ | ||
|  | 
 | ||
|  | const expandRange = (args, options) => { | ||
|  |   if (typeof options.expandRange === 'function') { | ||
|  |     return options.expandRange(...args, options); | ||
|  |   } | ||
|  | 
 | ||
|  |   args.sort(); | ||
|  |   const value = `[${args.join('-')}]`; | ||
|  | 
 | ||
|  |   try { | ||
|  |     /* eslint-disable-next-line no-new */ | ||
|  |     new RegExp(value); | ||
|  |   } catch (ex) { | ||
|  |     return args.map(v => utils.escapeRegex(v)).join('..'); | ||
|  |   } | ||
|  | 
 | ||
|  |   return value; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Create the message for a syntax error | ||
|  |  */ | ||
|  | 
 | ||
|  | const syntaxError = (type, char) => { | ||
|  |   return `Missing ${type}: "${char}" - use "\\\\${char}" to match literal characters`; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Parse the given input string. | ||
|  |  * @param {String} input | ||
|  |  * @param {Object} options | ||
|  |  * @return {Object} | ||
|  |  */ | ||
|  | 
 | ||
|  | const parse = (input, options) => { | ||
|  |   if (typeof input !== 'string') { | ||
|  |     throw new TypeError('Expected a string'); | ||
|  |   } | ||
|  | 
 | ||
|  |   input = REPLACEMENTS[input] || input; | ||
|  | 
 | ||
|  |   const opts = { ...options }; | ||
|  |   const max = typeof opts.maxLength === 'number' ? Math.min(MAX_LENGTH, opts.maxLength) : MAX_LENGTH; | ||
|  | 
 | ||
|  |   let len = input.length; | ||
|  |   if (len > max) { | ||
|  |     throw new SyntaxError(`Input length: ${len}, exceeds maximum allowed length: ${max}`); | ||
|  |   } | ||
|  | 
 | ||
|  |   const bos = { type: 'bos', value: '', output: opts.prepend || '' }; | ||
|  |   const tokens = [bos]; | ||
|  | 
 | ||
|  |   const capture = opts.capture ? '' : '?:'; | ||
|  |   const win32 = utils.isWindows(options); | ||
|  | 
 | ||
|  |   // create constants based on platform, for windows or posix
 | ||
|  |   const PLATFORM_CHARS = constants.globChars(win32); | ||
|  |   const EXTGLOB_CHARS = constants.extglobChars(PLATFORM_CHARS); | ||
|  | 
 | ||
|  |   const { | ||
|  |     DOT_LITERAL, | ||
|  |     PLUS_LITERAL, | ||
|  |     SLASH_LITERAL, | ||
|  |     ONE_CHAR, | ||
|  |     DOTS_SLASH, | ||
|  |     NO_DOT, | ||
|  |     NO_DOT_SLASH, | ||
|  |     NO_DOTS_SLASH, | ||
|  |     QMARK, | ||
|  |     QMARK_NO_DOT, | ||
|  |     STAR, | ||
|  |     START_ANCHOR | ||
|  |   } = PLATFORM_CHARS; | ||
|  | 
 | ||
|  |   const globstar = opts => { | ||
|  |     return `(${capture}(?:(?!${START_ANCHOR}${opts.dot ? DOTS_SLASH : DOT_LITERAL}).)*?)`; | ||
|  |   }; | ||
|  | 
 | ||
|  |   const nodot = opts.dot ? '' : NO_DOT; | ||
|  |   const qmarkNoDot = opts.dot ? QMARK : QMARK_NO_DOT; | ||
|  |   let star = opts.bash === true ? globstar(opts) : STAR; | ||
|  | 
 | ||
|  |   if (opts.capture) { | ||
|  |     star = `(${star})`; | ||
|  |   } | ||
|  | 
 | ||
|  |   // minimatch options support
 | ||
|  |   if (typeof opts.noext === 'boolean') { | ||
|  |     opts.noextglob = opts.noext; | ||
|  |   } | ||
|  | 
 | ||
|  |   const state = { | ||
|  |     input, | ||
|  |     index: -1, | ||
|  |     start: 0, | ||
|  |     dot: opts.dot === true, | ||
|  |     consumed: '', | ||
|  |     output: '', | ||
|  |     prefix: '', | ||
|  |     backtrack: false, | ||
|  |     negated: false, | ||
|  |     brackets: 0, | ||
|  |     braces: 0, | ||
|  |     parens: 0, | ||
|  |     quotes: 0, | ||
|  |     globstar: false, | ||
|  |     tokens | ||
|  |   }; | ||
|  | 
 | ||
|  |   input = utils.removePrefix(input, state); | ||
|  |   len = input.length; | ||
|  | 
 | ||
|  |   const extglobs = []; | ||
|  |   const braces = []; | ||
|  |   const stack = []; | ||
|  |   let prev = bos; | ||
|  |   let value; | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Tokenizing helpers | ||
|  |    */ | ||
|  | 
 | ||
|  |   const eos = () => state.index === len - 1; | ||
|  |   const peek = state.peek = (n = 1) => input[state.index + n]; | ||
|  |   const advance = state.advance = () => input[++state.index] || ''; | ||
|  |   const remaining = () => input.slice(state.index + 1); | ||
|  |   const consume = (value = '', num = 0) => { | ||
|  |     state.consumed += value; | ||
|  |     state.index += num; | ||
|  |   }; | ||
|  | 
 | ||
|  |   const append = token => { | ||
|  |     state.output += token.output != null ? token.output : token.value; | ||
|  |     consume(token.value); | ||
|  |   }; | ||
|  | 
 | ||
|  |   const negate = () => { | ||
|  |     let count = 1; | ||
|  | 
 | ||
|  |     while (peek() === '!' && (peek(2) !== '(' || peek(3) === '?')) { | ||
|  |       advance(); | ||
|  |       state.start++; | ||
|  |       count++; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (count % 2 === 0) { | ||
|  |       return false; | ||
|  |     } | ||
|  | 
 | ||
|  |     state.negated = true; | ||
|  |     state.start++; | ||
|  |     return true; | ||
|  |   }; | ||
|  | 
 | ||
|  |   const increment = type => { | ||
|  |     state[type]++; | ||
|  |     stack.push(type); | ||
|  |   }; | ||
|  | 
 | ||
|  |   const decrement = type => { | ||
|  |     state[type]--; | ||
|  |     stack.pop(); | ||
|  |   }; | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Push tokens onto the tokens array. This helper speeds up | ||
|  |    * tokenizing by 1) helping us avoid backtracking as much as possible, | ||
|  |    * and 2) helping us avoid creating extra tokens when consecutive | ||
|  |    * characters are plain text. This improves performance and simplifies | ||
|  |    * lookbehinds. | ||
|  |    */ | ||
|  | 
 | ||
|  |   const push = tok => { | ||
|  |     if (prev.type === 'globstar') { | ||
|  |       const isBrace = state.braces > 0 && (tok.type === 'comma' || tok.type === 'brace'); | ||
|  |       const isExtglob = tok.extglob === true || (extglobs.length && (tok.type === 'pipe' || tok.type === 'paren')); | ||
|  | 
 | ||
|  |       if (tok.type !== 'slash' && tok.type !== 'paren' && !isBrace && !isExtglob) { | ||
|  |         state.output = state.output.slice(0, -prev.output.length); | ||
|  |         prev.type = 'star'; | ||
|  |         prev.value = '*'; | ||
|  |         prev.output = star; | ||
|  |         state.output += prev.output; | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     if (extglobs.length && tok.type !== 'paren') { | ||
|  |       extglobs[extglobs.length - 1].inner += tok.value; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (tok.value || tok.output) append(tok); | ||
|  |     if (prev && prev.type === 'text' && tok.type === 'text') { | ||
|  |       prev.value += tok.value; | ||
|  |       prev.output = (prev.output || '') + tok.value; | ||
|  |       return; | ||
|  |     } | ||
|  | 
 | ||
|  |     tok.prev = prev; | ||
|  |     tokens.push(tok); | ||
|  |     prev = tok; | ||
|  |   }; | ||
|  | 
 | ||
|  |   const extglobOpen = (type, value) => { | ||
|  |     const token = { ...EXTGLOB_CHARS[value], conditions: 1, inner: '' }; | ||
|  | 
 | ||
|  |     token.prev = prev; | ||
|  |     token.parens = state.parens; | ||
|  |     token.output = state.output; | ||
|  |     const output = (opts.capture ? '(' : '') + token.open; | ||
|  | 
 | ||
|  |     increment('parens'); | ||
|  |     push({ type, value, output: state.output ? '' : ONE_CHAR }); | ||
|  |     push({ type: 'paren', extglob: true, value: advance(), output }); | ||
|  |     extglobs.push(token); | ||
|  |   }; | ||
|  | 
 | ||
|  |   const extglobClose = token => { | ||
|  |     let output = token.close + (opts.capture ? ')' : ''); | ||
|  |     let rest; | ||
|  | 
 | ||
|  |     if (token.type === 'negate') { | ||
|  |       let extglobStar = star; | ||
|  | 
 | ||
|  |       if (token.inner && token.inner.length > 1 && token.inner.includes('/')) { | ||
|  |         extglobStar = globstar(opts); | ||
|  |       } | ||
|  | 
 | ||
|  |       if (extglobStar !== star || eos() || /^\)+$/.test(remaining())) { | ||
|  |         output = token.close = `)$))${extglobStar}`; | ||
|  |       } | ||
|  | 
 | ||
|  |       if (token.inner.includes('*') && (rest = remaining()) && /^\.[^\\/.]+$/.test(rest)) { | ||
|  |         // Any non-magical string (`.ts`) or even nested expression (`.{ts,tsx}`) can follow after the closing parenthesis.
 | ||
|  |         // In this case, we need to parse the string and use it in the output of the original pattern.
 | ||
|  |         // Suitable patterns: `/!(*.d).ts`, `/!(*.d).{ts,tsx}`, `**/!(*-dbg).@(js)`.
 | ||
|  |         //
 | ||
|  |         // Disabling the `fastpaths` option due to a problem with parsing strings as `.ts` in the pattern like `**/!(*.d).ts`.
 | ||
|  |         const expression = parse(rest, { ...options, fastpaths: false }).output; | ||
|  | 
 | ||
|  |         output = token.close = `)${expression})${extglobStar})`; | ||
|  |       } | ||
|  | 
 | ||
|  |       if (token.prev.type === 'bos') { | ||
|  |         state.negatedExtglob = true; | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     push({ type: 'paren', extglob: true, value, output }); | ||
|  |     decrement('parens'); | ||
|  |   }; | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Fast paths | ||
|  |    */ | ||
|  | 
 | ||
|  |   if (opts.fastpaths !== false && !/(^[*!]|[/()[\]{}"])/.test(input)) { | ||
|  |     let backslashes = false; | ||
|  | 
 | ||
|  |     let output = input.replace(REGEX_SPECIAL_CHARS_BACKREF, (m, esc, chars, first, rest, index) => { | ||
|  |       if (first === '\\') { | ||
|  |         backslashes = true; | ||
|  |         return m; | ||
|  |       } | ||
|  | 
 | ||
|  |       if (first === '?') { | ||
|  |         if (esc) { | ||
|  |           return esc + first + (rest ? QMARK.repeat(rest.length) : ''); | ||
|  |         } | ||
|  |         if (index === 0) { | ||
|  |           return qmarkNoDot + (rest ? QMARK.repeat(rest.length) : ''); | ||
|  |         } | ||
|  |         return QMARK.repeat(chars.length); | ||
|  |       } | ||
|  | 
 | ||
|  |       if (first === '.') { | ||
|  |         return DOT_LITERAL.repeat(chars.length); | ||
|  |       } | ||
|  | 
 | ||
|  |       if (first === '*') { | ||
|  |         if (esc) { | ||
|  |           return esc + first + (rest ? star : ''); | ||
|  |         } | ||
|  |         return star; | ||
|  |       } | ||
|  |       return esc ? m : `\\${m}`; | ||
|  |     }); | ||
|  | 
 | ||
|  |     if (backslashes === true) { | ||
|  |       if (opts.unescape === true) { | ||
|  |         output = output.replace(/\\/g, ''); | ||
|  |       } else { | ||
|  |         output = output.replace(/\\+/g, m => { | ||
|  |           return m.length % 2 === 0 ? '\\\\' : (m ? '\\' : ''); | ||
|  |         }); | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     if (output === input && opts.contains === true) { | ||
|  |       state.output = input; | ||
|  |       return state; | ||
|  |     } | ||
|  | 
 | ||
|  |     state.output = utils.wrapOutput(output, state, options); | ||
|  |     return state; | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Tokenize input until we reach end-of-string | ||
|  |    */ | ||
|  | 
 | ||
|  |   while (!eos()) { | ||
|  |     value = advance(); | ||
|  | 
 | ||
|  |     if (value === '\u0000') { | ||
|  |       continue; | ||
|  |     } | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Escaped characters | ||
|  |      */ | ||
|  | 
 | ||
|  |     if (value === '\\') { | ||
|  |       const next = peek(); | ||
|  | 
 | ||
|  |       if (next === '/' && opts.bash !== true) { | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       if (next === '.' || next === ';') { | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       if (!next) { | ||
|  |         value += '\\'; | ||
|  |         push({ type: 'text', value }); | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       // collapse slashes to reduce potential for exploits
 | ||
|  |       const match = /^\\+/.exec(remaining()); | ||
|  |       let slashes = 0; | ||
|  | 
 | ||
|  |       if (match && match[0].length > 2) { | ||
|  |         slashes = match[0].length; | ||
|  |         state.index += slashes; | ||
|  |         if (slashes % 2 !== 0) { | ||
|  |           value += '\\'; | ||
|  |         } | ||
|  |       } | ||
|  | 
 | ||
|  |       if (opts.unescape === true) { | ||
|  |         value = advance(); | ||
|  |       } else { | ||
|  |         value += advance(); | ||
|  |       } | ||
|  | 
 | ||
|  |       if (state.brackets === 0) { | ||
|  |         push({ type: 'text', value }); | ||
|  |         continue; | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     /** | ||
|  |      * If we're inside a regex character class, continue | ||
|  |      * until we reach the closing bracket. | ||
|  |      */ | ||
|  | 
 | ||
|  |     if (state.brackets > 0 && (value !== ']' || prev.value === '[' || prev.value === '[^')) { | ||
|  |       if (opts.posix !== false && value === ':') { | ||
|  |         const inner = prev.value.slice(1); | ||
|  |         if (inner.includes('[')) { | ||
|  |           prev.posix = true; | ||
|  | 
 | ||
|  |           if (inner.includes(':')) { | ||
|  |             const idx = prev.value.lastIndexOf('['); | ||
|  |             const pre = prev.value.slice(0, idx); | ||
|  |             const rest = prev.value.slice(idx + 2); | ||
|  |             const posix = POSIX_REGEX_SOURCE[rest]; | ||
|  |             if (posix) { | ||
|  |               prev.value = pre + posix; | ||
|  |               state.backtrack = true; | ||
|  |               advance(); | ||
|  | 
 | ||
|  |               if (!bos.output && tokens.indexOf(prev) === 1) { | ||
|  |                 bos.output = ONE_CHAR; | ||
|  |               } | ||
|  |               continue; | ||
|  |             } | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  | 
 | ||
|  |       if ((value === '[' && peek() !== ':') || (value === '-' && peek() === ']')) { | ||
|  |         value = `\\${value}`; | ||
|  |       } | ||
|  | 
 | ||
|  |       if (value === ']' && (prev.value === '[' || prev.value === '[^')) { | ||
|  |         value = `\\${value}`; | ||
|  |       } | ||
|  | 
 | ||
|  |       if (opts.posix === true && value === '!' && prev.value === '[') { | ||
|  |         value = '^'; | ||
|  |       } | ||
|  | 
 | ||
|  |       prev.value += value; | ||
|  |       append({ value }); | ||
|  |       continue; | ||
|  |     } | ||
|  | 
 | ||
|  |     /** | ||
|  |      * If we're inside a quoted string, continue | ||
|  |      * until we reach the closing double quote. | ||
|  |      */ | ||
|  | 
 | ||
|  |     if (state.quotes === 1 && value !== '"') { | ||
|  |       value = utils.escapeRegex(value); | ||
|  |       prev.value += value; | ||
|  |       append({ value }); | ||
|  |       continue; | ||
|  |     } | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Double quotes | ||
|  |      */ | ||
|  | 
 | ||
|  |     if (value === '"') { | ||
|  |       state.quotes = state.quotes === 1 ? 0 : 1; | ||
|  |       if (opts.keepQuotes === true) { | ||
|  |         push({ type: 'text', value }); | ||
|  |       } | ||
|  |       continue; | ||
|  |     } | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Parentheses | ||
|  |      */ | ||
|  | 
 | ||
|  |     if (value === '(') { | ||
|  |       increment('parens'); | ||
|  |       push({ type: 'paren', value }); | ||
|  |       continue; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (value === ')') { | ||
|  |       if (state.parens === 0 && opts.strictBrackets === true) { | ||
|  |         throw new SyntaxError(syntaxError('opening', '(')); | ||
|  |       } | ||
|  | 
 | ||
|  |       const extglob = extglobs[extglobs.length - 1]; | ||
|  |       if (extglob && state.parens === extglob.parens + 1) { | ||
|  |         extglobClose(extglobs.pop()); | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       push({ type: 'paren', value, output: state.parens ? ')' : '\\)' }); | ||
|  |       decrement('parens'); | ||
|  |       continue; | ||
|  |     } | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Square brackets | ||
|  |      */ | ||
|  | 
 | ||
|  |     if (value === '[') { | ||
|  |       if (opts.nobracket === true || !remaining().includes(']')) { | ||
|  |         if (opts.nobracket !== true && opts.strictBrackets === true) { | ||
|  |           throw new SyntaxError(syntaxError('closing', ']')); | ||
|  |         } | ||
|  | 
 | ||
|  |         value = `\\${value}`; | ||
|  |       } else { | ||
|  |         increment('brackets'); | ||
|  |       } | ||
|  | 
 | ||
|  |       push({ type: 'bracket', value }); | ||
|  |       continue; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (value === ']') { | ||
|  |       if (opts.nobracket === true || (prev && prev.type === 'bracket' && prev.value.length === 1)) { | ||
|  |         push({ type: 'text', value, output: `\\${value}` }); | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       if (state.brackets === 0) { | ||
|  |         if (opts.strictBrackets === true) { | ||
|  |           throw new SyntaxError(syntaxError('opening', '[')); | ||
|  |         } | ||
|  | 
 | ||
|  |         push({ type: 'text', value, output: `\\${value}` }); | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       decrement('brackets'); | ||
|  | 
 | ||
|  |       const prevValue = prev.value.slice(1); | ||
|  |       if (prev.posix !== true && prevValue[0] === '^' && !prevValue.includes('/')) { | ||
|  |         value = `/${value}`; | ||
|  |       } | ||
|  | 
 | ||
|  |       prev.value += value; | ||
|  |       append({ value }); | ||
|  | 
 | ||
|  |       // when literal brackets are explicitly disabled
 | ||
|  |       // assume we should match with a regex character class
 | ||
|  |       if (opts.literalBrackets === false || utils.hasRegexChars(prevValue)) { | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       const escaped = utils.escapeRegex(prev.value); | ||
|  |       state.output = state.output.slice(0, -prev.value.length); | ||
|  | 
 | ||
|  |       // when literal brackets are explicitly enabled
 | ||
|  |       // assume we should escape the brackets to match literal characters
 | ||
|  |       if (opts.literalBrackets === true) { | ||
|  |         state.output += escaped; | ||
|  |         prev.value = escaped; | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       // when the user specifies nothing, try to match both
 | ||
|  |       prev.value = `(${capture}${escaped}|${prev.value})`; | ||
|  |       state.output += prev.value; | ||
|  |       continue; | ||
|  |     } | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Braces | ||
|  |      */ | ||
|  | 
 | ||
|  |     if (value === '{' && opts.nobrace !== true) { | ||
|  |       increment('braces'); | ||
|  | 
 | ||
|  |       const open = { | ||
|  |         type: 'brace', | ||
|  |         value, | ||
|  |         output: '(', | ||
|  |         outputIndex: state.output.length, | ||
|  |         tokensIndex: state.tokens.length | ||
|  |       }; | ||
|  | 
 | ||
|  |       braces.push(open); | ||
|  |       push(open); | ||
|  |       continue; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (value === '}') { | ||
|  |       const brace = braces[braces.length - 1]; | ||
|  | 
 | ||
|  |       if (opts.nobrace === true || !brace) { | ||
|  |         push({ type: 'text', value, output: value }); | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       let output = ')'; | ||
|  | 
 | ||
|  |       if (brace.dots === true) { | ||
|  |         const arr = tokens.slice(); | ||
|  |         const range = []; | ||
|  | 
 | ||
|  |         for (let i = arr.length - 1; i >= 0; i--) { | ||
|  |           tokens.pop(); | ||
|  |           if (arr[i].type === 'brace') { | ||
|  |             break; | ||
|  |           } | ||
|  |           if (arr[i].type !== 'dots') { | ||
|  |             range.unshift(arr[i].value); | ||
|  |           } | ||
|  |         } | ||
|  | 
 | ||
|  |         output = expandRange(range, opts); | ||
|  |         state.backtrack = true; | ||
|  |       } | ||
|  | 
 | ||
|  |       if (brace.comma !== true && brace.dots !== true) { | ||
|  |         const out = state.output.slice(0, brace.outputIndex); | ||
|  |         const toks = state.tokens.slice(brace.tokensIndex); | ||
|  |         brace.value = brace.output = '\\{'; | ||
|  |         value = output = '\\}'; | ||
|  |         state.output = out; | ||
|  |         for (const t of toks) { | ||
|  |           state.output += (t.output || t.value); | ||
|  |         } | ||
|  |       } | ||
|  | 
 | ||
|  |       push({ type: 'brace', value, output }); | ||
|  |       decrement('braces'); | ||
|  |       braces.pop(); | ||
|  |       continue; | ||
|  |     } | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Pipes | ||
|  |      */ | ||
|  | 
 | ||
|  |     if (value === '|') { | ||
|  |       if (extglobs.length > 0) { | ||
|  |         extglobs[extglobs.length - 1].conditions++; | ||
|  |       } | ||
|  |       push({ type: 'text', value }); | ||
|  |       continue; | ||
|  |     } | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Commas | ||
|  |      */ | ||
|  | 
 | ||
|  |     if (value === ',') { | ||
|  |       let output = value; | ||
|  | 
 | ||
|  |       const brace = braces[braces.length - 1]; | ||
|  |       if (brace && stack[stack.length - 1] === 'braces') { | ||
|  |         brace.comma = true; | ||
|  |         output = '|'; | ||
|  |       } | ||
|  | 
 | ||
|  |       push({ type: 'comma', value, output }); | ||
|  |       continue; | ||
|  |     } | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Slashes | ||
|  |      */ | ||
|  | 
 | ||
|  |     if (value === '/') { | ||
|  |       // if the beginning of the glob is "./", advance the start
 | ||
|  |       // to the current index, and don't add the "./" characters
 | ||
|  |       // to the state. This greatly simplifies lookbehinds when
 | ||
|  |       // checking for BOS characters like "!" and "." (not "./")
 | ||
|  |       if (prev.type === 'dot' && state.index === state.start + 1) { | ||
|  |         state.start = state.index + 1; | ||
|  |         state.consumed = ''; | ||
|  |         state.output = ''; | ||
|  |         tokens.pop(); | ||
|  |         prev = bos; // reset "prev" to the first token
 | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       push({ type: 'slash', value, output: SLASH_LITERAL }); | ||
|  |       continue; | ||
|  |     } | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Dots | ||
|  |      */ | ||
|  | 
 | ||
|  |     if (value === '.') { | ||
|  |       if (state.braces > 0 && prev.type === 'dot') { | ||
|  |         if (prev.value === '.') prev.output = DOT_LITERAL; | ||
|  |         const brace = braces[braces.length - 1]; | ||
|  |         prev.type = 'dots'; | ||
|  |         prev.output += value; | ||
|  |         prev.value += value; | ||
|  |         brace.dots = true; | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       if ((state.braces + state.parens) === 0 && prev.type !== 'bos' && prev.type !== 'slash') { | ||
|  |         push({ type: 'text', value, output: DOT_LITERAL }); | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       push({ type: 'dot', value, output: DOT_LITERAL }); | ||
|  |       continue; | ||
|  |     } | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Question marks | ||
|  |      */ | ||
|  | 
 | ||
|  |     if (value === '?') { | ||
|  |       const isGroup = prev && prev.value === '('; | ||
|  |       if (!isGroup && opts.noextglob !== true && peek() === '(' && peek(2) !== '?') { | ||
|  |         extglobOpen('qmark', value); | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       if (prev && prev.type === 'paren') { | ||
|  |         const next = peek(); | ||
|  |         let output = value; | ||
|  | 
 | ||
|  |         if (next === '<' && !utils.supportsLookbehinds()) { | ||
|  |           throw new Error('Node.js v10 or higher is required for regex lookbehinds'); | ||
|  |         } | ||
|  | 
 | ||
|  |         if ((prev.value === '(' && !/[!=<:]/.test(next)) || (next === '<' && !/<([!=]|\w+>)/.test(remaining()))) { | ||
|  |           output = `\\${value}`; | ||
|  |         } | ||
|  | 
 | ||
|  |         push({ type: 'text', value, output }); | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       if (opts.dot !== true && (prev.type === 'slash' || prev.type === 'bos')) { | ||
|  |         push({ type: 'qmark', value, output: QMARK_NO_DOT }); | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       push({ type: 'qmark', value, output: QMARK }); | ||
|  |       continue; | ||
|  |     } | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Exclamation | ||
|  |      */ | ||
|  | 
 | ||
|  |     if (value === '!') { | ||
|  |       if (opts.noextglob !== true && peek() === '(') { | ||
|  |         if (peek(2) !== '?' || !/[!=<:]/.test(peek(3))) { | ||
|  |           extglobOpen('negate', value); | ||
|  |           continue; | ||
|  |         } | ||
|  |       } | ||
|  | 
 | ||
|  |       if (opts.nonegate !== true && state.index === 0) { | ||
|  |         negate(); | ||
|  |         continue; | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Plus | ||
|  |      */ | ||
|  | 
 | ||
|  |     if (value === '+') { | ||
|  |       if (opts.noextglob !== true && peek() === '(' && peek(2) !== '?') { | ||
|  |         extglobOpen('plus', value); | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       if ((prev && prev.value === '(') || opts.regex === false) { | ||
|  |         push({ type: 'plus', value, output: PLUS_LITERAL }); | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       if ((prev && (prev.type === 'bracket' || prev.type === 'paren' || prev.type === 'brace')) || state.parens > 0) { | ||
|  |         push({ type: 'plus', value }); | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       push({ type: 'plus', value: PLUS_LITERAL }); | ||
|  |       continue; | ||
|  |     } | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Plain text | ||
|  |      */ | ||
|  | 
 | ||
|  |     if (value === '@') { | ||
|  |       if (opts.noextglob !== true && peek() === '(' && peek(2) !== '?') { | ||
|  |         push({ type: 'at', extglob: true, value, output: '' }); | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       push({ type: 'text', value }); | ||
|  |       continue; | ||
|  |     } | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Plain text | ||
|  |      */ | ||
|  | 
 | ||
|  |     if (value !== '*') { | ||
|  |       if (value === '$' || value === '^') { | ||
|  |         value = `\\${value}`; | ||
|  |       } | ||
|  | 
 | ||
|  |       const match = REGEX_NON_SPECIAL_CHARS.exec(remaining()); | ||
|  |       if (match) { | ||
|  |         value += match[0]; | ||
|  |         state.index += match[0].length; | ||
|  |       } | ||
|  | 
 | ||
|  |       push({ type: 'text', value }); | ||
|  |       continue; | ||
|  |     } | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Stars | ||
|  |      */ | ||
|  | 
 | ||
|  |     if (prev && (prev.type === 'globstar' || prev.star === true)) { | ||
|  |       prev.type = 'star'; | ||
|  |       prev.star = true; | ||
|  |       prev.value += value; | ||
|  |       prev.output = star; | ||
|  |       state.backtrack = true; | ||
|  |       state.globstar = true; | ||
|  |       consume(value); | ||
|  |       continue; | ||
|  |     } | ||
|  | 
 | ||
|  |     let rest = remaining(); | ||
|  |     if (opts.noextglob !== true && /^\([^?]/.test(rest)) { | ||
|  |       extglobOpen('star', value); | ||
|  |       continue; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (prev.type === 'star') { | ||
|  |       if (opts.noglobstar === true) { | ||
|  |         consume(value); | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       const prior = prev.prev; | ||
|  |       const before = prior.prev; | ||
|  |       const isStart = prior.type === 'slash' || prior.type === 'bos'; | ||
|  |       const afterStar = before && (before.type === 'star' || before.type === 'globstar'); | ||
|  | 
 | ||
|  |       if (opts.bash === true && (!isStart || (rest[0] && rest[0] !== '/'))) { | ||
|  |         push({ type: 'star', value, output: '' }); | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       const isBrace = state.braces > 0 && (prior.type === 'comma' || prior.type === 'brace'); | ||
|  |       const isExtglob = extglobs.length && (prior.type === 'pipe' || prior.type === 'paren'); | ||
|  |       if (!isStart && prior.type !== 'paren' && !isBrace && !isExtglob) { | ||
|  |         push({ type: 'star', value, output: '' }); | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       // strip consecutive `/**/`
 | ||
|  |       while (rest.slice(0, 3) === '/**') { | ||
|  |         const after = input[state.index + 4]; | ||
|  |         if (after && after !== '/') { | ||
|  |           break; | ||
|  |         } | ||
|  |         rest = rest.slice(3); | ||
|  |         consume('/**', 3); | ||
|  |       } | ||
|  | 
 | ||
|  |       if (prior.type === 'bos' && eos()) { | ||
|  |         prev.type = 'globstar'; | ||
|  |         prev.value += value; | ||
|  |         prev.output = globstar(opts); | ||
|  |         state.output = prev.output; | ||
|  |         state.globstar = true; | ||
|  |         consume(value); | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       if (prior.type === 'slash' && prior.prev.type !== 'bos' && !afterStar && eos()) { | ||
|  |         state.output = state.output.slice(0, -(prior.output + prev.output).length); | ||
|  |         prior.output = `(?:${prior.output}`; | ||
|  | 
 | ||
|  |         prev.type = 'globstar'; | ||
|  |         prev.output = globstar(opts) + (opts.strictSlashes ? ')' : '|$)'); | ||
|  |         prev.value += value; | ||
|  |         state.globstar = true; | ||
|  |         state.output += prior.output + prev.output; | ||
|  |         consume(value); | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       if (prior.type === 'slash' && prior.prev.type !== 'bos' && rest[0] === '/') { | ||
|  |         const end = rest[1] !== void 0 ? '|$' : ''; | ||
|  | 
 | ||
|  |         state.output = state.output.slice(0, -(prior.output + prev.output).length); | ||
|  |         prior.output = `(?:${prior.output}`; | ||
|  | 
 | ||
|  |         prev.type = 'globstar'; | ||
|  |         prev.output = `${globstar(opts)}${SLASH_LITERAL}|${SLASH_LITERAL}${end})`; | ||
|  |         prev.value += value; | ||
|  | 
 | ||
|  |         state.output += prior.output + prev.output; | ||
|  |         state.globstar = true; | ||
|  | 
 | ||
|  |         consume(value + advance()); | ||
|  | 
 | ||
|  |         push({ type: 'slash', value: '/', output: '' }); | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       if (prior.type === 'bos' && rest[0] === '/') { | ||
|  |         prev.type = 'globstar'; | ||
|  |         prev.value += value; | ||
|  |         prev.output = `(?:^|${SLASH_LITERAL}|${globstar(opts)}${SLASH_LITERAL})`; | ||
|  |         state.output = prev.output; | ||
|  |         state.globstar = true; | ||
|  |         consume(value + advance()); | ||
|  |         push({ type: 'slash', value: '/', output: '' }); | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       // remove single star from output
 | ||
|  |       state.output = state.output.slice(0, -prev.output.length); | ||
|  | 
 | ||
|  |       // reset previous token to globstar
 | ||
|  |       prev.type = 'globstar'; | ||
|  |       prev.output = globstar(opts); | ||
|  |       prev.value += value; | ||
|  | 
 | ||
|  |       // reset output with globstar
 | ||
|  |       state.output += prev.output; | ||
|  |       state.globstar = true; | ||
|  |       consume(value); | ||
|  |       continue; | ||
|  |     } | ||
|  | 
 | ||
|  |     const token = { type: 'star', value, output: star }; | ||
|  | 
 | ||
|  |     if (opts.bash === true) { | ||
|  |       token.output = '.*?'; | ||
|  |       if (prev.type === 'bos' || prev.type === 'slash') { | ||
|  |         token.output = nodot + token.output; | ||
|  |       } | ||
|  |       push(token); | ||
|  |       continue; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (prev && (prev.type === 'bracket' || prev.type === 'paren') && opts.regex === true) { | ||
|  |       token.output = value; | ||
|  |       push(token); | ||
|  |       continue; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (state.index === state.start || prev.type === 'slash' || prev.type === 'dot') { | ||
|  |       if (prev.type === 'dot') { | ||
|  |         state.output += NO_DOT_SLASH; | ||
|  |         prev.output += NO_DOT_SLASH; | ||
|  | 
 | ||
|  |       } else if (opts.dot === true) { | ||
|  |         state.output += NO_DOTS_SLASH; | ||
|  |         prev.output += NO_DOTS_SLASH; | ||
|  | 
 | ||
|  |       } else { | ||
|  |         state.output += nodot; | ||
|  |         prev.output += nodot; | ||
|  |       } | ||
|  | 
 | ||
|  |       if (peek() !== '*') { | ||
|  |         state.output += ONE_CHAR; | ||
|  |         prev.output += ONE_CHAR; | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     push(token); | ||
|  |   } | ||
|  | 
 | ||
|  |   while (state.brackets > 0) { | ||
|  |     if (opts.strictBrackets === true) throw new SyntaxError(syntaxError('closing', ']')); | ||
|  |     state.output = utils.escapeLast(state.output, '['); | ||
|  |     decrement('brackets'); | ||
|  |   } | ||
|  | 
 | ||
|  |   while (state.parens > 0) { | ||
|  |     if (opts.strictBrackets === true) throw new SyntaxError(syntaxError('closing', ')')); | ||
|  |     state.output = utils.escapeLast(state.output, '('); | ||
|  |     decrement('parens'); | ||
|  |   } | ||
|  | 
 | ||
|  |   while (state.braces > 0) { | ||
|  |     if (opts.strictBrackets === true) throw new SyntaxError(syntaxError('closing', '}')); | ||
|  |     state.output = utils.escapeLast(state.output, '{'); | ||
|  |     decrement('braces'); | ||
|  |   } | ||
|  | 
 | ||
|  |   if (opts.strictSlashes !== true && (prev.type === 'star' || prev.type === 'bracket')) { | ||
|  |     push({ type: 'maybe_slash', value: '', output: `${SLASH_LITERAL}?` }); | ||
|  |   } | ||
|  | 
 | ||
|  |   // rebuild the output if we had to backtrack at any point
 | ||
|  |   if (state.backtrack === true) { | ||
|  |     state.output = ''; | ||
|  | 
 | ||
|  |     for (const token of state.tokens) { | ||
|  |       state.output += token.output != null ? token.output : token.value; | ||
|  | 
 | ||
|  |       if (token.suffix) { | ||
|  |         state.output += token.suffix; | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   return state; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Fast paths for creating regular expressions for common glob patterns. | ||
|  |  * This can significantly speed up processing and has very little downside | ||
|  |  * impact when none of the fast paths match. | ||
|  |  */ | ||
|  | 
 | ||
|  | parse.fastpaths = (input, options) => { | ||
|  |   const opts = { ...options }; | ||
|  |   const max = typeof opts.maxLength === 'number' ? Math.min(MAX_LENGTH, opts.maxLength) : MAX_LENGTH; | ||
|  |   const len = input.length; | ||
|  |   if (len > max) { | ||
|  |     throw new SyntaxError(`Input length: ${len}, exceeds maximum allowed length: ${max}`); | ||
|  |   } | ||
|  | 
 | ||
|  |   input = REPLACEMENTS[input] || input; | ||
|  |   const win32 = utils.isWindows(options); | ||
|  | 
 | ||
|  |   // create constants based on platform, for windows or posix
 | ||
|  |   const { | ||
|  |     DOT_LITERAL, | ||
|  |     SLASH_LITERAL, | ||
|  |     ONE_CHAR, | ||
|  |     DOTS_SLASH, | ||
|  |     NO_DOT, | ||
|  |     NO_DOTS, | ||
|  |     NO_DOTS_SLASH, | ||
|  |     STAR, | ||
|  |     START_ANCHOR | ||
|  |   } = constants.globChars(win32); | ||
|  | 
 | ||
|  |   const nodot = opts.dot ? NO_DOTS : NO_DOT; | ||
|  |   const slashDot = opts.dot ? NO_DOTS_SLASH : NO_DOT; | ||
|  |   const capture = opts.capture ? '' : '?:'; | ||
|  |   const state = { negated: false, prefix: '' }; | ||
|  |   let star = opts.bash === true ? '.*?' : STAR; | ||
|  | 
 | ||
|  |   if (opts.capture) { | ||
|  |     star = `(${star})`; | ||
|  |   } | ||
|  | 
 | ||
|  |   const globstar = opts => { | ||
|  |     if (opts.noglobstar === true) return star; | ||
|  |     return `(${capture}(?:(?!${START_ANCHOR}${opts.dot ? DOTS_SLASH : DOT_LITERAL}).)*?)`; | ||
|  |   }; | ||
|  | 
 | ||
|  |   const create = str => { | ||
|  |     switch (str) { | ||
|  |       case '*': | ||
|  |         return `${nodot}${ONE_CHAR}${star}`; | ||
|  | 
 | ||
|  |       case '.*': | ||
|  |         return `${DOT_LITERAL}${ONE_CHAR}${star}`; | ||
|  | 
 | ||
|  |       case '*.*': | ||
|  |         return `${nodot}${star}${DOT_LITERAL}${ONE_CHAR}${star}`; | ||
|  | 
 | ||
|  |       case '*/*': | ||
|  |         return `${nodot}${star}${SLASH_LITERAL}${ONE_CHAR}${slashDot}${star}`; | ||
|  | 
 | ||
|  |       case '**': | ||
|  |         return nodot + globstar(opts); | ||
|  | 
 | ||
|  |       case '**/*': | ||
|  |         return `(?:${nodot}${globstar(opts)}${SLASH_LITERAL})?${slashDot}${ONE_CHAR}${star}`; | ||
|  | 
 | ||
|  |       case '**/*.*': | ||
|  |         return `(?:${nodot}${globstar(opts)}${SLASH_LITERAL})?${slashDot}${star}${DOT_LITERAL}${ONE_CHAR}${star}`; | ||
|  | 
 | ||
|  |       case '**/.*': | ||
|  |         return `(?:${nodot}${globstar(opts)}${SLASH_LITERAL})?${DOT_LITERAL}${ONE_CHAR}${star}`; | ||
|  | 
 | ||
|  |       default: { | ||
|  |         const match = /^(.*?)\.(\w+)$/.exec(str); | ||
|  |         if (!match) return; | ||
|  | 
 | ||
|  |         const source = create(match[1]); | ||
|  |         if (!source) return; | ||
|  | 
 | ||
|  |         return source + DOT_LITERAL + match[2]; | ||
|  |       } | ||
|  |     } | ||
|  |   }; | ||
|  | 
 | ||
|  |   const output = utils.removePrefix(input, state); | ||
|  |   let source = create(output); | ||
|  | 
 | ||
|  |   if (source && opts.strictSlashes !== true) { | ||
|  |     source += `${SLASH_LITERAL}?`; | ||
|  |   } | ||
|  | 
 | ||
|  |   return source; | ||
|  | }; | ||
|  | 
 | ||
|  | module.exports = parse; |