80 lines
		
	
	
		
			2.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			80 lines
		
	
	
		
			2.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import * as XRegExp from 'xregexp';
 | |
| 
 | |
| // The `XRegExp` typings are not accurate.
 | |
| interface XRegExp extends RegExp {
 | |
|   xregexp: { captureNames?: string[] };
 | |
| }
 | |
| 
 | |
| const dot = /\./g;
 | |
| const star = /\*/g;
 | |
| const doubleStar = /(^|\/)\*\*($|\/)/g;           // e.g. a/**/b or **/b or a/** but not a**b
 | |
| const modifiedPatterns = /(.)\(([^)]+)\)/g;       // e.g. `@(a|b)
 | |
| const restParam = /\/:([A-Za-z]+)\*/g;            // e.g. `:rest*`
 | |
| const namedParam = /\/:([A-Za-z]+)/g;             // e.g. `:api`
 | |
| const possiblyEmptyInitialSegments = /^\.🐷\//g;  // e.g. `**/a` can also match `a`
 | |
| const possiblyEmptySegments = /\/\.🐷\//g;        // e.g. `a/**/b` can also match `a/b`
 | |
| const willBeStar = /🐷/g;                         // e.g. `a**b` not matched by previous rule
 | |
| 
 | |
| export class FirebaseGlob {
 | |
|   pattern: string;
 | |
|   regex: XRegExp;
 | |
|   namedParams: { [key: string]: boolean } = {};
 | |
|   restParams: { [key: string]: boolean } = {};
 | |
|   constructor(glob: string) {
 | |
|     try {
 | |
|       const pattern = glob
 | |
|           .replace(dot, '\\.')
 | |
|           .replace(modifiedPatterns, replaceModifiedPattern)
 | |
|           .replace(restParam, (_, param) => {
 | |
|             // capture the rest of the string
 | |
|             this.restParams[param] = true;
 | |
|             return `(?:/(?<${param}>.🐷))?`;
 | |
|           })
 | |
|           .replace(namedParam, (_, param) => {
 | |
|             // capture the named parameter
 | |
|             this.namedParams[param] = true;
 | |
|             return `/(?<${param}>[^/]+)`;
 | |
|           })
 | |
|           .replace(doubleStar, '$1.🐷$2')                 // use the pig to avoid replacing ** in next rule
 | |
|           .replace(star, '[^/]*')                         // match a single segment
 | |
|           .replace(possiblyEmptyInitialSegments, '(?:.*)')// deal with **/ special cases
 | |
|           .replace(possiblyEmptySegments, '(?:/|/.*/)')   // deal with /**/ special cases
 | |
|           .replace(willBeStar, '*');                      // other ** matches
 | |
|       this.pattern = `^${pattern}$`;
 | |
|       this.regex = XRegExp(this.pattern) as XRegExp;
 | |
|     } catch (e) {
 | |
|       throw new Error(`Error in FirebaseGlob: "${glob}" - ${e.message}`);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   test(url: string) {
 | |
|     return XRegExp.test(url, this.regex);
 | |
|   }
 | |
| 
 | |
|   match(url: string) {
 | |
|     const match = XRegExp.exec(url, this.regex) as ReturnType<typeof XRegExp.exec> & { [captured: string]: string };
 | |
|     if (match) {
 | |
|       const result: { [key: string]: string } = {};
 | |
|       const names = this.regex.xregexp.captureNames || [];
 | |
|       names.forEach(name => result[name] = (match[name]));
 | |
|       return result;
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| function replaceModifiedPattern(_: string, modifier: string, pattern: string) {
 | |
|   switch (modifier) {
 | |
|     case '!':
 | |
|       throw new Error(`"not" expansions are not supported: "${_}"`);
 | |
|     case '?':
 | |
|     case '+':
 | |
|       return `(${pattern})${modifier}`;
 | |
|     case '*':
 | |
|       return `(${pattern})🐷`;  // it will become a star
 | |
|     case '@':
 | |
|       return `(${pattern})`;
 | |
|     default:
 | |
|       throw new Error(`unknown expansion type: "${modifier}" in "${_}"`);
 | |
|   }
 | |
| }
 |