Skip to content

Commit

Permalink
Merge pull request #2602 from KaelenProctor/feature/card-search-with-…
Browse files Browse the repository at this point in the history
…apostophe

Allow card search with apostrophe
  • Loading branch information
dekkerglen authored Feb 9, 2025
2 parents f854945 + 6e0915c commit a1ec7f4
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
uses: tj-actions/eslint-changed-files@v25
with:
config_path: 'eslint.config.mjs'
extra_args: '--max-warnings=0'
extra_args: '--max-warnings=0 --no-warn-ignored'
reporter: github-pr-review

- name: Run Prettier on changed files
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import tseslint from 'typescript-eslint';

export default [
{
ignores: ['dist/**/*', '.git/**/*', 'node_modules/**/*', 'src/generated/**/*', 'jobs/archived/*.js'],
ignores: ['dist/**/*', '.git/**/*', 'node_modules/**/*', 'src/client/generated/**/*', 'jobs/archived/*.js'],
},
js.configs.recommended,
...tseslint.configs.recommended,
Expand Down
2 changes: 1 addition & 1 deletion nearley/values.ne
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ noQuoteStringValue ->
| "and"i [^ \t\n"'\\=<>:]
| "o"i [^rR \t\n"'\\=<>:]
| "or"i [^ \t\n"'\\=<>:]
) [^ \t\n"'\\=<>:]:* {% ([startChars, chars]) => startChars.concat(chars).join('').toLowerCase() %}
) [^ \t\n"\\=<>:]:* {% ([startChars, chars]) => startChars.concat(chars).join('').toLowerCase() %}
# "

manaCostOpValue -> equalityOperator manaCostValue {% ([op, value]) => manaCostOperation(op, value) %}
Expand Down
4 changes: 3 additions & 1 deletion src/client/filtering/FilterCards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ export function defaultFilter(): FilterFunction {
return result as FilterFunction;
}

export function makeFilter(filterText: string): { err: any; filter: FilterFunction | null } {
export type FilterResult = { err: any; filter: FilterFunction | null };

export function makeFilter(filterText: string): FilterResult {
if (!filterText || filterText.trim() === '') {
return {
err: false,
Expand Down
2 changes: 1 addition & 1 deletion src/client/generated/filtering/cardFilters.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

131 changes: 131 additions & 0 deletions tests/cards/filtering.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { FilterResult, makeFilter } from '../../src/client/filtering/FilterCards';

describe('Filter syntax', () => {
const assertValidNameFilter = (result: FilterResult) => {
expect(result.err).toBeFalsy();
expect(result.filter).toBeInstanceOf(Function);
expect(result.filter?.fieldsUsed).toEqual(['name_lower']);
};

it('Empty filter is no filtering', async () => {
const result = makeFilter('');
expect(result.err).toBeFalsy();
expect(result.filter).toBeNull();

const result2 = makeFilter(' ');
expect(result2.err).toBeFalsy();
expect(result2.filter).toBeNull();
});

it('Default filter is card name', async () => {
assertValidNameFilter(makeFilter('Urza'));
assertValidNameFilter(makeFilter('"Armageddon"'));
assertValidNameFilter(makeFilter("'Goblin Welder'"));
});

it('Negative name filter', async () => {
assertValidNameFilter(makeFilter('-mox'));
assertValidNameFilter(makeFilter('-"Diamond"'));
assertValidNameFilter(makeFilter("-'Dockside Chef'"));
});

it('Explicit name filter', async () => {
assertValidNameFilter(makeFilter('name:Blood'));
assertValidNameFilter(makeFilter('name:"Caustic Bro"'));
assertValidNameFilter(makeFilter("name:'The Meathook Mass'"));
});

it('Short explicit name filter', async () => {
assertValidNameFilter(makeFilter('n:Master of Death'));
assertValidNameFilter(makeFilter('n:"Abrupt Decay"'));
assertValidNameFilter(makeFilter("n:'March of Otherworldly Light'"));
});

it('Exact name filter', async () => {
assertValidNameFilter(makeFilter('name=Bloodghast'));
assertValidNameFilter(makeFilter('name="Caustic Bronco"'));
assertValidNameFilter(makeFilter("name='The Meathook Massacre'"));
});

it('Not exact name filter', async () => {
assertValidNameFilter(makeFilter('name!=Bloodghast'));
assertValidNameFilter(makeFilter('name!="Caustic Bronco"'));
assertValidNameFilter(makeFilter("name!='The Meathook Massacre'"));

assertValidNameFilter(makeFilter('name<>Bloodghast'));
assertValidNameFilter(makeFilter('name<>"Caustic Bronco"'));
assertValidNameFilter(makeFilter("name<>'The Meathook Massacre'"));
});

it('Combination name filters', async () => {
assertValidNameFilter(makeFilter('Dragon or angel'));
assertValidNameFilter(makeFilter('dragon AND orb'));
assertValidNameFilter(makeFilter('-Dragon AND rage'));
assertValidNameFilter(makeFilter('-Dragon or rage'));
});

const workingNamesWithInterestingCharacters = [
'Chatterfang, Squirrel',
'Oni-Cult Anvil',
'Busted!',
'+2 mace',
'Mr. Orfeo, the Boulder',
'Borrowing 100,000 Arrows',
'TL;DR',
'Question Elemental?',
'_____ Goblin',
'The Ultimate Nightmare of Wizards of the Coast® Customer Service',
'Ratonhnhaké꞉ton',
];

it.each(workingNamesWithInterestingCharacters)('Working names with interesting characters (%s)', async (name) => {
assertValidNameFilter(makeFilter(name));
assertValidNameFilter(makeFilter(`'${name}'`));
assertValidNameFilter(makeFilter(`"${name}"`));
assertValidNameFilter(makeFilter(`name:'${name}'`));
assertValidNameFilter(makeFilter(`name:"${name}"`));
});

it('Partial working names with single quotes', async () => {
const name = "Urza's Bauble";
assertValidNameFilter(makeFilter(name));
assertValidNameFilter(makeFilter(`"${name}"`));
assertValidNameFilter(makeFilter(`name:"${name}"`));

//Cannot single-quote surround a name containing a single quote
const result = makeFilter(`'${name}'`);
expect(result.err).toBeTruthy();
const result2 = makeFilter(`name:'${name}'`);
expect(result2.err).toBeTruthy();

//But it does work if the quote is escaped
assertValidNameFilter(makeFilter(`'Urza\\'s Bauble'`));
assertValidNameFilter(makeFilter(`n:'Urza\\'s Bauble'`));
});

it('Partial working names with double quotes', async () => {
const name = 'Kongming, "Sleeping Dragon"';
assertValidNameFilter(makeFilter(name));
assertValidNameFilter(makeFilter(`'${name}'`));
assertValidNameFilter(makeFilter(`name:'${name}'`));

//Cannot double-quote surround a name containing a double quote
const result = makeFilter(`"${name}"`);
expect(result.err).toBeTruthy();
const result2 = makeFilter(`name:"${name}"`);
expect(result2.err).toBeTruthy();

//But it does work if the quote is escaped
assertValidNameFilter(makeFilter(`"Kongming, \\"Sleeping Dragon\\""`));
assertValidNameFilter(makeFilter(`n:'Urza\\'s Bauble'`));
});

const failingNamesWithInterestingCharacters = [
'Hazmat Suit (Used)', //The brackets get interpreted as another clause and we only want a single filter
];

it.each(failingNamesWithInterestingCharacters)('Failing names with interesting characters (%s)', async (name) => {
const result = makeFilter(name);
expect(result.err).toBeTruthy();
});
});

0 comments on commit a1ec7f4

Please sign in to comment.