-
-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
25 changed files
with
1,225 additions
and
901 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { defaultMatcher } from './query/matcher'; | ||
import { generateLexArray } from './query/lex'; | ||
import { parseTokens } from './query/parse'; | ||
import { parseTerm } from './query/term'; | ||
|
||
function parseWithDefaultMatcher(term: string, fuzz: number) { | ||
return parseTerm(term, fuzz, defaultMatcher); | ||
} | ||
|
||
function parseSearch(query: string) { | ||
const tokens = generateLexArray(query, parseWithDefaultMatcher); | ||
return parseTokens(tokens); | ||
} | ||
|
||
export default parseSearch; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import { makeDateMatcher } from '../date'; | ||
|
||
function daysAgo(days: number) { | ||
return new Date(Date.now() - days * 86400000).toISOString(); | ||
} | ||
|
||
describe('Date parsing', () => { | ||
it('should match relative dates (upper bound)', () => { | ||
const matcher = makeDateMatcher('3 days ago', 'lte'); | ||
|
||
expect(matcher(daysAgo(4), 'created_at', 0)).toBe(true); | ||
expect(matcher(daysAgo(2), 'created_at', 0)).toBe(false); | ||
}); | ||
|
||
it('should match relative dates (lower bound)', () => { | ||
const matcher = makeDateMatcher('3 days ago', 'gte'); | ||
|
||
expect(matcher(daysAgo(4), 'created_at', 0)).toBe(false); | ||
expect(matcher(daysAgo(2), 'created_at', 0)).toBe(true); | ||
}); | ||
|
||
it('should match absolute date ranges', () => { | ||
const ltMatcher = makeDateMatcher('2025', 'lt'); | ||
const gtMatcher = makeDateMatcher('2023', 'gt'); | ||
|
||
expect(ltMatcher(new Date(Date.UTC(2025, 5, 21)).toISOString(), 'created_at', 0)).toBe(false); | ||
expect(ltMatcher(new Date(Date.UTC(2024, 5, 21)).toISOString(), 'created_at', 0)).toBe(true); | ||
expect(ltMatcher(new Date(Date.UTC(2023, 5, 21)).toISOString(), 'created_at', 0)).toBe(true); | ||
|
||
expect(gtMatcher(new Date(Date.UTC(2025, 5, 21)).toISOString(), 'created_at', 0)).toBe(true); | ||
expect(gtMatcher(new Date(Date.UTC(2024, 5, 21)).toISOString(), 'created_at', 0)).toBe(true); | ||
expect(gtMatcher(new Date(Date.UTC(2023, 5, 21)).toISOString(), 'created_at', 0)).toBe(false); | ||
}); | ||
|
||
it('should match absolute dates through years', () => { | ||
const matcher = makeDateMatcher('2024', 'eq'); | ||
|
||
expect(matcher(new Date(Date.UTC(2025, 5, 21)).toISOString(), 'created_at', 0)).toBe(false); | ||
expect(matcher(new Date(Date.UTC(2024, 5, 21)).toISOString(), 'created_at', 0)).toBe(true); | ||
expect(matcher(new Date(Date.UTC(2023, 5, 21)).toISOString(), 'created_at', 0)).toBe(false); | ||
}); | ||
|
||
it('should match absolute dates through months', () => { | ||
const matcher = makeDateMatcher('2024-06', 'eq'); | ||
|
||
expect(matcher(new Date(Date.UTC(2024, 6, 21)).toISOString(), 'created_at', 0)).toBe(false); | ||
expect(matcher(new Date(Date.UTC(2024, 5, 21)).toISOString(), 'created_at', 0)).toBe(true); | ||
expect(matcher(new Date(Date.UTC(2024, 4, 21)).toISOString(), 'created_at', 0)).toBe(false); | ||
}); | ||
|
||
it('should match absolute dates through days', () => { | ||
const matcher = makeDateMatcher('2024-06-21', 'eq'); | ||
|
||
expect(matcher(new Date(Date.UTC(2024, 5, 22)).toISOString(), 'created_at', 0)).toBe(false); | ||
expect(matcher(new Date(Date.UTC(2024, 5, 21)).toISOString(), 'created_at', 0)).toBe(true); | ||
expect(matcher(new Date(Date.UTC(2024, 5, 20)).toISOString(), 'created_at', 0)).toBe(false); | ||
}); | ||
|
||
it('should match absolute dates through hours', () => { | ||
const matcher = makeDateMatcher('2024-06-21T06', 'eq'); | ||
|
||
expect(matcher(new Date(Date.UTC(2024, 5, 21, 7)).toISOString(), 'created_at', 0)).toBe(false); | ||
expect(matcher(new Date(Date.UTC(2024, 5, 21, 6)).toISOString(), 'created_at', 0)).toBe(true); | ||
expect(matcher(new Date(Date.UTC(2024, 5, 21, 5)).toISOString(), 'created_at', 0)).toBe(false); | ||
}); | ||
|
||
it('should match absolute dates through minutes', () => { | ||
const matcher = makeDateMatcher('2024-06-21T06:21', 'eq'); | ||
|
||
expect(matcher(new Date(Date.UTC(2024, 5, 21, 6, 22)).toISOString(), 'created_at', 0)).toBe(false); | ||
expect(matcher(new Date(Date.UTC(2024, 5, 21, 6, 21)).toISOString(), 'created_at', 0)).toBe(true); | ||
expect(matcher(new Date(Date.UTC(2024, 5, 21, 6, 20)).toISOString(), 'created_at', 0)).toBe(false); | ||
}); | ||
|
||
it('should match absolute dates through seconds', () => { | ||
const matcher = makeDateMatcher('2024-06-21T06:21:30Z', 'eq'); | ||
|
||
expect(matcher(new Date(Date.UTC(2024, 5, 21, 6, 21, 31)).toISOString(), 'created_at', 0)).toBe(false); | ||
expect(matcher(new Date(Date.UTC(2024, 5, 21, 6, 21, 30)).toISOString(), 'created_at', 0)).toBe(true); | ||
expect(matcher(new Date(Date.UTC(2024, 5, 21, 6, 21, 29)).toISOString(), 'created_at', 0)).toBe(false); | ||
}); | ||
|
||
it('should match absolute dates through seconds with positive timezone offset', () => { | ||
const matcher = makeDateMatcher('2024-06-21T06:21:30+01:30', 'eq'); | ||
|
||
expect(matcher(new Date(Date.UTC(2024, 5, 21, 4, 51, 31)).toISOString(), 'created_at', 0)).toBe(false); | ||
expect(matcher(new Date(Date.UTC(2024, 5, 21, 4, 51, 30)).toISOString(), 'created_at', 0)).toBe(true); | ||
expect(matcher(new Date(Date.UTC(2024, 5, 21, 4, 51, 29)).toISOString(), 'created_at', 0)).toBe(false); | ||
}); | ||
|
||
it('should match absolute dates through seconds with negative timezone offset', () => { | ||
const matcher = makeDateMatcher('2024-06-21T06:21:30-01:30', 'eq'); | ||
|
||
expect(matcher(new Date(Date.UTC(2024, 5, 21, 7, 51, 31)).toISOString(), 'created_at', 0)).toBe(false); | ||
expect(matcher(new Date(Date.UTC(2024, 5, 21, 7, 51, 30)).toISOString(), 'created_at', 0)).toBe(true); | ||
expect(matcher(new Date(Date.UTC(2024, 5, 21, 7, 51, 29)).toISOString(), 'created_at', 0)).toBe(false); | ||
}); | ||
|
||
it('should not match malformed absolute date expressions', () => { | ||
expect(() => makeDateMatcher('2024-06-21T06:21:30+01:3020', 'eq')).toThrow('Cannot parse date string: 2024-06-21T06:21:30+01:3020'); | ||
}); | ||
|
||
it('should not match malformed relative date expressions', () => { | ||
expect(() => makeDateMatcher('3 test failures ago', 'eq')).toThrow('Cannot parse date string: 3 test failures ago'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { makeLiteralMatcher } from '../literal'; | ||
|
||
describe('Literal field parsing', () => { | ||
it('should handle exact matching in arrayed fields', () => { | ||
const matcher = makeLiteralMatcher('safe', 0, false); | ||
expect(matcher('safe, solo', 'tags', 0)).toBe(true); | ||
expect(matcher('solo', 'tags', 0)).toBe(false); | ||
}); | ||
|
||
it('should handle exact matching in non-arrayed fields', () => { | ||
const matcher = makeLiteralMatcher('safe', 0, false); | ||
expect(matcher('safe, solo', 'description', 0)).toBe(false); | ||
expect(matcher('safe', 'description', 0)).toBe(true); | ||
expect(matcher('solo', 'description', 0)).toBe(false); | ||
}); | ||
|
||
it('should handle fuzzy matching based on normalized edit distance', () => { | ||
const matcher = makeLiteralMatcher('fluttersho', 0.8, false); | ||
expect(matcher('fluttershy', 'tags', 0)).toBe(true); | ||
expect(matcher('rarity', 'tags', 0)).toBe(false); | ||
}); | ||
|
||
it('should handle fuzzy matching based on raw edit distance', () => { | ||
const matcher = makeLiteralMatcher('fluttersho', 1, false); | ||
expect(matcher('fluttershy', 'tags', 0)).toBe(true); | ||
expect(matcher('rarity', 'tags', 0)).toBe(false); | ||
}); | ||
|
||
it('should handle wildcard matching', () => { | ||
const matcher = makeLiteralMatcher('fl?tter*', 0, true); | ||
expect(matcher('fluttershy', 'tags', 0)).toBe(true); | ||
expect(matcher('flitter', 'tags', 0)).toBe(true); | ||
expect(matcher('rainbow dash', 'tags', 0)).toBe(false); | ||
expect(matcher('gentle flutter', 'tags', 0)).toBe(false); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { makeNumberMatcher } from '../number'; | ||
|
||
describe('Number parsing', () => { | ||
it('should match numbers directly', () => { | ||
const intMatch = makeNumberMatcher(2067, 0, 'eq'); | ||
|
||
expect(intMatch('2066', 'value', 0)).toBe(false); | ||
expect(intMatch('2067', 'value', 0)).toBe(true); | ||
expect(intMatch('2068', 'value', 0)).toBe(false); | ||
expect(intMatch('20677', 'value', 0)).toBe(false); | ||
}); | ||
|
||
it('should match number ranges', () => { | ||
const ltMatch = makeNumberMatcher(2067, 0, 'lt'); | ||
const lteMatch = makeNumberMatcher(2067, 0, 'lte'); | ||
const gtMatch = makeNumberMatcher(2067, 0, 'gt'); | ||
const gteMatch = makeNumberMatcher(2067, 0, 'gte'); | ||
|
||
expect(ltMatch('2066', 'value', 0)).toBe(true); | ||
expect(ltMatch('2067', 'value', 0)).toBe(false); | ||
expect(ltMatch('2068', 'value', 0)).toBe(false); | ||
expect(lteMatch('2066', 'value', 0)).toBe(true); | ||
expect(lteMatch('2067', 'value', 0)).toBe(true); | ||
expect(lteMatch('2068', 'value', 0)).toBe(false); | ||
expect(gtMatch('2066', 'value', 0)).toBe(false); | ||
expect(gtMatch('2067', 'value', 0)).toBe(false); | ||
expect(gtMatch('2068', 'value', 0)).toBe(true); | ||
expect(gteMatch('2066', 'value', 0)).toBe(false); | ||
expect(gteMatch('2067', 'value', 0)).toBe(true); | ||
expect(gteMatch('2068', 'value', 0)).toBe(true); | ||
}); | ||
|
||
it('should not match unparsed values', () => { | ||
const matcher = makeNumberMatcher(2067, 0, 'eq'); | ||
|
||
expect(matcher('NaN', 'value', 0)).toBe(false); | ||
expect(matcher('test', 'value', 0)).toBe(false); | ||
}); | ||
|
||
it('should interpret fuzz as an inclusive range around the value', () => { | ||
const matcher = makeNumberMatcher(2067, 3, 'eq'); | ||
|
||
expect(matcher('2063', 'value', 0)).toBe(false); | ||
expect(matcher('2064', 'value', 0)).toBe(true); | ||
expect(matcher('2065', 'value', 0)).toBe(true); | ||
expect(matcher('2066', 'value', 0)).toBe(true); | ||
expect(matcher('2067', 'value', 0)).toBe(true); | ||
expect(matcher('2068', 'value', 0)).toBe(true); | ||
expect(matcher('2069', 'value', 0)).toBe(true); | ||
expect(matcher('2070', 'value', 0)).toBe(true); | ||
expect(matcher('2071', 'value', 0)).toBe(false); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
import { parseTerm } from '../term'; | ||
import { MatcherFactory, defaultMatcher } from '../matcher'; | ||
import { termSpaceToImageField } from '../fields'; | ||
|
||
function noMatch() { | ||
return false; | ||
} | ||
|
||
class TestMatcherFactory implements MatcherFactory { | ||
public dateVals: string[]; | ||
public literalVals: string[]; | ||
public numberVals: number[]; | ||
public userVals: string[]; | ||
|
||
constructor() { | ||
this.dateVals = []; | ||
this.literalVals = []; | ||
this.numberVals = []; | ||
this.userVals = []; | ||
} | ||
|
||
makeDateMatcher(term: string) { | ||
this.dateVals.push(term); | ||
return noMatch; | ||
} | ||
|
||
makeLiteralMatcher(term: string) { | ||
this.literalVals.push(term); | ||
return noMatch; | ||
} | ||
|
||
makeNumberMatcher(term: number) { | ||
this.numberVals.push(term); | ||
return noMatch; | ||
} | ||
|
||
makeUserMatcher(term: string) { | ||
this.userVals.push(term); | ||
return noMatch; | ||
} | ||
} | ||
|
||
describe('Search terms', () => { | ||
let factory: TestMatcherFactory; | ||
|
||
beforeEach(() => { | ||
factory = new TestMatcherFactory(); | ||
}); | ||
|
||
it('should parse the default field', () => { | ||
parseTerm('default', 0, factory); | ||
expect(factory.literalVals).toEqual(['default']); | ||
}); | ||
|
||
it('should parse the default field with wildcarding', () => { | ||
parseTerm('def?ul*', 0, factory); | ||
expect(factory.literalVals).toEqual(['def?ul*']); | ||
}); | ||
|
||
it('should parse the default field with fuzzing', () => { | ||
parseTerm('default', 1, factory); | ||
expect(factory.literalVals).toEqual(['default']); | ||
}); | ||
|
||
it('should parse the default field within quotes', () => { | ||
parseTerm('"default"', 0, factory); | ||
expect(factory.literalVals).toEqual(['default']); | ||
}); | ||
|
||
it('should parse exact date field values', () => { | ||
parseTerm('created_at:2024', 0, factory); | ||
expect(factory.dateVals).toEqual(['2024']); | ||
}); | ||
|
||
it('should parse ranged date field values', () => { | ||
parseTerm('created_at.lte:2024', 0, factory); | ||
parseTerm('created_at.lt:2024', 0, factory); | ||
parseTerm('created_at.gte:2024', 0, factory); | ||
parseTerm('created_at.gt:2024', 0, factory); | ||
expect(factory.dateVals).toEqual(['2024', '2024', '2024', '2024']); | ||
}); | ||
|
||
it('should parse exact number field values', () => { | ||
parseTerm('width:1920', 0, factory); | ||
expect(factory.numberVals).toEqual([1920]); | ||
}); | ||
|
||
it('should parse ranged number field values', () => { | ||
parseTerm('width.lte:1920', 0, factory); | ||
parseTerm('width.lt:1920', 0, factory); | ||
parseTerm('width.gte:1920', 0, factory); | ||
parseTerm('width.gt:1920', 0, factory); | ||
expect(factory.numberVals).toEqual([1920, 1920, 1920, 1920]); | ||
}); | ||
|
||
it('should parse literal field values', () => { | ||
parseTerm('source_url:*twitter*', 0, factory); | ||
expect(factory.literalVals).toEqual(['*twitter*']); | ||
}); | ||
|
||
it('should parse user field values', () => { | ||
parseTerm('my:upvotes', 0, factory); | ||
parseTerm('my:downvotes', 0, factory); | ||
parseTerm('my:faves', 0, factory); | ||
expect(factory.userVals).toEqual(['upvotes', 'downvotes', 'faves']); | ||
}); | ||
|
||
it('should match document with proper field values', () => { | ||
const idMatcher = parseTerm('id.lt:1', 0, defaultMatcher); | ||
const sourceMatcher = parseTerm('source_url:twitter.com', 0, defaultMatcher); | ||
|
||
const idAttribute = termSpaceToImageField.id; | ||
const sourceUrlAttribute = termSpaceToImageField.source_url; | ||
|
||
const properElement = document.createElement('div'); | ||
properElement.setAttribute(idAttribute, '0'); | ||
properElement.setAttribute(sourceUrlAttribute, 'twitter.com'); | ||
|
||
expect(idMatcher(properElement)).toBe(true); | ||
expect(sourceMatcher(properElement)).toBe(true); | ||
}); | ||
|
||
it('should not match document without field values', () => { | ||
const idMatcher = parseTerm('id.lt:1', 0, defaultMatcher); | ||
const sourceMatcher = parseTerm('source_url:twitter.com', 0, defaultMatcher); | ||
const improperElement = document.createElement('div'); | ||
|
||
expect(idMatcher(improperElement)).toBe(false); | ||
expect(sourceMatcher(improperElement)).toBe(false); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { makeUserMatcher } from '../user'; | ||
|
||
describe('User field parsing', () => { | ||
beforeEach(() => { | ||
/* eslint-disable camelcase */ | ||
window.booru.interactions = [ | ||
{image_id: 0, user_id: 0, interaction_type: 'faved', value: null}, | ||
{image_id: 0, user_id: 0, interaction_type: 'voted', value: 'up'}, | ||
{image_id: 1, user_id: 0, interaction_type: 'voted', value: 'down'}, | ||
{image_id: 2, user_id: 0, interaction_type: 'hidden', value: null}, | ||
]; | ||
/* eslint-enable camelcase */ | ||
}); | ||
|
||
it('should parse my:faves', () => { | ||
const matcher = makeUserMatcher('faves'); | ||
|
||
expect(matcher('', 'my', 0)).toBe(true); | ||
expect(matcher('', 'my', 1)).toBe(false); | ||
expect(matcher('', 'my', 2)).toBe(false); | ||
}); | ||
|
||
it('should parse my:upvotes', () => { | ||
const matcher = makeUserMatcher('upvotes'); | ||
|
||
expect(matcher('', 'my', 0)).toBe(true); | ||
expect(matcher('', 'my', 1)).toBe(false); | ||
expect(matcher('', 'my', 2)).toBe(false); | ||
}); | ||
|
||
it('should parse my:downvotes', () => { | ||
const matcher = makeUserMatcher('downvotes'); | ||
|
||
expect(matcher('', 'my', 0)).toBe(false); | ||
expect(matcher('', 'my', 1)).toBe(true); | ||
expect(matcher('', 'my', 2)).toBe(false); | ||
}); | ||
|
||
it('should not parse other my: fields', () => { | ||
const hiddenMatcher = makeUserMatcher('hidden'); | ||
const watchedMatcher = makeUserMatcher('watched'); | ||
|
||
expect(hiddenMatcher('', 'my', 0)).toBe(false); | ||
expect(hiddenMatcher('', 'my', 1)).toBe(false); | ||
expect(hiddenMatcher('', 'my', 2)).toBe(false); | ||
expect(watchedMatcher('', 'my', 0)).toBe(false); | ||
expect(watchedMatcher('', 'my', 1)).toBe(false); | ||
expect(watchedMatcher('', 'my', 2)).toBe(false); | ||
}); | ||
}); |
Oops, something went wrong.