query-filter
The language behind Query: free text and key:value qualifiers in one string, such as is:shared -staging api. The module is plain TypeScript with no React import, so a server, a script, or a test can parse and match a query with the same rules the field uses.
The language
Keys, values, and their aliases match without regard to case, and never fuzzily. A key the grammar doesn't know stays free text, so a pasted https://api.example.com searches for that URL. A value an enumerated key doesn't know matches no item.
Write a grammar
A grammar is one FilterGrammar<Item> object per list. Declare it as a module constant: the engine caches its alias tables per grammar object.
getSearchTextbuilds the text a free-text term searches. Join the fields with a newline, so a term can't match across two fields.- A matcher that compares a date against the clock reads
context.now, neverDate.now(). Pass{ now }from a ticking state hook, so a memoized filter re-runs as time passes. negatable: falseon a key removes its-key:suggestion and paints a typed-as invalid. A typed-key:valueon that key matches no items.
Filter items
filterItemsByQuery keeps the input order. An empty query returns the same array, so a memoized list keeps its identity.
countItemsByQuery(grammar, items, query) counts the matches without building the array. It caches up to 1,000 counts per grammar and items array, so the counts in the suggestions cost one scan for each query.
The engine caches the search text per item and per items array. Treat items as immutable: replace a changed item with a new object, and pass a new array, as React state already requires.
A query that a server runs
defineRemoteFilterGrammar declares the vocabulary of a query that another system runs. Nothing matches on the client. Its syntax sets what the highlighter paints as structure:
"qualifiers"(the default) reads only barekey:valuetokens. Quotes,-,OR, and commas stay text. A value with a space can't be written, sodefineRemoteFilterGrammardrops it, and drops an alias with a space too. Values often come from a server, so the grammar never throws on one."conjunction"reads one AND group of exclusions, comma lists, and quotes. A bare term paints as invalid, because the executor can't run it.
Parse a query
parseFilterQuery is total: it returns tokens for every input and never throws. A query is a hand-edited URL param, and the field re-parses it on every keystroke.
formatFacet({ key, value, negated })writes a qualifier in its canonical form, with quotes where the value needs them.toggleFacet({ grammar, query, facet })adds a qualifier, or takes it out when the query has it. The Filter from a value recipe uses it for a value a person selects in a table row.queryUsesQualifiers(grammar, query)reports whether a query holds a qualifier. Use it to pick the advice in a filtered-empty state.getFilterSuggestionsandgetHighlightSegmentsare whatQueryrenders. Use them to build your own field.
Natural language search
Two helpers connect the language to a model. Neither one calls a model: your server owns the request.
describeFilterGrammar(grammar)describes the keys, the allowed values, and the syntax rules as plain data. Send it to the model with the person's words.checkFilterQuery(grammar, query)returns the unknown keys, the unknown values, and the exclusions a key can't do. Check the model's query before you show it, so an invented filter never empties the list.
The natural language query recipe wires them to a Query field.