cspell-glob
Version:
Glob matcher for cspell
102 lines (101 loc) • 4.74 kB
JavaScript
import mm from 'micromatch';
import * as Path from 'path';
import { doesRootContainPath, normalizeGlobPatterns, normalizeGlobToRoot } from './globHelper.mjs';
export class GlobMatcher {
constructor(patterns, rootOrOptions, _nodePath) {
_nodePath = _nodePath ?? Path;
const options = typeof rootOrOptions === 'string' ? { root: rootOrOptions } : rootOrOptions ?? {};
const { mode = 'exclude' } = options;
const isExcludeMode = mode !== 'include';
_nodePath = options.nodePath ?? _nodePath;
const { root = _nodePath.resolve(), dot = isExcludeMode, nodePath = _nodePath, nested = isExcludeMode, cwd = process.cwd(), nobrace, } = options;
const normalizedRoot = nodePath.resolve(nodePath.normalize(root));
this.options = { root: normalizedRoot, dot, nodePath, nested, mode, nobrace, cwd };
patterns = Array.isArray(patterns)
? patterns
: typeof patterns === 'string'
? patterns.split(/\r?\n/g)
: [patterns];
const globPatterns = normalizeGlobPatterns(patterns, this.options);
this.patternsNormalizedToRoot = globPatterns
.map((g) => normalizeGlobToRoot(g, normalizedRoot, nodePath))
// Only keep globs that do not match the root when using exclude mode.
.filter((g) => nodePath.relative(g.root, normalizedRoot) === '');
this.patterns = globPatterns;
this.root = normalizedRoot;
this.path = nodePath;
this.dot = dot;
this.matchEx = buildMatcherFn(this.patterns, this.options);
}
/**
* Check to see if a filename matches any of the globs.
* If filename is relative, it is considered relative to the root.
* If filename is absolute and contained within the root, it will be made relative before being tested for a glob match.
* If filename is absolute and not contained within the root, it will be tested as is.
* @param filename full path of the file to check.
*/
match(filename) {
return this.matchEx(filename).matched;
}
}
/**
* This function attempts to emulate .gitignore functionality as much as possible.
*
* The resulting matcher function: (filename: string) => GlobMatch
*
* If filename is relative, it is considered relative to the root.
* If filename is absolute and contained within the root, it will be made relative before being tested for a glob match.
* If filename is absolute and not contained within the root, it will return a GlobMatchNoRule.
*
* @param patterns - the contents of a .gitignore style file or an array of individual glob rules.
* @param options - defines root and other options
* @returns a function given a filename returns true if it matches.
*/
function buildMatcherFn(patterns, options) {
const { nodePath: path, dot, nobrace } = options;
const makeReOptions = { dot, nobrace };
const rules = patterns
.map((pattern, index) => ({ pattern, index }))
.filter((r) => !!r.pattern.glob)
.filter((r) => !r.pattern.glob.startsWith('#'))
.map(({ pattern, index }) => {
const matchNeg = pattern.glob.match(/^!/);
const glob = pattern.glob.replace(/^!/, '');
const isNeg = (matchNeg && matchNeg[0].length & 1 && true) || false;
const reg = mm.makeRe(glob, makeReOptions);
const fn = (filename) => {
const match = filename.match(reg);
return !!match;
};
return { pattern, index, isNeg, fn, reg };
});
const negRules = rules.filter((r) => r.isNeg);
const posRules = rules.filter((r) => !r.isNeg);
const fn = (filename) => {
filename = path.resolve(path.normalize(filename));
function testRules(rules, matched) {
for (const rule of rules) {
const pattern = rule.pattern;
const root = pattern.root;
const isRelPat = !pattern.isGlobalPattern;
if (isRelPat && !doesRootContainPath(root, filename, path)) {
continue;
}
const relName = isRelPat ? path.relative(root, filename) : filename;
const fname = path.sep === '\\' ? relName.replace(/\\/g, '/') : relName;
if (rule.fn(fname)) {
return {
matched,
glob: pattern.glob,
root,
pattern,
index: rule.index,
isNeg: rule.isNeg,
};
}
}
}
return testRules(negRules, false) || testRules(posRules, true) || { matched: false };
};
return fn;
}