-
-
Notifications
You must be signed in to change notification settings - Fork 31
Expand file tree
/
Copy pathindex.js
More file actions
166 lines (133 loc) · 4.31 KB
/
index.js
File metadata and controls
166 lines (133 loc) · 4.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
import escapeStringRegexp from 'escape-string-regexp';
const regexpCache = new Map();
const sanitizeArray = (input, inputName) => {
if (!Array.isArray(input)) {
switch (typeof input) {
case 'string': {
input = [input];
break;
}
case 'undefined': {
input = [];
break;
}
default: {
throw new TypeError(`Expected '${inputName}' to be a string or an array, but got a type of '${typeof input}'`);
}
}
}
return input.filter(string => {
if (typeof string !== 'string') {
if (string === undefined) {
return false;
}
throw new TypeError(`Expected '${inputName}' to be an array of strings, but found a type of '${typeof string}' in the array`);
}
return true;
});
};
const makeRegexp = (pattern, options) => {
options = {
caseSensitive: false,
...options,
};
const flags = 's' + (options.caseSensitive ? '' : 'i'); // Always dotAll, optionally case-insensitive
const cacheKey = pattern + '|' + flags;
if (regexpCache.has(cacheKey)) {
return regexpCache.get(cacheKey);
}
const negated = pattern[0] === '!';
if (negated) {
pattern = pattern.slice(1);
}
// Handle escapes: first preserve escaped chars, then convert * to wildcards
pattern = pattern
.replaceAll(String.raw`\*`, '__ESCAPED_STAR__') // \* -> placeholder
.replaceAll('\\\\', '__ESCAPED_BACKSLASH__') // \\ -> placeholder
.replaceAll(/\\(.)/g, '$1'); // Other escapes like \<space> -> <space>
pattern = escapeStringRegexp(pattern).replaceAll(String.raw`\*`, '.*'); // * -> .*
pattern = pattern
.replaceAll('__ESCAPED_STAR__', String.raw`\*`) // Restore escaped *
.replaceAll('__ESCAPED_BACKSLASH__', '\\\\'); // Restore escaped \
const regexp = new RegExp(`^${pattern}$`, flags);
regexp.negated = negated;
regexpCache.set(cacheKey, regexp);
return regexp;
};
const baseMatcher = (inputs, patterns, options, firstMatchOnly) => {
inputs = sanitizeArray(inputs, 'inputs');
patterns = sanitizeArray(patterns, 'patterns');
if (patterns.length === 0) {
return [];
}
patterns = patterns.map(pattern => makeRegexp(pattern, options));
// Partition patterns for faster processing
const negatedPatterns = patterns.filter(pattern => pattern.negated);
const positivePatterns = patterns.filter(pattern => !pattern.negated);
const {allPatterns} = options || {};
const result = [];
// Special handling for multiple negations with allPatterns and isMatch
if (allPatterns && firstMatchOnly && negatedPatterns.length > 1 && positivePatterns.length === 0) {
// Multiple negations only: ALL inputs must satisfy constraints (none should match any negation)
for (const input of inputs) {
for (const pattern of negatedPatterns) {
if (pattern.test(input)) {
return []; // Any input matching a negation means no match
}
}
}
return inputs.slice(0, 1); // All inputs passed negation constraints
}
for (const input of inputs) {
// Check negated patterns first (immediate exclusion)
let excludedByNegation = false;
for (const pattern of negatedPatterns) {
if (pattern.test(input)) {
excludedByNegation = true;
break;
}
}
if (excludedByNegation) {
continue; // Skip this input
}
// Check positive patterns
if (positivePatterns.length === 0) {
// No positive patterns - include if no negations matched (already checked above)
result.push(input);
} else if (allPatterns) {
// AND logic: include if ALL positive patterns match
const matchedPositive = Array.from({length: positivePatterns.length}, () => false);
for (const [index, pattern] of positivePatterns.entries()) {
if (pattern.test(input)) {
matchedPositive[index] = true;
}
}
// All positive patterns must match
if (matchedPositive.every(Boolean)) {
result.push(input);
}
} else {
// OR logic: include if any positive pattern matches
let matchedAny = false;
for (const pattern of positivePatterns) {
if (pattern.test(input)) {
matchedAny = true;
break; // Short-circuit on first match
}
}
if (matchedAny) {
result.push(input);
}
}
if (firstMatchOnly && result.length > 0) {
break;
}
}
return result;
};
export function matcher(inputs, patterns, options) {
return baseMatcher(inputs, patterns, options, false);
}
export function isMatch(inputs, patterns, options) {
return baseMatcher(inputs, patterns, options, true).length > 0;
}