๐Ÿ‘๏ธ
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

support bare regex in search

+30 -11
+9
src/lib/search/__tests__/describe.test.ts
··· 22 22 }); 23 23 24 24 it.each([ 25 + ["/goblin/", "name matches /goblin/"], 26 + ["/goblin.*king/i", "name matches /goblin.*king/"], 27 + ["/^Lightning/", "name matches /^Lightning/"], 28 + ["/^bolt$/m", "name matches /^bolt$/m"], 29 + ])("bare regex name search: `%s` โ†’ %s", (query, expected) => { 30 + expect(desc(query)).toBe(expected); 31 + }); 32 + 33 + it.each([ 25 34 ["t:creature", 'type includes "creature"'], 26 35 ["o:flying", 'oracle text includes "flying"'], 27 36 ["kw:trample", 'keyword includes "trample"'],
+7
src/lib/search/__tests__/operators.test.ts
··· 11 11 ["fire-breathing", "hyphenated names"], 12 12 ["The Ur-Dragon", "proper card name with hyphens"], 13 13 ["bolt And shock", "mixed case AND"], 14 + ["Fire // Ice", "split card name with double slashes"], 15 + ["Wear // Tear", "another split card"], 14 16 ])("`%s` โ†’ false (%s)", (query) => { 15 17 expect(hasSearchOperators(query)).toBe(false); 16 18 }); ··· 45 47 ["(red", "opening paren"], 46 48 ["red)", "closing paren"], 47 49 ["(red OR blue)", "grouped expression"], 50 + // Regex patterns 51 + ["/goblin/", "bare regex"], 52 + ["/goblin.*king/i", "regex with flags"], 53 + ["/^Lightning/", "regex with anchor"], 54 + ["o:/deals? \\d+ damage/", "regex in field value"], 48 55 ])("`%s` โ†’ true (%s)", (query) => { 49 56 expect(hasSearchOperators(query)).toBe(true); 50 57 });
+10 -5
src/lib/search/describe.ts
··· 179 179 return sorted.map((c) => `{${c}}`).join(""); 180 180 } 181 181 182 + function formatRegex(pattern: RegExp, source: string): string { 183 + const flags = pattern.flags.replace("i", ""); 184 + return `/${source}/${flags}`; 185 + } 186 + 182 187 function describeValue(value: FieldValue, quoted = true): string { 183 188 switch (value.kind) { 184 189 case "string": ··· 187 192 : value.value.toLowerCase(); 188 193 case "number": 189 194 return String(value.value); 190 - case "regex": { 191 - // Filter out 'i' since case-insensitive is the default 192 - const flags = value.pattern.flags.replace("i", ""); 193 - return `/${value.source}/${flags}`; 194 - } 195 + case "regex": 196 + return formatRegex(value.pattern, value.source); 195 197 case "colors": 196 198 return formatColors(value.colors); 197 199 } ··· 272 274 export function describeQuery(node: SearchNode): string { 273 275 switch (node.type) { 274 276 case "NAME": 277 + if (node.pattern) { 278 + return `name matches ${formatRegex(node.pattern, node.value)}`; 279 + } 275 280 return `name includes "${node.value.toLowerCase()}"`; 276 281 277 282 case "EXACT_NAME":
+4 -1
src/lib/search/operators.ts
··· 25 25 const NEGATION = /(^|[\s(])-\w/; 26 26 const QUOTES = /"/; 27 27 const PARENS = /[()]/; 28 + const REGEX_LITERAL = /(?:^|\s)\/.+\//; 28 29 29 30 /** 30 31 * Check if a query string contains search operators that indicate ··· 37 38 * - "-blue" (negation) 38 39 * - '"Lightning Bolt"' (quoted) 39 40 * - "(red OR blue)" (grouping) 41 + * - "/goblin.*king/" (regex pattern) 40 42 * 41 43 * Returns false for simple name searches like: 42 44 * - "lightning bolt" ··· 50 52 EXACT_MATCH.test(query) || 51 53 NEGATION.test(query) || 52 54 QUOTES.test(query) || 53 - PARENS.test(query) 55 + PARENS.test(query) || 56 + REGEX_LITERAL.test(query) 54 57 ); 55 58 }
-5
todos.md
··· 8 8 9 9 ## Bugs 10 10 11 - ### Bare regex for name search doesn't work 12 - - **Location**: `src/lib/search/parser.ts`, `parseNameExpr()` 13 - - **Issue**: `/goblin.*king/i` syntax is parsed but not matched correctly for bare name searches (works in field values like `o:/regex/`) 14 - - **Why it matters**: Documented in grammar but broken 15 - 16 11 ### Flaky property test for OR parsing 17 12 - **Location**: `src/lib/search/__tests__/parser.test.ts:276` ("parses OR combinations") 18 13 - **Issue**: fast-check property test occasionally finds edge cases that fail parsing