👁️
5
fork

Configure Feed

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

random bugfixes

+51 -8
+6
src/lib/search/__tests__/describe.test.ts
··· 212 212 ["is:mdfc", "modal double-faced cards"], 213 213 ["is:saga", "sagas"], 214 214 ["is:adventure", "adventure cards"], 215 + ["is:prepared", "cards with a prepared spell"], 216 + ["is:prepare", "cards with a prepared spell"], 217 + 218 + // Aliases resolve to canonical descriptions 219 + ["is:battleland", "battle lands (enter tapped unless ≥2 basics)"], 220 + ["is:creatureland", "creature lands (can become creatures)"], 215 221 216 222 // Printing characteristics 217 223 ["is:reserved", "reserved list cards"],
+30
src/lib/search/__tests__/lexer.test.ts
··· 146 146 { type: "EXACT_NAME", value: "Lightning Bolt" }, 147 147 ]); 148 148 }); 149 + 150 + it("tokenizes exact name with smart quotes", () => { 151 + expectTokens("!\u201CLightning Bolt\u201D", [ 152 + { type: "EXACT_NAME", value: "Lightning Bolt" }, 153 + ]); 154 + }); 155 + 156 + it("tokenizes exact name with mismatched smart quotes", () => { 157 + expectTokens("!\u201DLightning Bolt\u201C", [ 158 + { type: "EXACT_NAME", value: "Lightning Bolt" }, 159 + ]); 160 + }); 161 + }); 162 + 163 + describe("smart quotes", () => { 164 + it("tokenizes quoted string with left/right smart quotes", () => { 165 + expectTokens("t:\u201Ccreature\u201D", [ 166 + { type: "WORD", value: "t" }, 167 + { type: "COLON", value: ":" }, 168 + { type: "QUOTED", value: "creature" }, 169 + ]); 170 + }); 171 + 172 + it("tokenizes quoted string with right/right smart quotes", () => { 173 + expectTokens("o:\u201Dflying\u201D", [ 174 + { type: "WORD", value: "o" }, 175 + { type: "COLON", value: ":" }, 176 + { type: "QUOTED", value: "flying" }, 177 + ]); 178 + }); 149 179 }); 150 180 151 181 describe("complex queries", () => {
+3 -2
src/lib/search/describe.ts
··· 2 2 * Converts a parsed search AST to a human-readable description. 3 3 */ 4 4 5 - import { RARITY_ALIASES } from "./fields"; 5 + import { IS_PREDICATE_ALIASES, RARITY_ALIASES } from "./fields"; 6 6 import { 7 7 type ComparisonOp, 8 8 DISCRETE_FIELDS, ··· 245 245 node.value.kind === "string" 246 246 ) { 247 247 const predicate = node.value.value.toLowerCase(); 248 - const description = IS_DESCRIPTIONS[predicate]; 248 + const canonical = IS_PREDICATE_ALIASES[predicate] ?? predicate; 249 + const description = IS_DESCRIPTIONS[canonical]; 249 250 if (description) { 250 251 return node.field === "not" ? `not ${description}` : description; 251 252 }
+12 -6
src/lib/search/lexer.ts
··· 30 30 "<", 31 31 ">", 32 32 '"', 33 + "\u201C", 34 + "\u201D", 33 35 ]); 36 + 37 + function isQuoteChar(ch: string): boolean { 38 + return ch === '"' || ch === "\u201C" || ch === "\u201D"; 39 + } 34 40 35 41 // Track what token types can precede a regex 36 42 const REGEX_STARTERS = new Set<TokenType>([ ··· 100 106 101 107 function readQuoted(): Result<string> { 102 108 const start = pos; 103 - advance(); // consume opening " 109 + advance(); // consume opening quote (ASCII or smart) 104 110 let value = ""; 105 111 106 - while (pos < input.length && peek() !== '"') { 112 + while (pos < input.length && !isQuoteChar(peek())) { 107 113 if (peek() === "\\") { 108 114 advance(); 109 115 if (pos < input.length) { ··· 114 120 } 115 121 } 116 122 117 - if (peek() !== '"') { 123 + if (!isQuoteChar(peek())) { 118 124 return err(makeError("Unterminated quoted string", start)); 119 125 } 120 - advance(); // consume closing " 126 + advance(); // consume closing quote 121 127 return ok(value); 122 128 } 123 129 ··· 225 231 } 226 232 227 233 // Quoted string 228 - if (char === '"') { 234 + if (isQuoteChar(char)) { 229 235 const result = readQuoted(); 230 236 if (!result.ok) return result; 231 237 tokens.push(makeToken("QUOTED", result.value, start)); ··· 262 268 advance(); 263 269 skipWhitespace(); 264 270 // Read until end or quote 265 - if (peek() === '"') { 271 + if (isQuoteChar(peek())) { 266 272 const result = readQuoted(); 267 273 if (!result.ok) return result; 268 274 tokens.push(makeToken("EXACT_NAME", result.value, start));