glob-to-regex.js 1.1.0 → 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,6 +1,8 @@
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;
4
6
  }
5
7
  /**
6
8
  * Convert a glob pattern to a regular expression
@@ -12,6 +14,12 @@ export interface GlobOptions {
12
14
  * - `**` to match any number of path segments, including none
13
15
  * - `{}` to group conditions (e.g. `{html,txt}`)
14
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
15
23
  */
16
24
  export declare const toRegex: (pattern: string, options?: GlobOptions) => RegExp;
17
25
  /**
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,6 +74,12 @@ 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
84
  const toRegex = (pattern, options) => {
17
85
  let regexStr = '';
@@ -48,8 +116,21 @@ const toRegex = (pattern, options) => {
48
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 **
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.2.0",
8
8
  "description": "Transform GLOB patterns to JavaScript regular expressions for fast file path matching.",
9
9
  "author": {
10
10
  "name": "streamich",