glob-to-regex.js 1.2.0 → 1.3.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
@@ -2,7 +2,8 @@
2
2
 
3
3
  Transform GLOB patterns to JavaScript regular expressions for fast file path matching.
4
4
 
5
- This tiny library converts familiar shell-style glob patterns like `**/*.ts` or `src/{a,b}/**/*.js` into JavaScript `RegExp` objects and provides a convenient matcher utility.
5
+ This tiny library converts familiar shell-style glob patterns like `**/*.ts` or `src/{a,b}/**/*.js` into JavaScript `RegExp` objects and provides a convenient matcher utility. Matching semantics follow `minimatch`,
6
+ so the regexes can stand in for it segment by segment.
6
7
 
7
8
  ## Install
8
9
 
@@ -15,7 +16,7 @@ npm i glob-to-regex.js
15
16
  ## Quick start
16
17
 
17
18
  ```ts
18
- import {toRegex, toMatcher} from 'glob-to-regex.js';
19
+ import {toRegex, toMatcher, expandBraces} from 'glob-to-regex.js';
19
20
 
20
21
  // Build a RegExp from a glob
21
22
  const re = toRegex('src/**/test.ts');
@@ -23,41 +24,61 @@ re.test('src/a/b/test.ts'); // true
23
24
  re.test('src/test.ts'); // true
24
25
  re.test('src/test.tsx'); // false
25
26
 
27
+ // Shell rules for dotfiles and extended globs
28
+ const shell = toRegex('**/!(*.test).js', {dot: false, extglob: true});
29
+ shell.test('src/index.js'); // true
30
+ shell.test('src/app.test.js'); // false
31
+ shell.test('.git/hooks.js'); // false
32
+
26
33
  // Build a predicate function from a pattern or an array of patterns
27
- const match = toMatcher(['**/*.ts', '!**/*.d.ts']); // negative patterns are not special; use a RegExp if needed
28
- match('index.ts'); // true
29
- match('types.d.ts'); // true (negation is not parsed specially)
34
+ const match = toMatcher(['**/*.ts', '/\\.d\\.ts$/']);
35
+ match('index.ts'); // true
36
+ match('types.d.ts'); // true
37
+
38
+ expandBraces('src/{lib,test}/{1..3}.ts');
39
+ // ['src/lib/1.ts', 'src/lib/2.ts', ..., 'src/test/3.ts']
30
40
  ```
31
41
 
32
42
  ## API
33
43
 
34
- - toRegex(pattern: string): RegExp
44
+ - `toRegex(pattern: string, options?: GlobOptions): RegExp`
35
45
  - Converts a glob pattern to an anchored regular expression (`^...$`).
36
46
 
37
- - toMatcher(pattern: string | RegExp | Array<string | RegExp>): (path: string) => boolean
47
+ - `toMatcher(pattern: string | RegExp | Array<string | RegExp>, options?: GlobOptions): (path: string) => boolean`
38
48
  - Accepts a glob string, a RegExp, or an array of them. If given an array, it returns true if any item matches (logical OR, short-circuited).
39
49
  - Strings starting with `/` and ending with `/flags?` are treated as regular expressions (e.g. `"/\\.test\\.ts$/"`).
40
50
 
51
+ - `expandBraces(pattern: string, max?: number): string[]`
52
+ - Expands `{a,b}` alternations and `{1..3}` ranges into the list of patterns they stand for, nesting included. Stops after `max` patterns (default 100,000).
53
+
54
+ ### Options
55
+
56
+ - `nocase` — case-insensitive matching.
57
+ - `extglob` — enables the `?(...)`, `*(...)`, `+(...)`, `@(...)` and `!(...)` groups.
58
+ - `dot` — whether `*`, `?`, `[...]` and extglobs match a leading `.` of a path segment. Defaults to `true`; `false` is the shell rule, where a dotfile is matched only by a literal dot in the pattern (`.*` matches `.gitignore`, `*` does not) and `**` does not descend into dot directories.
59
+
41
60
  ## Supported glob features
42
61
 
43
62
  - `/` separates path segments
44
- - `*` matches zero or more characters within a single segment (does not cross `/`)
63
+ - `*` matches zero or more characters within a single segment (does not cross `/`). A segment that is only `*` matches at least one character, as a file name is never empty: `a/*` does not match `a/`
45
64
  - `?` matches exactly one character within a single segment
46
- - `**` matches across path segments, including none
47
- - `{a,b,c}` alternation groups (no nesting). Each item inside can itself contain glob syntax
48
- - Character classes: `[abc]`, `[a-z]`, `[!a-z]`, `[!abc]`
65
+ - `**` matches across path segments, including none. `**/**/` is the same as `**/`
66
+ - `{a,b,c}` alternation groups, nested as in `{a,{b,c}}`. Each item can itself contain glob syntax
67
+ - `{1..3}`, `{a..c}`, `{01..10..2}` ranges, with the step and zero padding of bash
68
+ - Character classes: `[abc]`, `[a-z]`, `[!a-z]`, `[^a-z]`, and POSIX classes such as `[[:alpha:]]` or `[a[:digit:]]`. A `]` right after the opening bracket is a member of the class: `[]a]` matches `]` or `a`
49
69
  - **Extended globbing** (when `extglob: true` option is set):
50
70
  - `?(pattern-list)` matches zero or one occurrence of the given patterns
51
71
  - `*(pattern-list)` matches zero or more occurrences of the given patterns
52
72
  - `+(pattern-list)` matches one or more occurrences of the given patterns
53
73
  - `@(pattern-list)` matches exactly one of the given patterns
54
- - `!(pattern-list)` matches anything except one of the given patterns
74
+ - `!(pattern-list)` matches anything except one of the given patterns. The exclusion covers the rest of the pattern, so `!(a).js` rejects `a.js` and accepts `ab.js`; `!()` matches any non-empty segment
55
75
  - Pattern lists use `|` as separator (e.g., `@(jpg|png|gif)`)
56
76
 
57
77
  Notes:
58
78
  - The produced RegExp is anchored at start and end (`^...$`).
59
- - Character classes are copied through to the output regex. Use standard JavaScript class syntax.
60
- - Brace groups are not nestable. If an unmatched `{` is found, it is treated literally.
79
+ - A brace group without a comma or range (`{a}`, `{}`) and an unmatched `{`, `[` or `(` are literal.
80
+ - A range with its ends reversed, like `[z-a]`, matches nothing instead of throwing.
81
+ - There is no escape character: a backslash is a literal backslash.
61
82
 
62
83
  ## Examples
63
84
 
@@ -70,10 +91,18 @@ toRegex('src/**/test.ts').test('src/a/b/test.ts'); // true
70
91
  toRegex('assets/**').test('assets/a/b.png'); // true
71
92
  toRegex('*.{html,txt}').test('page.html'); // true
72
93
  toRegex('src/{a,b}/**/*.ts').test('src/b/x/y.ts'); // true
94
+ toRegex('v{1..3}.tgz').test('v2.tgz'); // true
73
95
  toRegex('file[0-9].txt').test('file5.txt'); // true
74
96
  toRegex('file[!0-9].txt').test('filea.txt'); // true
97
+ toRegex('[[:upper:]]*.ts').test('App.ts'); // true
75
98
  toRegex('**/*.[jt]s{,x}').test('dir/a/b.jsx'); // true
76
99
 
100
+ // Dotfiles
101
+ toRegex('*.js').test('.eslintrc.js'); // true
102
+ toRegex('*.js', {dot: false}).test('.eslintrc.js'); // false
103
+ toRegex('.*', {dot: false}).test('.eslintrc.js'); // true
104
+ toRegex('**/*.js', {dot: false}).test('node_modules/.cache/a.js'); // false
105
+
77
106
  // Extended globbing examples
78
107
  toRegex('file?(s).txt', {extglob: true}).test('file.txt'); // true
79
108
  toRegex('file?(s).txt', {extglob: true}).test('files.txt'); // true
@@ -82,6 +111,8 @@ toRegex('/var/log/!(*.gz)', {extglob: true}).test('/var/log/syslog'); // true
82
111
  toRegex('/var/log/!(*.gz)', {extglob: true}).test('/var/log/error.log.gz'); // false
83
112
  toRegex('src/**/!(*.test).js', {extglob: true}).test('src/app.test.js'); // false
84
113
  toRegex('src/**/!(*.test).js', {extglob: true}).test('src/index.js'); // true
114
+ toRegex('!(a).js', {extglob: true}).test('ab.js'); // true
115
+ toRegex('!(a).js', {extglob: true}).test('a.js'); // false
85
116
  ```
86
117
 
87
118
  ## TypeScript
@@ -94,10 +125,10 @@ Types are bundled. The library targets modern Node.js and browsers.
94
125
 
95
126
  ## Limitations
96
127
 
97
- - Brace groups are not nested.
98
128
  - Negated globs like `!**/*.d.ts` are not parsed specially. If you need exclusion, combine multiple matchers or filter results separately.
129
+ - `**` is a globstar wherever it appears, not only as a whole segment: `a**` matches across `/`.
130
+ - Like minimatch, `!(a)*` does not match `a`: the negation rejects the whole remainder, while bash tries every split.
99
131
 
100
132
  ## License
101
133
 
102
134
  Apache-2.0 © streamich
103
-
package/lib/index.d.ts CHANGED
@@ -3,7 +3,17 @@ export interface GlobOptions {
3
3
  nocase?: boolean;
4
4
  /** Enable extended globbing: ?(pattern), *(pattern), +(pattern), @(pattern), !(pattern) */
5
5
  extglob?: boolean;
6
+ /** Whether `*`, `?`, `[...]` and extglobs match a leading `.` of a path segment. */
7
+ dot?: boolean;
6
8
  }
9
+ /**
10
+ * Expands `{a,b}` alternations and `{1..3}`, `{a..c}`, `{01..10..2}` ranges the
11
+ * way bash does, nesting included. A group with neither a comma nor a range,
12
+ * or without its closing brace, is kept as it is.
13
+ *
14
+ * @param max Number of expansions to stop at, as `brace-expansion` does.
15
+ */
16
+ export declare const expandBraces: (pattern: string, max?: number) => string[];
7
17
  /**
8
18
  * Convert a glob pattern to a regular expression
9
19
  *
@@ -12,8 +22,8 @@ export interface GlobOptions {
12
22
  * - `*` to match zero or more characters in a path segment
13
23
  * - `?` to match one character in a path segment
14
24
  * - `**` to match any number of path segments, including none
15
- * - `{}` to group conditions (e.g. `{html,txt}`)
16
- * - `[abc]`, `[a-z]`, `[!a-z]`, `[!abc]` character classes
25
+ * - `{}` to group conditions (e.g. `{html,txt}`), nested, and `{1..3}` ranges
26
+ * - `[abc]`, `[a-z]`, `[!a-z]`, `[!abc]`, `[[:alpha:]]` character classes
17
27
  * - Extended globbing (when `extglob: true` option is set):
18
28
  * - `?(pattern-list)` zero or one occurrence
19
29
  * - `*(pattern-list)` zero or more occurrences
package/lib/index.js CHANGED
@@ -1,69 +1,385 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.toMatcher = exports.toRegex = void 0;
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 === '(') {
3
+ exports.toMatcher = exports.toRegex = exports.expandBraces = void 0;
4
+ const NODOT = '(?!\\.)';
5
+ const STAR = '[^/]*';
6
+ const GLOBSTAR_NODOT = '[^/]*(?:/(?!\\.)[^/]*)*';
7
+ const literal = (pattern, i, code) => {
8
+ switch (code) {
9
+ case 36 /* Code.Dollar */:
10
+ case 40 /* Code.LParen */:
11
+ case 41 /* Code.RParen */:
12
+ case 43 /* Code.Plus */:
13
+ case 46 /* Code.Dot */:
14
+ case 92 /* Code.Backslash */:
15
+ case 93 /* Code.RBracket */:
16
+ case 94 /* Code.Caret */:
17
+ case 123 /* Code.LBrace */:
18
+ case 124 /* Code.Pipe */:
19
+ case 125 /* Code.RBrace */:
20
+ return '\\' + pattern[i];
21
+ default:
22
+ return pattern[i];
23
+ }
24
+ };
25
+ const posixClass = (name) => {
26
+ switch (name) {
27
+ case 'alnum':
28
+ return '0-9A-Za-z';
29
+ case 'alpha':
30
+ return 'A-Za-z';
31
+ case 'ascii':
32
+ return '\\x00-\\x7f';
33
+ case 'blank':
34
+ return ' \\t';
35
+ case 'cntrl':
36
+ return '\\x00-\\x1f\\x7f';
37
+ case 'digit':
38
+ return '0-9';
39
+ case 'graph':
40
+ return '\\x21-\\x7e';
41
+ case 'lower':
42
+ return 'a-z';
43
+ case 'print':
44
+ return '\\x20-\\x7e';
45
+ case 'punct':
46
+ return '\\x21-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\x7e';
47
+ case 'space':
48
+ return ' \\t\\r\\n\\v\\f';
49
+ case 'upper':
50
+ return 'A-Z';
51
+ case 'word':
52
+ return '0-9A-Za-z_';
53
+ case 'xdigit':
54
+ return '0-9A-Fa-f';
55
+ }
56
+ return;
57
+ };
58
+ const classEnd = (pattern, i) => {
59
+ const length = pattern.length;
60
+ let j = i + 1;
61
+ const first = pattern.charCodeAt(j);
62
+ if (first === 33 /* Code.Bang */ || first === 94 /* Code.Caret */)
63
+ j++;
64
+ if (pattern.charCodeAt(j) === 93 /* Code.RBracket */)
65
+ j++;
66
+ for (; j < length; j++) {
67
+ const code = pattern.charCodeAt(j);
68
+ if (code === 93 /* Code.RBracket */)
69
+ return j + 1;
70
+ if (code === 91 /* Code.LBracket */ && pattern.charCodeAt(j + 1) === 58 /* Code.Colon */) {
71
+ const close = pattern.indexOf(':]', j + 2);
72
+ if (close > 0)
73
+ j = close + 1;
74
+ }
75
+ }
76
+ return -1;
77
+ };
78
+ const member = (pattern, i) => {
79
+ const code = pattern.charCodeAt(i);
80
+ const special = code === 93 /* Code.RBracket */ ||
81
+ code === 92 /* Code.Backslash */ ||
82
+ code === 91 /* Code.LBracket */ ||
83
+ code === 94 /* Code.Caret */ ||
84
+ code === 45 /* Code.Minus */;
85
+ return (special ? '\\' : '') + pattern[i];
86
+ };
87
+ const classSource = (pattern, i, end) => {
88
+ const last = end - 1;
89
+ let j = i + 1;
90
+ let out = '';
91
+ let members = 0;
92
+ const first = pattern.charCodeAt(j);
93
+ const negate = first === 33 /* Code.Bang */ || first === 94 /* Code.Caret */;
94
+ if (negate)
95
+ j++;
96
+ while (j < last) {
97
+ if (pattern.charCodeAt(j) === 91 /* Code.LBracket */ && pattern.charCodeAt(j + 1) === 58 /* Code.Colon */) {
98
+ const close = pattern.indexOf(':]', j + 2);
99
+ const cls = close > 0 ? posixClass(pattern.slice(j + 2, close)) : undefined;
100
+ if (cls !== undefined) {
101
+ out += cls;
102
+ members += 2;
103
+ j = close + 2;
104
+ continue;
105
+ }
106
+ }
107
+ if (pattern.charCodeAt(j + 1) === 45 /* Code.Minus */ && j + 2 < last) {
108
+ if (pattern.charCodeAt(j + 2) >= pattern.charCodeAt(j)) {
109
+ out += member(pattern, j) + '-' + member(pattern, j + 2);
110
+ members += 2;
111
+ }
112
+ j += 3;
113
+ continue;
114
+ }
115
+ out += member(pattern, j);
116
+ members++;
117
+ j++;
118
+ }
119
+ if (!negate && members === 1)
120
+ return literal(pattern, last - 1, pattern.charCodeAt(last - 1));
121
+ if (!members)
122
+ return negate ? '[^/]' : '[]';
123
+ return (negate ? '[^' : '[') + out + ']';
124
+ };
125
+ const groupEnd = (pattern, i) => {
126
+ const length = pattern.length;
127
+ let depth = 1;
128
+ for (; i < length; i++) {
129
+ const code = pattern.charCodeAt(i);
130
+ if (code === 91 /* Code.LBracket */) {
131
+ const end = classEnd(pattern, i);
132
+ if (end > 0)
133
+ i = end - 1;
134
+ }
135
+ else if (code === 40 /* Code.LParen */)
17
136
  depth++;
18
- cur += ch;
19
- i++;
137
+ else if (code === 41 /* Code.RParen */ && !--depth)
138
+ return i + 1;
139
+ }
140
+ return -1;
141
+ };
142
+ const splitAlts = (body) => {
143
+ const length = body.length;
144
+ const alts = [];
145
+ let depth = 0;
146
+ let from = 0;
147
+ for (let i = 0; i < length; i++) {
148
+ const code = body.charCodeAt(i);
149
+ if (code === 91 /* Code.LBracket */) {
150
+ const end = classEnd(body, i);
151
+ if (end > 0)
152
+ i = end - 1;
20
153
  }
21
- else if (ch === ')') {
154
+ else if (code === 40 /* Code.LParen */)
155
+ depth++;
156
+ else if (code === 41 /* Code.RParen */)
22
157
  depth--;
23
- if (depth === 0) {
24
- // Found the closing parenthesis
25
- parts.push(cur);
26
- i++; // consume ')'
158
+ else if (code === 124 /* Code.Pipe */ && !depth) {
159
+ alts.push(body.slice(from, i));
160
+ from = i + 1;
161
+ }
162
+ }
163
+ alts.push(body.slice(from));
164
+ return alts;
165
+ };
166
+ const isExtglobPrefix = (code) => code === 63 /* Code.Qmark */ || code === 42 /* Code.Star */ || code === 43 /* Code.Plus */ || code === 64 /* Code.At */ || code === 33 /* Code.Bang */;
167
+ const altsSource = (alts, extglob, nodot, tail, segStart) => {
168
+ let out = '';
169
+ for (let i = 0; i < alts.length; i++)
170
+ out += (i ? '|' : '') + compile(alts[i], extglob, nodot, tail, segStart);
171
+ return out;
172
+ };
173
+ const extglobSource = (type, alts, extglob, nodot, tail, segStart) => {
174
+ const guard = nodot && segStart ? NODOT : '';
175
+ const body = altsSource(alts, extglob, nodot, tail, segStart);
176
+ switch (type) {
177
+ case 63 /* Code.Qmark */:
178
+ return '(?:' + body + ')?';
179
+ case 64 /* Code.At */:
180
+ return '(?:' + body + ')';
181
+ case 33 /* Code.Bang */:
182
+ if (alts.length === 1 && !alts[0])
183
+ return guard + '[^/]+';
184
+ return '(?!(?:' + body + ')' + tail + '$)' + guard + STAR;
185
+ }
186
+ const more = guard ? altsSource(alts, extglob, nodot, tail, false) : body;
187
+ if (more === body)
188
+ return '(?:' + body + ')' + (type === 42 /* Code.Star */ ? '*' : '+');
189
+ const once = '(?:' + body + ')(?:' + more + ')*';
190
+ return type === 42 /* Code.Star */ ? '(?:' + once + ')?' : once;
191
+ };
192
+ const compile = (pattern, extglob, nodot, tail, segStart) => {
193
+ const length = pattern.length;
194
+ let out = '';
195
+ let i = 0;
196
+ let start = segStart;
197
+ while (i < length) {
198
+ const code = pattern.charCodeAt(i);
199
+ if (extglob && pattern.charCodeAt(i + 1) === 40 /* Code.LParen */ && isExtglobPrefix(code)) {
200
+ const end = groupEnd(pattern, i + 2);
201
+ if (end > 0) {
202
+ const after = compile(pattern.slice(end), extglob, nodot, tail, false);
203
+ const alts = splitAlts(pattern.slice(i + 2, end - 1));
204
+ return out + extglobSource(code, alts, extglob, nodot, after + tail, start) + after;
205
+ }
206
+ }
207
+ const atStart = start;
208
+ const guard = nodot && atStart ? NODOT : '';
209
+ start = false;
210
+ switch (code) {
211
+ case 42 /* Code.Star */: {
212
+ let j = i + 1;
213
+ while (pattern.charCodeAt(j) === 42 /* Code.Star */)
214
+ j++;
215
+ if (j === i + 1) {
216
+ // a whole segment of `*` needs a character: a file name is never empty
217
+ const whole = atStart && (j === length || pattern.charCodeAt(j) === 47 /* Code.Slash */);
218
+ out += guard + (whole ? '[^/]+' : STAR);
219
+ i = j;
220
+ break;
221
+ }
222
+ // `**/**/` is `**/`, and `**/**` is `**`: a second globstar only multiplies the backtracking
223
+ let slash = pattern.charCodeAt(j) === 47 /* Code.Slash */;
224
+ while (slash) {
225
+ let k = j + 1;
226
+ if (pattern.charCodeAt(k) !== 42 /* Code.Star */ || pattern.charCodeAt(k + 1) !== 42 /* Code.Star */)
227
+ break;
228
+ k += 2;
229
+ while (pattern.charCodeAt(k) === 42 /* Code.Star */)
230
+ k++;
231
+ if (k === length) {
232
+ j = k;
233
+ slash = false;
234
+ }
235
+ else if (pattern.charCodeAt(k) === 47 /* Code.Slash */)
236
+ j = k;
237
+ else
238
+ break;
239
+ }
240
+ if (slash) {
241
+ out += nodot ? '(?:' + guard + GLOBSTAR_NODOT + '/)?' : '(?:.*/)?';
242
+ j++;
243
+ start = true;
244
+ }
245
+ else
246
+ out += nodot ? guard + GLOBSTAR_NODOT : '.*';
247
+ i = j;
27
248
  break;
28
249
  }
29
- else {
30
- cur += ch;
250
+ case 63 /* Code.Qmark */:
251
+ out += guard + '[^/]';
31
252
  i++;
253
+ break;
254
+ case 91 /* Code.LBracket */: {
255
+ const end = classEnd(pattern, i);
256
+ if (end < 0) {
257
+ out += '\\[';
258
+ i++;
259
+ break;
260
+ }
261
+ const cls = classSource(pattern, i, end);
262
+ out += (cls.charCodeAt(0) === 91 /* Code.LBracket */ ? guard : '') + cls;
263
+ i = end;
264
+ break;
32
265
  }
266
+ case 47 /* Code.Slash */:
267
+ out += '/';
268
+ i++;
269
+ start = true;
270
+ break;
271
+ default:
272
+ out += literal(pattern, i, code);
273
+ i++;
33
274
  }
34
- else if (ch === '|' && depth === 1) {
35
- // Pipe separator at top level of this extglob
36
- parts.push(cur);
37
- cur = '';
38
- i++;
275
+ }
276
+ return out;
277
+ };
278
+ const closingBrace = (pattern, open) => {
279
+ const length = pattern.length;
280
+ let depth = 0;
281
+ for (let i = open; i < length; i++) {
282
+ const code = pattern.charCodeAt(i);
283
+ if (code === 123 /* Code.LBrace */)
284
+ depth++;
285
+ else if (code === 125 /* Code.RBrace */ && !--depth)
286
+ return i;
287
+ }
288
+ return -1;
289
+ };
290
+ const commaSplit = (body) => {
291
+ const length = body.length;
292
+ const parts = [];
293
+ let depth = 0;
294
+ let from = 0;
295
+ for (let i = 0; i < length; i++) {
296
+ const code = body.charCodeAt(i);
297
+ if (code === 123 /* Code.LBrace */)
298
+ depth++;
299
+ else if (code === 125 /* Code.RBrace */)
300
+ depth--;
301
+ else if (code === 44 /* Code.Comma */ && !depth) {
302
+ parts.push(body.slice(from, i));
303
+ from = i + 1;
39
304
  }
40
- else {
41
- cur += ch;
42
- i++;
305
+ }
306
+ if (!parts.length)
307
+ return;
308
+ parts.push(body.slice(from));
309
+ return parts;
310
+ };
311
+ const NUMERIC_RANGE = /^(-?\d+)\.\.(-?\d+)(?:\.\.(-?\d+))?$/;
312
+ const ALPHA_RANGE = /^([a-zA-Z])\.\.([a-zA-Z])(?:\.\.(-?\d+))?$/;
313
+ const PADDED = /^-?0\d/;
314
+ const rangeOf = (body, max) => {
315
+ let match = NUMERIC_RANGE.exec(body);
316
+ const alpha = !match;
317
+ if (alpha)
318
+ match = ALPHA_RANGE.exec(body);
319
+ if (!match)
320
+ return;
321
+ const from = alpha ? match[1].charCodeAt(0) : +match[1];
322
+ const to = alpha ? match[2].charCodeAt(0) : +match[2];
323
+ const step = match[3] === undefined ? 1 : Math.abs(+match[3]) || 1;
324
+ const width = !alpha && (PADDED.test(match[1]) || PADDED.test(match[2])) ? Math.max(match[1].length, match[2].length) : 0;
325
+ const out = [];
326
+ const dir = from <= to ? step : -step;
327
+ for (let n = from; (dir > 0 ? n <= to : n >= to) && out.length < max; n += dir) {
328
+ if (alpha) {
329
+ out.push(String.fromCharCode(n));
330
+ continue;
43
331
  }
332
+ let s = String(n);
333
+ const need = width - s.length;
334
+ if (need > 0) {
335
+ let zeros = '';
336
+ for (let k = 0; k < need; k++)
337
+ zeros += '0';
338
+ s = n < 0 ? '-' + zeros + s.slice(1) : zeros + s;
339
+ }
340
+ out.push(s);
44
341
  }
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];
342
+ return out;
343
+ };
344
+ const expandInto = (pattern, out, max) => {
345
+ for (let open = pattern.indexOf('{'); open >= 0; open = pattern.indexOf('{', open + 1)) {
346
+ const close = closingBrace(pattern, open);
347
+ if (close < 0)
348
+ continue;
349
+ const body = pattern.slice(open + 1, close);
350
+ const alts = commaSplit(body) || rangeOf(body, max);
351
+ if (!alts)
352
+ continue;
353
+ const prefix = pattern.slice(0, open);
354
+ const suffixes = (0, exports.expandBraces)(pattern.slice(close + 1), max);
355
+ for (let i = 0; i < alts.length; i++) {
356
+ const heads = (0, exports.expandBraces)(alts[i], max);
357
+ for (let j = 0; j < heads.length; j++) {
358
+ const head = prefix + heads[j];
359
+ for (let k = 0; k < suffixes.length; k++) {
360
+ if (out.length >= max)
361
+ return;
362
+ out.push(head + suffixes[k]);
363
+ }
364
+ }
365
+ }
366
+ return;
64
367
  }
65
- return;
368
+ out.push(pattern);
66
369
  };
370
+ /**
371
+ * Expands `{a,b}` alternations and `{1..3}`, `{a..c}`, `{01..10..2}` ranges the
372
+ * way bash does, nesting included. A group with neither a comma nor a range,
373
+ * or without its closing brace, is kept as it is.
374
+ *
375
+ * @param max Number of expansions to stop at, as `brace-expansion` does.
376
+ */
377
+ const expandBraces = (pattern, max = 100000) => {
378
+ const out = [];
379
+ expandInto(pattern, out, max);
380
+ return out;
381
+ };
382
+ exports.expandBraces = expandBraces;
67
383
  /**
68
384
  * Convert a glob pattern to a regular expression
69
385
  *
@@ -72,8 +388,8 @@ const parseExtGlob = (pattern, startIdx, prefix, options) => {
72
388
  * - `*` to match zero or more characters in a path segment
73
389
  * - `?` to match one character in a path segment
74
390
  * - `**` to match any number of path segments, including none
75
- * - `{}` to group conditions (e.g. `{html,txt}`)
76
- * - `[abc]`, `[a-z]`, `[!a-z]`, `[!abc]` character classes
391
+ * - `{}` to group conditions (e.g. `{html,txt}`), nested, and `{1..3}` ranges
392
+ * - `[abc]`, `[a-z]`, `[!a-z]`, `[!abc]`, `[[:alpha:]]` character classes
77
393
  * - Extended globbing (when `extglob: true` option is set):
78
394
  * - `?(pattern-list)` zero or one occurrence
79
395
  * - `*(pattern-list)` zero or more occurrences
@@ -82,141 +398,32 @@ const parseExtGlob = (pattern, startIdx, prefix, options) => {
82
398
  * - `!(pattern-list)` anything except the patterns
83
399
  */
84
400
  const toRegex = (pattern, options) => {
85
- let regexStr = '';
86
- let i = 0;
87
- // Helper to parse a brace group like {a,b,c}. No nesting support.
88
- const parseBraceGroup = () => {
89
- // Assume current char is '{'
90
- i++; // skip '{'
91
- const parts = [];
92
- let cur = '';
93
- let closed = false;
94
- while (i < pattern.length) {
95
- const ch = pattern[i];
96
- if (ch === '}') {
97
- parts.push(cur);
98
- i++; // consume '}'
99
- closed = true;
100
- break;
101
- }
102
- if (ch === ',') {
103
- parts.push(cur);
104
- cur = '';
105
- i++;
106
- continue;
107
- }
108
- cur += ch;
109
- i++;
110
- }
111
- if (!closed) {
112
- // treat as literal '{...'
113
- return '\\{' + escapeRe(cur);
114
- }
115
- // Convert each part recursively to support globs inside braces
116
- const alt = parts.map((p) => (0, exports.toRegex)(p, options).source.replace(/^\^/, '').replace(/\$$/, '')).join('|');
117
- return `(?:${alt})`;
118
- };
119
401
  const extglob = !!options?.extglob;
120
- while (i < pattern.length) {
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
- }
134
- switch (char) {
135
- case '*': {
136
- // Check for double star **
137
- if (pattern[i + 1] === '*') {
138
- // Collapse consecutive * beyond two (e.g., *** -> **)
139
- let j = i + 2;
140
- while (pattern[j] === '*')
141
- j++;
142
- // If followed by a slash, make it optional to allow zero segments
143
- if (pattern[j] === '/') {
144
- regexStr += '(?:.*/)?';
145
- i = j + 1; // consume **/
146
- }
147
- else {
148
- regexStr += '.*';
149
- i = j; // consume **
150
- }
151
- }
152
- else {
153
- regexStr += '[^/]*';
154
- i++;
155
- }
156
- break;
157
- }
158
- case '?':
159
- regexStr += '[^/]';
160
- i++;
161
- break;
162
- case '[': {
163
- // Copy character class as-is with support for leading '!'
164
- let cls = '[';
165
- i++;
166
- if (i < pattern.length && pattern[i] === '!') {
167
- cls += '^';
168
- i++;
169
- }
170
- // if first after [ or [^ is ']' include it literally
171
- if (i < pattern.length && pattern[i] === ']') {
172
- cls += ']';
173
- i++;
174
- }
175
- while (i < pattern.length && pattern[i] !== ']') {
176
- const ch = pattern[i];
177
- // Escape backslash inside class
178
- cls += ch === '\\' ? '\\\\' : ch;
179
- i++;
180
- }
181
- if (i < pattern.length && pattern[i] === ']') {
182
- cls += ']';
183
- i++;
184
- }
185
- else {
186
- // Unclosed class -> treat '[' literally
187
- regexStr += '\\[';
402
+ const nodot = options?.dot === false;
403
+ let source;
404
+ if (pattern.indexOf('{') < 0)
405
+ source = compile(pattern, extglob, nodot, '', true);
406
+ else {
407
+ const set = (0, exports.expandBraces)(pattern);
408
+ const length = set.length;
409
+ if (length === 1)
410
+ source = compile(set[0], extglob, nodot, '', true);
411
+ else {
412
+ const seen = new Set();
413
+ source = '(?:';
414
+ for (let i = 0; i < length; i++) {
415
+ const one = set[i];
416
+ if (seen.has(one))
188
417
  continue;
189
- }
190
- regexStr += cls;
191
- break;
418
+ if (seen.size)
419
+ source += '|';
420
+ seen.add(one);
421
+ source += compile(one, extglob, nodot, '', true);
192
422
  }
193
- case '{': {
194
- regexStr += parseBraceGroup();
195
- break;
196
- }
197
- case '/':
198
- regexStr += '/';
199
- i++;
200
- break;
201
- case '.':
202
- case '^':
203
- case '$':
204
- case '+':
205
- case '(':
206
- case ')':
207
- case '|':
208
- case '\\':
209
- regexStr += `\\${char}`;
210
- i++;
211
- break;
212
- default:
213
- regexStr += char;
214
- i++;
215
- break;
423
+ source += ')';
216
424
  }
217
425
  }
218
- const flags = options?.nocase ? 'i' : '';
219
- return new RegExp('^' + regexStr + '$', flags);
426
+ return new RegExp('^' + source + '$', options?.nocase ? 'i' : '');
220
427
  };
221
428
  exports.toRegex = toRegex;
222
429
  const isRegExp = /^\/(.{1,4096})\/([gimsuy]{0,6})$/;
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "version": "1.2.0",
7
+ "version": "1.3.0",
8
8
  "description": "Transform GLOB patterns to JavaScript regular expressions for fast file path matching.",
9
9
  "author": {
10
10
  "name": "streamich",