Accent-insensitive card search

A fold() (lowercase + strip accents, using the same table as nucleo's own
normalization) is registered as a SQLite function and used by the name,
oracle-text and location filters, so "khazad-dum" finds Troll of
Khazad-dûm. The fuzzy name search folds the query too (accented queries
used to match nothing) and its exact-name promotion; decklist/ManaBox
import falls back to a folded name match; the deck quick filter and tag
suggestions fold client-side.

Closes #17
This commit is contained in:
2026-10-23 23:05:19 -05:00
parent d4d19dcdb4
commit 945e5ed766
9 changed files with 163 additions and 32 deletions

View File

@@ -632,6 +632,12 @@ export const CardSearchBox = forwardRef<
);
});
/** Lowercase and strip accents, so "khazad-dum" matches "Khazad-dûm". The
* frontend twin of `mtgc_core::text::fold`, for matching done client-side. */
export function fold(s: string): string {
return s.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
}
/**
* A chip-based multi-value text filter with autocomplete: selected values
* render as removable chips, and typing filters `suggestions` into a
@@ -653,11 +659,11 @@ export function TagInput({
const [text, setText] = useState("");
const [sel, setSel] = useState(0);
const query = text.trim().toLowerCase();
const query = fold(text.trim());
const filtered = query
? suggestions
.filter((s) => !values.some((v) => v.toLowerCase() === s.toLowerCase()))
.filter((s) => s.toLowerCase().includes(query))
.filter((s) => fold(s).includes(query))
.slice(0, 8)
: [];

View File

@@ -37,6 +37,7 @@ import {
usePersistedState,
useTileColumns,
useViewMode,
fold,
} from "../components";
import { SuggestionsModal } from "../SuggestionsModal";
import { useBackButton } from "../backButton";
@@ -878,9 +879,9 @@ export function DeckView({ refreshSignal }: { refreshSignal?: number }) {
// filter only, distinct from the CardSearchBox boxes below (which search
// the library/reference db to find NEW cards to add).
function matchesFilter(e: DeckEntry): boolean {
const q = filterQuery.trim().toLowerCase();
const q = fold(filterQuery.trim());
if (!q) return true;
return e.name.toLowerCase().includes(q) || e.type_line.toLowerCase().includes(q);
return fold(e.name).includes(q) || fold(e.type_line).includes(q);
}
const owned = contents.filter((e) => !e.is_maybeboard && e.quantity_owned > 0 && matchesFilter(e));