UNPKG

cspell-glob

Version:
258 lines (257 loc) 9.18 kB
/* eslint-disable no-irregular-whitespace */ import * as Path from 'path'; const { posix } = Path; const relRegExp = /^\.[\\/]/; /** test for glob patterns starting with `**` */ const isGlobalPatternRegExp = /^!*[*]{2}/; /** * This function tries its best to determine if `fileOrGlob` is a path to a file or a glob pattern. * @param fileOrGlob - file (with absolute path) or glob. * @param root - absolute path to the directory that will be considered the root when testing the glob pattern. * @param path - optional node path methods - used for testing */ export function fileOrGlobToGlob(fileOrGlob, root, path = Path) { const pathToGlob = path.sep === '\\' ? (p) => p.replace(/\\/g, '/') : (p) => p; const isGlobalPattern = false; if (isGlobPatternWithOptionalRoot(fileOrGlob)) { const useRoot = fileOrGlob.root ?? root; const isGlobalPattern = isGlobPatternWithRoot(fileOrGlob) ? fileOrGlob.isGlobalPattern : isGlobalGlob(fileOrGlob.glob); return { ...fileOrGlob, root: useRoot, isGlobalPattern }; } if (doesRootContainPath(root, fileOrGlob, path) || relRegExp.test(fileOrGlob)) { const rel = path.relative(root, path.resolve(root, fileOrGlob)); return { glob: pathToGlob(rel), root, isGlobalPattern }; } return { glob: pathToGlob(fileOrGlob), root, isGlobalPattern }; } /** * Decide if a childPath is contained within a root or at the same level. * @param root - absolute path * @param childPath - absolute path */ export function doesRootContainPath(root, child, path) { if (child.startsWith(root)) return true; const rel = path.relative(root, child); return !rel || (rel !== child && !rel.startsWith('..') && !path.isAbsolute(rel)); } export function isGlobPatternWithOptionalRoot(g) { return typeof g !== 'string' && typeof g.glob === 'string'; } export function isGlobPatternWithRoot(g) { return typeof g.root === 'string' && 'isGlobalPattern' in g; } export function isGlobPatternNormalized(g) { if (!isGlobPatternWithOptionalRoot(g)) return false; if (!isGlobPatternWithRoot(g)) return false; const gr = g; return 'rawGlob' in gr && 'rawRoot' in gr && typeof gr.rawGlob === 'string'; } /** * @param pattern glob pattern * @param nested when true add `**​/<glob>/​**` * @returns the set of matching globs. */ function normalizePattern(pattern, nested) { pattern = pattern.replace(/^(!!)+/, ''); const isNeg = pattern.startsWith('!'); const prefix = isNeg ? '!' : ''; pattern = isNeg ? pattern.slice(1) : pattern; const patterns = nested ? normalizePatternNested(pattern) : normalizePatternGeneral(pattern); return patterns.map((p) => prefix + p); } function normalizePatternNested(pattern) { // no slashes will match files names or folders if (!pattern.includes('/')) { if (pattern === '**') return ['**']; return ['**/' + pattern, '**/' + pattern + '/**']; } const hasLeadingSlash = pattern.startsWith('/'); pattern = hasLeadingSlash ? pattern.slice(1) : pattern; if (pattern.endsWith('/')) { // legacy behavior, if it only has a trailing slash, allow matching against a nested directory. return hasLeadingSlash || pattern.slice(0, -1).includes('/') ? [pattern + '**/*'] : ['**/' + pattern + '**/*']; } if (pattern.endsWith('**')) { return [pattern]; } return [pattern, pattern + '/**']; } function normalizePatternGeneral(pattern) { pattern = pattern.startsWith('/') ? pattern.slice(1) : pattern; pattern = pattern.endsWith('/') ? pattern + '**/*' : pattern; return [pattern]; } /** * * @param patterns - glob patterns to normalize. * @param options - Normalization options. */ export function normalizeGlobPatterns(patterns, options) { function* normalize() { for (const glob of patterns) { if (isGlobPatternNormalized(glob)) { yield glob; continue; } yield* normalizeGlobPattern(glob, options); } } return [...normalize()]; } export function normalizeGlobPattern(g, options) { const { root, nodePath: path = Path, nested, cwd = Path.resolve() } = options; g = !isGlobPatternWithOptionalRoot(g) ? { glob: g } : g; const gr = { ...g, root: g.root ?? root }; const rawRoot = gr.root; const rawGlob = g.glob; gr.glob = gr.glob.trim(); // trimGlob(g.glob); if (gr.glob.startsWith('${cwd}')) { gr.glob = gr.glob.replace('${cwd}', ''); gr.root = '${cwd}'; } if (gr.root.startsWith('${cwd}')) { gr.root = path.resolve(gr.root.replace('${cwd}', cwd)); } const isGlobalPattern = isGlobalGlob(gr.glob); gr.root = path.resolve(root, path.normalize(gr.root)); const globs = normalizePattern(gr.glob, nested); return globs.map((glob) => ({ ...gr, glob, rawGlob, rawRoot, isGlobalPattern })); } /** * Try to adjust the root of a glob to match a new root. If it is not possible, the original glob is returned. * Note: this does NOT generate absolutely correct glob patterns. The results are intended to be used as a * first pass only filter. Followed by testing against the original glob/root pair. * @param glob - glob to map * @param root - new root to use if possible * @param path - Node Path modules to use (testing only) */ export function normalizeGlobToRoot(glob, root, path) { function relToGlob(relativePath) { return path.sep === '\\' ? relativePath.replace(/\\/g, '/') : relativePath; } if (glob.root === root) { return glob; } const relFromRootToGlob = path.relative(root, glob.root); if (!relFromRootToGlob) { return glob; } if (glob.isGlobalPattern) { return { ...glob, root }; } const relFromGlobToRoot = path.relative(glob.root, root); const globIsUnderRoot = relFromRootToGlob[0] !== '.' && !path.isAbsolute(relFromRootToGlob); const rootIsUnderGlob = relFromGlobToRoot[0] !== '.' && !path.isAbsolute(relFromGlobToRoot); // Root and Glob are not in the same part of the directory tree. if (!globIsUnderRoot && !rootIsUnderGlob) { return glob; } const isNeg = glob.glob.startsWith('!'); const g = isNeg ? glob.glob.slice(1) : glob.glob; const prefix = isNeg ? '!' : ''; // prefix with root if (globIsUnderRoot) { const relGlob = relToGlob(relFromRootToGlob); return { ...glob, glob: prefix + posix.join(relGlob, g), root, }; } // The root is under the glob root // The more difficult case, the glob is higher than the root // A best effort is made, but does not do advanced matching. const relGlob = relToGlob(relFromGlobToRoot) + '/'; const rebasedGlob = rebaseGlob(g, relGlob); return rebasedGlob ? { ...glob, glob: prefix + rebasedGlob, root } : glob; } /** * Rebase a glob string to a new prefix * @param glob - glob string * @param rebaseTo - glob prefix */ function rebaseGlob(glob, rebaseTo) { if (!rebaseTo || rebaseTo === '/') return glob; if (glob.startsWith('**')) return glob; rebaseTo = rebaseTo.endsWith('/') ? rebaseTo : rebaseTo + '/'; if (glob.startsWith(rebaseTo)) { return glob.slice(rebaseTo.length); } const relParts = rebaseTo.split('/'); const globParts = glob.split('/'); for (let i = 0; i < relParts.length && i < globParts.length; ++i) { const relSeg = relParts[i]; const globSeg = globParts[i]; // the empty segment due to the end relGlob / allows for us to test against an empty segment. if (!relSeg || globSeg === '**') { return globParts.slice(i).join('/'); } if (relSeg !== globSeg && globSeg !== '*') { break; } } return undefined; } /** * Trims any trailing spaces, tabs, line-feeds, new-lines, and comments * @param glob - glob string * @returns trimmed glob */ function trimGlob(glob) { glob = glob.replace(/(?<!\\)#.*/g, ''); glob = trimGlobLeft(glob); glob = trimGlobRight(glob); return glob; } const spaces = { ' ': true, '\t': true, '\n': true, '\r': true, }; /** * Trim any trailing spaces, tabs, line-feeds, or new-lines * Handles a trailing \<space> * @param glob - glob string * @returns glob string with space to the right removed. */ function trimGlobRight(glob) { const lenMin1 = glob.length - 1; let i = lenMin1; while (i >= 0 && glob[i] in spaces) { --i; } if (glob[i] === '\\' && i < lenMin1) { ++i; } ++i; return i ? glob.slice(0, i) : ''; } /** * Trim any leading spaces, tabs, line-feeds, or new-lines * @param glob - any string * @returns string with leading spaces removed. */ function trimGlobLeft(glob) { let i = 0; while (i < glob.length && glob[i] in spaces) { ++i; } return glob.slice(i); } function isGlobalGlob(glob) { return isGlobalPatternRegExp.test(glob); } export const __testing__ = { rebaseGlob, trimGlob, isGlobalGlob, };