👁️
6
fork

Configure Feed

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

at dev 147 lines 3.5 kB view raw
1/** 2 * AST to predicate compiler for Scryfall search 3 * 4 * Compiles a SearchNode AST into a function that tests cards. 5 */ 6 7import { stripDiacritics } from "../normalize-text"; 8import { type CardPredicate, compileField } from "./fields"; 9import type { CompileError, Result, SearchNode } from "./types"; 10import { ok } from "./types"; 11 12// Re-export CardPredicate for convenience 13export type { CardPredicate }; 14 15/** 16 * Compile an AST node into a card predicate function 17 */ 18export function compile(node: SearchNode): Result<CardPredicate, CompileError> { 19 switch (node.type) { 20 case "AND": 21 return compileAnd(node.children); 22 23 case "OR": 24 return compileOr(node.children); 25 26 case "NOT": 27 return compileNot(node.child); 28 29 case "FIELD": 30 return compileField(node.field, node.operator, node.value, node.span); 31 32 case "NAME": 33 return ok(compileName(node.value, node.pattern)); 34 35 case "EXACT_NAME": 36 return ok(compileExactName(node.value)); 37 } 38} 39 40/** 41 * Compile AND node - all children must match 42 */ 43function compileAnd( 44 children: SearchNode[], 45): Result<CardPredicate, CompileError> { 46 const predicates: CardPredicate[] = []; 47 for (const child of children) { 48 const result = compile(child); 49 if (!result.ok) { 50 return result; 51 } 52 predicates.push(result.value); 53 } 54 return ok((card) => predicates.every((p) => p(card))); 55} 56 57/** 58 * Compile OR node - any child must match 59 */ 60function compileOr( 61 children: SearchNode[], 62): Result<CardPredicate, CompileError> { 63 const predicates: CardPredicate[] = []; 64 for (const child of children) { 65 const result = compile(child); 66 if (!result.ok) { 67 return result; 68 } 69 predicates.push(result.value); 70 } 71 return ok((card) => predicates.some((p) => p(card))); 72} 73 74/** 75 * Compile NOT node - child must not match 76 */ 77function compileNot(child: SearchNode): Result<CardPredicate, CompileError> { 78 const result = compile(child); 79 if (!result.ok) { 80 return result; 81 } 82 return ok((card) => !result.value(card)); 83} 84 85/** 86 * Normalize text for name comparison (lowercase + strip diacritics) 87 */ 88function normalizeName(text: string): string { 89 return stripDiacritics(text).toLowerCase(); 90} 91 92/** 93 * Compile name search - substring or regex match 94 */ 95function compileName(value: string, pattern: RegExp | null): CardPredicate { 96 if (pattern) { 97 return (card) => { 98 // Match against main name 99 if (pattern.test(card.name)) return true; 100 101 // Match against card face names for multi-face cards 102 if (card.card_faces) { 103 for (const face of card.card_faces) { 104 if (pattern.test(face.name)) return true; 105 } 106 } 107 108 return false; 109 }; 110 } 111 112 // Substring match (case-insensitive, diacritic-insensitive) 113 const normalized = normalizeName(value); 114 return (card) => { 115 // Match against main name 116 if (normalizeName(card.name).includes(normalized)) return true; 117 118 // Match against card face names for multi-face cards 119 if (card.card_faces) { 120 for (const face of card.card_faces) { 121 if (normalizeName(face.name).includes(normalized)) return true; 122 } 123 } 124 125 return false; 126 }; 127} 128 129/** 130 * Compile exact name match 131 */ 132function compileExactName(value: string): CardPredicate { 133 const normalized = normalizeName(value); 134 return (card) => { 135 // Match against main name exactly 136 if (normalizeName(card.name) === normalized) return true; 137 138 // Match against card face names for multi-face cards 139 if (card.card_faces) { 140 for (const face of card.card_faces) { 141 if (normalizeName(face.name) === normalized) return true; 142 } 143 } 144 145 return false; 146 }; 147}