UNPKG

@surface/path-matcher

Version:

Provides path matching capabilities.

359 lines (358 loc) 13.2 kB
import { ALPHABRACES_PATTERN, CHARACTERS_CLASS_MAP, ESCAPABLE_CHARACTERS, GROUPS, NUMBRACES_PATTERN, PATTERN_TOKENS, QUOTES, REGEX_SPECIAL_CHARACTERS, SEPARATORS, } from "./characters.js"; import ContextType from "./context-type.js"; import Context from "./context.js"; import { parseAlphaRange, parseNumericRange } from "./range-parser.js"; export default class Parser { source; options; index = 0; context = new Context(ContextType.Literal); constructor(source, options = {}) { this.source = source; this.options = options; } advance(offset = 1) { this.index += offset; /* c8 ignore start */ if (this.index > this.source.length) { throw new Error(); } /* c8 ignore stop */ } eos() { return this.index == this.source.length; } escape(token) { return REGEX_SPECIAL_CHARACTERS.has(token) ? `${!this.context.inside(ContextType.Class, 1) || token == "]" ? "\\" : ""}${token}` : token; } getChar(length) { return length ? this.source.substring(this.index, this.index + length) : this.source[this.index]; } getNextChar() { return this.source[this.index + 1]; } getPreviousChar() { return this.source[this.index - 1]; } lookahead(character, stopCondition = () => true) { let index = this.index; let end = null; while (index < this.source.length) { const char = this.source[index]; const next = this.source[index + 1]; if (char == "\\" && next == character) { index += 2; } else { if (this.source[index] == character) { if (stopCondition(index)) { return index; } end = index; } index++; } } return end; } scanBraces() { this.context = new Context(ContextType.Braces, this.context); const group = GROUPS["{"]; this.context.push(group.open, this.escape("{")); this.advance(); const start = this.index; let segments = 0; let isOptional = false; while (!this.eos()) { segments++; this.scanPattern(); const char = this.getChar(); if (char == ",") { const previous = this.getPreviousChar(); const isEmpty = previous == "{" || previous == char; if (!isOptional) { isOptional = isEmpty; } if (!isEmpty) { this.context.push("|", char); } this.advance(); } else { break; } } if (this.getChar() == "}") { const end = this.index; if (segments > 1) { this.context.push(group.close + (isOptional ? "?" : "")); this.advance(); } else { const segment = this.source.substring(start, end); const alphaMatch = ALPHABRACES_PATTERN.exec(segment); const digitMatch = NUMBRACES_PATTERN.exec(segment); if (alphaMatch || digitMatch) { this.context.discard(); this.context.push(group.open); if (alphaMatch) { this.context.push(parseAlphaRange(alphaMatch[1], alphaMatch[2], Number(alphaMatch[3] ?? "1"))); } else if (digitMatch) { this.context.push(parseNumericRange(digitMatch[1], digitMatch[2], Number(digitMatch[3] ?? "1"))); } this.context.push(group.close); this.advance(); } else { this.context.rollback(); } } } else { this.context.rollback(); } if (!this.context.rolledBack) { let parent = this.context.parent; while (parent && parent.type != ContextType.Literal) { if (parent.type != ContextType.Braces) { parent.rollback(); } parent = parent.parent; } } this.context = this.context.parent; } scanClasses() { if (this.getChar(2) == "[:") { for (const offset of [8, 9, 10]) { const characterClass = CHARACTERS_CLASS_MAP[this.getChar(offset)]; if (characterClass) { this.advance(offset); this.context.push(characterClass); return; } } } this.context = new Context(ContextType.Class, this.context); const group = GROUPS["["]; this.context.push(group.open, "\\["); this.advance(); const char = this.getChar(); if ((char == "^" || char == "!") && this.context.type == ContextType.Class && this.getPreviousChar() == "[") { this.context.push("^", this.escape(char)); this.advance(); } this.scanPattern(); if (!this.context.rolledBack && this.getChar() == "]") { this.context.push(group.close); this.context.children.forEach(x => x.rollback(true)); this.advance(); } else { this.context.rollback(); } this.context = this.context.parent; } scanEscaped() { this.context.push(this.escape(this.getNextChar())); this.advance(2); } scanLiteral() { const char = this.getChar(); this.context.push(this.escape(char)); this.advance(); } scanNegation() { this.context = new Context(ContextType.Negation, this.context); const group = GROUPS[`!${this.options.dot ? "." : ""}`]; this.context.push(group.open, ""); if (this.getNextChar() == "(") { this.scanPatternList(); } else { this.advance(); this.scanPattern(); } if (!this.context.rolledBack) { this.context.push(group.close); } this.context = this.context.parent; } scanQuotes() { const quote = this.getChar(); this.advance(); const end = this.lookahead(quote); if (end) { while (this.index < end && !this.eos()) { if (this.getChar() == "\\") { this.advance(); } this.scanLiteral(); } this.advance(); } else { this.context.tokens.push(quote); } } scanPattern() { while (!this.eos()) { const char = this.getChar(); const shouldStop = char == "," && this.context.type == ContextType.Braces || char == "}" && this.context.inside(ContextType.Braces) || char == "|" && this.context.type == ContextType.PatternList || char == ")" && this.context.inside(ContextType.PatternList) || char == "]" && !(this.getPreviousChar() == "[" && this.getNextChar() == "]") && this.context.inside(ContextType.Class); if (shouldStop) { return; } if (QUOTES.has(char)) { this.scanQuotes(); } else if (char == "\\") { this.scanEscaped(); } else if (char == "/") { this.scanSlash(); } else if (char == "{" && !this.options.noBrace) { this.scanBraces(); } else if (!this.options.noNegate && this.index == 0 && char == "!") { this.scanNegation(); } else if (!this.options.noExtGlob && PATTERN_TOKENS.has(char) && this.getNextChar() == "(") { this.scanPatternList(); } else if (char == "?") { this.scanPlaceholder(); } else if (char == "*") { this.scanStar(); } else if (char == "[" && this.context.type != ContextType.Class) { this.scanClasses(); } else { this.scanLiteral(); } } } scanPatternList() { const start = this.index; this.context = new Context(ContextType.PatternList, this.context); const char = this.getChar(); const group = GROUPS[`${char}(`]; this.context.push(group.open, char + this.escape("(")); this.advance(2); while (!this.eos()) { this.scanPattern(); const char = this.getChar(); if (char == "|") { this.context.push(char, this.escape(char)); this.advance(); } else { break; } } if (!this.context.rolledBack && this.getChar() == ")") { this.advance(); this.context.tokens.push(group.close); if (start == 0 && char == "!" && this.context.parent?.type == ContextType.Negation) { this.context.parent.rollback(); } } else { this.context.rollback(); } this.context = this.context.parent; } scanPlaceholder() { const pattern = `[^${this.options.dot ? "" : "."}\\/\\\\]`; this.context.type == ContextType.Class ? this.context.push("?", pattern) : this.context.push(pattern); this.advance(); } scanSlash() { this.context.type == ContextType.Class ? this.context.push("\\/\\\\", "[\\/\\\\]") : this.context.push("[\\/\\\\]"); this.advance(); } scanStar() { let stars = 0; const canUseGlobStar = this.index == 0 || SEPARATORS.has(this.source[this.index - 1]); while (this.getChar() == "*") { this.advance(); stars++; } if (canUseGlobStar && !this.options.noGlobStar && stars == 2) { const pattern = this.options.dot ? "(?!\\.\\.?[\\/\\\\]).*[\\/\\\\]?" : "(?:[^.\\/\\\\][^\\/\\\\]*[\\/\\\\]?)*"; this.context.type == ContextType.Class ? this.context.push("*", pattern) : this.context.push(pattern); if (SEPARATORS.has(this.getChar())) { this.advance(); } } else { const pattern = `${!this.options.dot && canUseGlobStar ? "[^.\\/\\\\]" : ""}[^\\/\\\\]*`; this.context.type == ContextType.Class ? this.context.push("*", pattern) : this.context.push(pattern); } } parse() { this.scanPattern(); return new RegExp(`^${this.context.tokens.join("")}$`, this.options.noCase ? "i" : ""); } split() { let negated = false; if (this.source.startsWith("!") && (this.source[1] != "(" || !this.lookahead(")", () => false))) { negated = true; this.advance(); } const start = this.index; let end = start; if (SEPARATORS.has(this.getChar())) { this.advance(); } let broken = false; while (!this.eos()) { const char = this.getChar(); if (char == "\\" && ESCAPABLE_CHARACTERS.has(this.getNextChar())) { broken = true; } else if (SEPARATORS.has(char)) { end = this.index; } else { broken = char == "?" || char == "*" && !this.options.noGlobStar || char == "\"" && !!this.lookahead("\"") || char == "'" && !!this.lookahead("'") || char == "[" && !!this.lookahead("]") || char == "{" && !this.options.noBrace && !!this.lookahead("}") || !this.options.noExtGlob && PATTERN_TOKENS.has(char) && (this.source[this.index + 1] == "(" && !!this.lookahead(")", () => false)); if (broken) { break; } } this.advance(); } if (!broken) { end = this.source.length - (this.source.endsWith("/") ? 1 : 0); } return { path: this.source.substring(start, end), pattern: (negated ? "!" : "") + (end == 0 ? this.source : this.source.substring(end + 1, this.source.length)), }; } }