glob-to-regex.js 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -46,6 +46,13 @@ match('types.d.ts'); // true (negation is not parsed specially)
46
46
  - `**` matches across path segments, including none
47
47
  - `{a,b,c}` alternation groups (no nesting). Each item inside can itself contain glob syntax
48
48
  - Character classes: `[abc]`, `[a-z]`, `[!a-z]`, `[!abc]`
49
+ - **Extended globbing** (when `extglob: true` option is set):
50
+ - `?(pattern-list)` matches zero or one occurrence of the given patterns
51
+ - `*(pattern-list)` matches zero or more occurrences of the given patterns
52
+ - `+(pattern-list)` matches one or more occurrences of the given patterns
53
+ - `@(pattern-list)` matches exactly one of the given patterns
54
+ - `!(pattern-list)` matches anything except one of the given patterns
55
+ - Pattern lists use `|` as separator (e.g., `@(jpg|png|gif)`)
49
56
 
50
57
  Notes:
51
58
  - The produced RegExp is anchored at start and end (`^...$`).
@@ -66,6 +73,15 @@ toRegex('src/{a,b}/**/*.ts').test('src/b/x/y.ts'); // true
66
73
  toRegex('file[0-9].txt').test('file5.txt'); // true
67
74
  toRegex('file[!0-9].txt').test('filea.txt'); // true
68
75
  toRegex('**/*.[jt]s{,x}').test('dir/a/b.jsx'); // true
76
+
77
+ // Extended globbing examples
78
+ toRegex('file?(s).txt', {extglob: true}).test('file.txt'); // true
79
+ toRegex('file?(s).txt', {extglob: true}).test('files.txt'); // true
80
+ toRegex('file.@(jpg|png|gif)', {extglob: true}).test('file.jpg'); // true
81
+ toRegex('/var/log/!(*.gz)', {extglob: true}).test('/var/log/syslog'); // true
82
+ toRegex('/var/log/!(*.gz)', {extglob: true}).test('/var/log/error.log.gz'); // false
83
+ toRegex('src/**/!(*.test).js', {extglob: true}).test('src/app.test.js'); // false
84
+ toRegex('src/**/!(*.test).js', {extglob: true}).test('src/index.js'); // true
69
85
  ```
70
86
 
71
87
  ## TypeScript
package/lib/index.d.ts CHANGED
@@ -1,3 +1,9 @@
1
+ export interface GlobOptions {
2
+ /** Treat pattern as case insensitive */
3
+ nocase?: boolean;
4
+ /** Enable extended globbing: ?(pattern), *(pattern), +(pattern), @(pattern), !(pattern) */
5
+ extglob?: boolean;
6
+ }
1
7
  /**
2
8
  * Convert a glob pattern to a regular expression
3
9
  *
@@ -8,8 +14,14 @@
8
14
  * - `**` to match any number of path segments, including none
9
15
  * - `{}` to group conditions (e.g. `{html,txt}`)
10
16
  * - `[abc]`, `[a-z]`, `[!a-z]`, `[!abc]` character classes
17
+ * - Extended globbing (when `extglob: true` option is set):
18
+ * - `?(pattern-list)` zero or one occurrence
19
+ * - `*(pattern-list)` zero or more occurrences
20
+ * - `+(pattern-list)` one or more occurrences
21
+ * - `@(pattern-list)` exactly one of the patterns
22
+ * - `!(pattern-list)` anything except the patterns
11
23
  */
12
- export declare const toRegex: (pattern: string) => RegExp;
24
+ export declare const toRegex: (pattern: string, options?: GlobOptions) => RegExp;
13
25
  /**
14
26
  * A glob pattern to match files paths against. An array or a single pattern
15
27
  * can be provided, if an array is given, then individual patterns will be
@@ -21,4 +33,4 @@ export declare const toRegex: (pattern: string) => RegExp;
21
33
  */
22
34
  export type Pattern = string | RegExp | (string | RegExp)[];
23
35
  export type Matcher = (path: string) => boolean;
24
- export declare const toMatcher: (pattern: Pattern) => Matcher;
36
+ export declare const toMatcher: (pattern: Pattern, options?: GlobOptions) => Matcher;
package/lib/index.js CHANGED
@@ -2,6 +2,68 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.toMatcher = exports.toRegex = void 0;
4
4
  const escapeRe = (ch) => (/[.^$+{}()|\\]/.test(ch) ? `\\${ch}` : ch);
5
+ /**
6
+ * Parse an extended glob pattern like ?(a|b|c)
7
+ * Returns the regex string equivalent and the new index position
8
+ */
9
+ const parseExtGlob = (pattern, startIdx, prefix, options) => {
10
+ let i = startIdx; // startIdx should be pointing at the character after '('
11
+ const parts = [];
12
+ let cur = '';
13
+ let depth = 1; // Track parenthesis depth for nested patterns
14
+ while (i < pattern.length && depth > 0) {
15
+ const ch = pattern[i];
16
+ if (ch === '(') {
17
+ depth++;
18
+ cur += ch;
19
+ i++;
20
+ }
21
+ else if (ch === ')') {
22
+ depth--;
23
+ if (depth === 0) {
24
+ // Found the closing parenthesis
25
+ parts.push(cur);
26
+ i++; // consume ')'
27
+ break;
28
+ }
29
+ else {
30
+ cur += ch;
31
+ i++;
32
+ }
33
+ }
34
+ else if (ch === '|' && depth === 1) {
35
+ // Pipe separator at top level of this extglob
36
+ parts.push(cur);
37
+ cur = '';
38
+ i++;
39
+ }
40
+ else {
41
+ cur += ch;
42
+ i++;
43
+ }
44
+ }
45
+ if (depth !== 0)
46
+ return; // Unclosed parenthesis
47
+ let alternatives = '';
48
+ const length = parts.length;
49
+ for (let j = 0; j < length; j++)
50
+ alternatives += (alternatives ? '|' : '') + (0, exports.toRegex)(parts[j], options).source.replace(/^\^/, '').replace(/\$$/, '');
51
+ switch (prefix) {
52
+ case '?': // zero or one
53
+ return [`(?:${alternatives})?`, i];
54
+ case '*': // zero or more
55
+ return [`(?:${alternatives})*`, i];
56
+ case '+': // one or more
57
+ return [`(?:${alternatives})+`, i];
58
+ case '@': // exactly one
59
+ return [`(?:${alternatives})`, i];
60
+ case '!': // none of (negative match)
61
+ // For negation, we need to match anything that doesn't match the pattern
62
+ // Use negative lookahead without consuming characters after
63
+ return [`(?!${alternatives})[^/]*`, i];
64
+ }
65
+ return;
66
+ };
5
67
  /**
6
68
  * Convert a glob pattern to a regular expression
7
69
  *
@@ -12,8 +74,14 @@ const escapeRe = (ch) => (/[.^$+{}()|\\]/.test(ch) ? `\\${ch}` : ch);
12
74
  * - `**` to match any number of path segments, including none
13
75
  * - `{}` to group conditions (e.g. `{html,txt}`)
14
76
  * - `[abc]`, `[a-z]`, `[!a-z]`, `[!abc]` character classes
77
+ * - Extended globbing (when `extglob: true` option is set):
78
+ * - `?(pattern-list)` zero or one occurrence
79
+ * - `*(pattern-list)` zero or more occurrences
80
+ * - `+(pattern-list)` one or more occurrences
81
+ * - `@(pattern-list)` exactly one of the patterns
82
+ * - `!(pattern-list)` anything except the patterns
15
83
  */
16
- const toRegex = (pattern) => {
84
+ const toRegex = (pattern, options) => {
17
85
  let regexStr = '';
18
86
  let i = 0;
19
87
  // Helper to parse a brace group like {a,b,c}. No nesting support.
@@ -45,11 +113,24 @@ const toRegex = (pattern) => {
45
113
  return '\\{' + escapeRe(cur);
46
114
  }
47
115
  // Convert each part recursively to support globs inside braces
48
- const alt = parts.map((p) => (0, exports.toRegex)(p).source.replace(/^\^/, '').replace(/\$$/, '')).join('|');
116
+ const alt = parts.map((p) => (0, exports.toRegex)(p, options).source.replace(/^\^/, '').replace(/\$$/, '')).join('|');
49
117
  return `(?:${alt})`;
50
118
  };
119
+ const extglob = !!options?.extglob;
51
120
  while (i < pattern.length) {
52
121
  const char = pattern[i];
122
+ // Check for extended glob patterns when extglob is enabled
123
+ if (extglob && pattern[i + 1] === '(') {
124
+ if (char === '?' || char === '*' || char === '+' || char === '@' || char === '!') {
125
+ const result = parseExtGlob(pattern, i + 2, char, options);
126
+ if (result) {
127
+ regexStr += result[0];
128
+ i = result[1];
129
+ continue;
130
+ }
131
+ // If parse failed, fall through to normal handling
132
+ }
133
+ }
53
134
  switch (char) {
54
135
  case '*': {
55
136
  // Check for double star **
@@ -134,11 +215,12 @@ const toRegex = (pattern) => {
134
215
  break;
135
216
  }
136
217
  }
137
- return new RegExp('^' + regexStr + '$');
218
+ const flags = options?.nocase ? 'i' : '';
219
+ return new RegExp('^' + regexStr + '$', flags);
138
220
  };
139
221
  exports.toRegex = toRegex;
140
222
  const isRegExp = /^\/(.{1,4096})\/([gimsuy]{0,6})$/;
141
- const toMatcher = (pattern) => {
223
+ const toMatcher = (pattern, options) => {
142
224
  const regexes = [];
143
225
  const patterns = Array.isArray(pattern) ? pattern : [pattern];
144
226
  for (const pat of patterns) {
@@ -149,19 +231,15 @@ const toMatcher = (pattern) => {
149
231
  regexes.push(new RegExp(expr, flags));
150
232
  }
151
233
  else {
152
- regexes.push((0, exports.toRegex)(pat));
234
+ regexes.push((0, exports.toRegex)(pat, options));
153
235
  }
154
236
  }
155
237
  else {
156
238
  regexes.push(pat);
157
239
  }
158
240
  }
159
- const length = regexes.length;
160
- return (path) => {
161
- for (let i = 0; i < length; i++)
162
- if (regexes[i].test(path))
163
- return true;
164
- return false;
165
- };
241
+ return regexes.length
242
+ ? new Function('p', 'return ' + regexes.map((r) => r + '.test(p)').join('||'))
243
+ : () => false;
166
244
  };
167
245
  exports.toMatcher = toMatcher;
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "glob-to-regex.js",
3
- "packageManager": "yarn@4.9.3",
3
+ "packageManager": "yarn@4.9.4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "version": "1.0.1",
7
+ "version": "1.2.0",
8
8
  "description": "Transform GLOB patterns to JavaScript regular expressions for fast file path matching.",
9
9
  "author": {
10
10
  "name": "streamich",