glob-to-regex.js 1.1.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,34 +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`
69
+ - **Extended globbing** (when `extglob: true` option is set):
70
+ - `?(pattern-list)` matches zero or one occurrence of the given patterns
71
+ - `*(pattern-list)` matches zero or more occurrences of the given patterns
72
+ - `+(pattern-list)` matches one or more occurrences of the given patterns
73
+ - `@(pattern-list)` matches exactly 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
75
+ - Pattern lists use `|` as separator (e.g., `@(jpg|png|gif)`)
49
76
 
50
77
  Notes:
51
78
  - The produced RegExp is anchored at start and end (`^...$`).
52
- - Character classes are copied through to the output regex. Use standard JavaScript class syntax.
53
- - 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.
54
82
 
55
83
  ## Examples
56
84
 
@@ -63,9 +91,28 @@ toRegex('src/**/test.ts').test('src/a/b/test.ts'); // true
63
91
  toRegex('assets/**').test('assets/a/b.png'); // true
64
92
  toRegex('*.{html,txt}').test('page.html'); // true
65
93
  toRegex('src/{a,b}/**/*.ts').test('src/b/x/y.ts'); // true
94
+ toRegex('v{1..3}.tgz').test('v2.tgz'); // true
66
95
  toRegex('file[0-9].txt').test('file5.txt'); // true
67
96
  toRegex('file[!0-9].txt').test('filea.txt'); // true
97
+ toRegex('[[:upper:]]*.ts').test('App.ts'); // true
68
98
  toRegex('**/*.[jt]s{,x}').test('dir/a/b.jsx'); // true
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
+
106
+ // Extended globbing examples
107
+ toRegex('file?(s).txt', {extglob: true}).test('file.txt'); // true
108
+ toRegex('file?(s).txt', {extglob: true}).test('files.txt'); // true
109
+ toRegex('file.@(jpg|png|gif)', {extglob: true}).test('file.jpg'); // true
110
+ toRegex('/var/log/!(*.gz)', {extglob: true}).test('/var/log/syslog'); // true
111
+ toRegex('/var/log/!(*.gz)', {extglob: true}).test('/var/log/error.log.gz'); // false
112
+ toRegex('src/**/!(*.test).js', {extglob: true}).test('src/app.test.js'); // false
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
69
116
  ```
70
117
 
71
118
  ## TypeScript
@@ -78,10 +125,10 @@ Types are bundled. The library targets modern Node.js and browsers.
78
125
 
79
126
  ## Limitations
80
127
 
81
- - Brace groups are not nested.
82
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.
83
131
 
84
132
  ## License
85
133
 
86
134
  Apache-2.0 © streamich
87
-
package/lib/index.d.ts CHANGED
@@ -1,7 +1,19 @@
1
1
  export interface GlobOptions {
2
2
  /** Treat pattern as case insensitive */
3
3
  nocase?: boolean;
4
+ /** Enable extended globbing: ?(pattern), *(pattern), +(pattern), @(pattern), !(pattern) */
5
+ extglob?: boolean;
6
+ /** Whether `*`, `?`, `[...]` and extglobs match a leading `.` of a path segment. */
7
+ dot?: boolean;
4
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[];
5
17
  /**
6
18
  * Convert a glob pattern to a regular expression
7
19
  *
@@ -10,8 +22,14 @@ export interface GlobOptions {
10
22
  * - `*` to match zero or more characters in a path segment
11
23
  * - `?` to match one character in a path segment
12
24
  * - `**` to match any number of path segments, including none
13
- * - `{}` to group conditions (e.g. `{html,txt}`)
14
- * - `[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
27
+ * - Extended globbing (when `extglob: true` option is set):
28
+ * - `?(pattern-list)` zero or one occurrence
29
+ * - `*(pattern-list)` zero or more occurrences
30
+ * - `+(pattern-list)` one or more occurrences
31
+ * - `@(pattern-list)` exactly one of the patterns
32
+ * - `!(pattern-list)` anything except the patterns
15
33
  */
16
34
  export declare const toRegex: (pattern: string, options?: GlobOptions) => RegExp;
17
35
  /**
package/lib/index.js CHANGED
@@ -1,141 +1,429 @@
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
- * Convert a glob pattern to a regular expression
7
- *
8
- * Supports:
9
- * - `/` to separate path segments
10
- * - `*` to match zero or more characters in a path segment
11
- * - `?` to match one character in a path segment
12
- * - `**` to match any number of path segments, including none
13
- * - `{}` to group conditions (e.g. `{html,txt}`)
14
- * - `[abc]`, `[a-z]`, `[!a-z]`, `[!abc]` character classes
15
- */
16
- const toRegex = (pattern, options) => {
17
- let regexStr = '';
18
- let i = 0;
19
- // Helper to parse a brace group like {a,b,c}. No nesting support.
20
- const parseBraceGroup = () => {
21
- // Assume current char is '{'
22
- i++; // skip '{'
23
- const parts = [];
24
- let cur = '';
25
- let closed = false;
26
- while (i < pattern.length) {
27
- const ch = pattern[i];
28
- if (ch === '}') {
29
- parts.push(cur);
30
- i++; // consume '}'
31
- closed = true;
32
- break;
33
- }
34
- if (ch === ',') {
35
- parts.push(cur);
36
- cur = '';
37
- i++;
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;
38
104
  continue;
39
105
  }
40
- cur += ch;
41
- i++;
42
106
  }
43
- if (!closed) {
44
- // treat as literal '{...'
45
- return '\\{' + escapeRe(cur);
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;
46
114
  }
47
- // Convert each part recursively to support globs inside braces
48
- const alt = parts.map((p) => (0, exports.toRegex)(p, options).source.replace(/^\^/, '').replace(/\$$/, '')).join('|');
49
- return `(?:${alt})`;
50
- };
51
- while (i < pattern.length) {
52
- const char = pattern[i];
53
- switch (char) {
54
- case '*': {
55
- // Check for double star **
56
- if (pattern[i + 1] === '*') {
57
- // Collapse consecutive * beyond two (e.g., *** -> **)
58
- let j = i + 2;
59
- while (pattern[j] === '*')
60
- j++;
61
- // If followed by a slash, make it optional to allow zero segments
62
- if (pattern[j] === '/') {
63
- regexStr += '(?:.*/)?';
64
- i = j + 1; // consume **/
65
- }
66
- else {
67
- regexStr += '.*';
68
- i = j; // consume **
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 */)
136
+ depth++;
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;
153
+ }
154
+ else if (code === 40 /* Code.LParen */)
155
+ depth++;
156
+ else if (code === 41 /* Code.RParen */)
157
+ depth--;
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;
69
234
  }
235
+ else if (pattern.charCodeAt(k) === 47 /* Code.Slash */)
236
+ j = k;
237
+ else
238
+ break;
70
239
  }
71
- else {
72
- regexStr += '[^/]*';
73
- i++;
240
+ if (slash) {
241
+ out += nodot ? '(?:' + guard + GLOBSTAR_NODOT + '/)?' : '(?:.*/)?';
242
+ j++;
243
+ start = true;
74
244
  }
245
+ else
246
+ out += nodot ? guard + GLOBSTAR_NODOT : '.*';
247
+ i = j;
75
248
  break;
76
249
  }
77
- case '?':
78
- regexStr += '[^/]';
250
+ case 63 /* Code.Qmark */:
251
+ out += guard + '[^/]';
79
252
  i++;
80
253
  break;
81
- case '[': {
82
- // Copy character class as-is with support for leading '!'
83
- let cls = '[';
84
- i++;
85
- if (i < pattern.length && pattern[i] === '!') {
86
- cls += '^';
254
+ case 91 /* Code.LBracket */: {
255
+ const end = classEnd(pattern, i);
256
+ if (end < 0) {
257
+ out += '\\[';
87
258
  i++;
259
+ break;
88
260
  }
89
- // if first after [ or [^ is ']' include it literally
90
- if (i < pattern.length && pattern[i] === ']') {
91
- cls += ']';
92
- i++;
93
- }
94
- while (i < pattern.length && pattern[i] !== ']') {
95
- const ch = pattern[i];
96
- // Escape backslash inside class
97
- cls += ch === '\\' ? '\\\\' : ch;
98
- i++;
99
- }
100
- if (i < pattern.length && pattern[i] === ']') {
101
- cls += ']';
102
- i++;
103
- }
104
- else {
105
- // Unclosed class -> treat '[' literally
106
- regexStr += '\\[';
107
- continue;
108
- }
109
- regexStr += cls;
261
+ const cls = classSource(pattern, i, end);
262
+ out += (cls.charCodeAt(0) === 91 /* Code.LBracket */ ? guard : '') + cls;
263
+ i = end;
110
264
  break;
111
265
  }
112
- case '{': {
113
- regexStr += parseBraceGroup();
114
- break;
115
- }
116
- case '/':
117
- regexStr += '/';
118
- i++;
119
- break;
120
- case '.':
121
- case '^':
122
- case '$':
123
- case '+':
124
- case '(':
125
- case ')':
126
- case '|':
127
- case '\\':
128
- regexStr += `\\${char}`;
266
+ case 47 /* Code.Slash */:
267
+ out += '/';
129
268
  i++;
269
+ start = true;
130
270
  break;
131
271
  default:
132
- regexStr += char;
272
+ out += literal(pattern, i, code);
133
273
  i++;
134
- break;
135
274
  }
136
275
  }
137
- const flags = options?.nocase ? 'i' : '';
138
- return new RegExp('^' + regexStr + '$', flags);
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;
304
+ }
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;
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);
341
+ }
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;
367
+ }
368
+ out.push(pattern);
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;
383
+ /**
384
+ * Convert a glob pattern to a regular expression
385
+ *
386
+ * Supports:
387
+ * - `/` to separate path segments
388
+ * - `*` to match zero or more characters in a path segment
389
+ * - `?` to match one character in a path segment
390
+ * - `**` to match any number of path segments, including none
391
+ * - `{}` to group conditions (e.g. `{html,txt}`), nested, and `{1..3}` ranges
392
+ * - `[abc]`, `[a-z]`, `[!a-z]`, `[!abc]`, `[[:alpha:]]` character classes
393
+ * - Extended globbing (when `extglob: true` option is set):
394
+ * - `?(pattern-list)` zero or one occurrence
395
+ * - `*(pattern-list)` zero or more occurrences
396
+ * - `+(pattern-list)` one or more occurrences
397
+ * - `@(pattern-list)` exactly one of the patterns
398
+ * - `!(pattern-list)` anything except the patterns
399
+ */
400
+ const toRegex = (pattern, options) => {
401
+ const extglob = !!options?.extglob;
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))
417
+ continue;
418
+ if (seen.size)
419
+ source += '|';
420
+ seen.add(one);
421
+ source += compile(one, extglob, nodot, '', true);
422
+ }
423
+ source += ')';
424
+ }
425
+ }
426
+ return new RegExp('^' + source + '$', options?.nocase ? 'i' : '');
139
427
  };
140
428
  exports.toRegex = toRegex;
141
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.1.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",