-
Notifications
You must be signed in to change notification settings - Fork 40
Restore Market Filter with Custom Expression Parser #403
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
47b0317
replace filtrex with custom expression compiler
GODrums c62d824
add comments to parser
GODrums 53e505a
Merge branch 'feat/market-pattern-enhancements' into feat/expression-…
GODrums 6736e91
reimplement card highlighting subscriber
GODrums d42d64c
Merge branch 'feat/market-pattern-enhancements' into feat/expression-…
GODrums 86a7466
fix: replace MHN with market ID
GODrums a7325b1
Merge branch 'master' into feat/expression-ast-parser
GODrums d05d7bf
add minimal Vitest setup
GODrums bafdfa1
add tests for AST parser
GODrums e8b068d
preserve original card styling
GODrums 4eb72e3
throw on empty list input
GODrums File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or 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
This file contains hidden or 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
This file contains hidden or 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,61 @@ | ||
| import {css, html, nothing, HTMLTemplateResult} from 'lit'; | ||
| import {CustomElement, InjectBefore, InjectionMode} from '../../injectors'; | ||
| import {FloatElement} from '../../custom'; | ||
| import {isReactSteamMarket} from '../mode'; | ||
|
|
||
| import '../../filter/filter_container'; | ||
|
|
||
| /** | ||
| * Steam Market Beta equivalent of {@link UtilityBelt}. | ||
| */ | ||
| @CustomElement() | ||
| @InjectBefore( | ||
| 'div:has(> [style*="grid-columns:repeat(auto-fill, minmax(260px"])', | ||
| InjectionMode.CONTINUOUS, | ||
| isReactSteamMarket | ||
| ) | ||
| export class ReactFilterPanel extends FloatElement { | ||
| static styles = [ | ||
| ...FloatElement.styles, | ||
| css` | ||
| .panel { | ||
| margin-bottom: 16px; | ||
| padding: 16px; | ||
| background-color: rgba(0, 0, 0, 0.25); | ||
| border: 1px solid rgba(255, 255, 255, 0.06); | ||
| border-radius: 8px; | ||
| color: #c7d5e0; | ||
| font-family: 'Motiva Sans', sans-serif; | ||
| } | ||
|
|
||
| .panel-title { | ||
| font-family: 'Motiva Sans', sans-serif; | ||
| font-size: 14px; | ||
| font-weight: 600; | ||
| letter-spacing: 0.04em; | ||
| text-transform: uppercase; | ||
| color: #ebebeb; | ||
| margin: 0 0 12px; | ||
| } | ||
| `, | ||
| ]; | ||
|
|
||
| private get key(): string { | ||
| return getSteamMarketID() || ''; | ||
| } | ||
|
|
||
| protected render(): HTMLTemplateResult | typeof nothing { | ||
| if (!this.key) return nothing; | ||
| return html` | ||
| <div class="panel"> | ||
| <h3 class="panel-title">CSFloat Filters</h3> | ||
| <csfloat-filter-container .key="${this.key}"></csfloat-filter-container> | ||
| </div> | ||
| `; | ||
| } | ||
| } | ||
|
|
||
| /** Example: G18FD03209F033003 */ | ||
| function getSteamMarketID(): string | undefined { | ||
| return location.pathname.split('/').pop(); | ||
| } | ||
This file contains hidden or 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,55 @@ | ||
| import {nothing} from 'lit'; | ||
| import {property} from 'lit/decorators.js'; | ||
| import type {Subscription} from 'rxjs'; | ||
| import {CustomElement, InjectIntoScope} from '../../injectors'; | ||
| import {FloatElement} from '../../custom'; | ||
| import {gFilterService} from '../../../services/filter'; | ||
| import {pickTextColour} from '../../../utils/colours'; | ||
| import {ReactMarketListingScope, type ReactListingContext} from './listing'; | ||
|
|
||
| @CustomElement() | ||
| @InjectIntoScope(ReactMarketListingScope, { | ||
| anchor: ({scope}) => scope, | ||
| }) | ||
| export class ReactListingHighlight extends FloatElement { | ||
| @property({attribute: false}) injectionContext?: ReactListingContext; | ||
|
|
||
| private filterSubscription?: Subscription; | ||
|
|
||
| private originalStyle: {backgroundColor: string; color: string} = {backgroundColor: '', color: ''}; | ||
|
|
||
| connectedCallback(): void { | ||
| super.connectedCallback(); | ||
| const card = this.parentElement; | ||
| if (card) { | ||
| this.originalStyle = {backgroundColor: card.style.backgroundColor, color: card.style.color}; | ||
| } | ||
| this.filterSubscription = gFilterService.onUpdate$.subscribe(() => this.applyColour()); | ||
| } | ||
|
|
||
| disconnectedCallback(): void { | ||
| super.disconnectedCallback(); | ||
| this.filterSubscription?.unsubscribe(); | ||
| this.filterSubscription = undefined; | ||
| } | ||
|
|
||
| private get convertedPrice(): number | undefined { | ||
| const listing = this.injectionContext?.listing; | ||
| if (!listing?.unPrice) return undefined; | ||
| return (listing.unPrice + listing.unFee) / 100; | ||
| } | ||
|
|
||
| private applyColour(): void { | ||
| const card = this.parentElement; | ||
| const context = this.injectionContext; | ||
| if (!card || !context) return; | ||
|
|
||
| const colour = gFilterService.matchColour(context.itemInfo, this.convertedPrice); | ||
| card.style.backgroundColor = colour ?? this.originalStyle.backgroundColor; | ||
| card.style.color = colour ? pickTextColour(colour, '#8F98A0', '#484848') : this.originalStyle.color; | ||
| } | ||
|
|
||
| protected render() { | ||
| return nothing; | ||
| } | ||
| } |
This file contains hidden or 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,135 @@ | ||
| import {describe, expect, it} from 'vitest'; | ||
| import {compileExpression, type FilterValue} from './compile_expression'; | ||
| import type {InternalInputVars} from './types'; | ||
|
|
||
| const VARS: InternalInputVars = { | ||
| float: 0.2, | ||
| seed: 555, | ||
| minfloat: 0, | ||
| maxfloat: 1, | ||
| minwearfloat: 0, | ||
| maxwearfloat: 1, | ||
| phase: 'Ruby', | ||
| low_rank: 10, | ||
| high_rank: 90, | ||
| price: 100, | ||
| pattern: 387, | ||
| }; | ||
|
|
||
| /** Compile and run in one step against {@link VARS} (or an override). */ | ||
| function run(expression: string, vars: Partial<InternalInputVars> = {}): FilterValue { | ||
| return compileExpression(expression)({...VARS, ...vars}); | ||
| } | ||
|
|
||
| describe('literals and variables', () => { | ||
| it('reads numbers, strings, and booleans', () => { | ||
| expect(run('42')).toBe(42); | ||
| expect(run('.5')).toBe(0.5); | ||
| expect(run('1e3')).toBe(1000); | ||
| expect(run("'hello'")).toBe('hello'); | ||
| expect(run('true')).toBe(true); | ||
| expect(run('false')).toBe(false); | ||
| }); | ||
|
|
||
| it('resolves variables', () => { | ||
| expect(run('float')).toBe(0.2); | ||
| expect(run('phase')).toBe('Ruby'); | ||
| }); | ||
|
|
||
| it('returns an error for unknown/undefined variables', () => { | ||
| expect(run('nope')).toBeInstanceOf(Error); | ||
| expect(run('price', {price: undefined})).toBeInstanceOf(Error); | ||
| }); | ||
| }); | ||
|
|
||
| describe('arithmetic', () => { | ||
| it('respects precedence and associativity', () => { | ||
| expect(run('1 + 2 * 3')).toBe(7); | ||
| expect(run('(1 + 2) * 3')).toBe(9); | ||
| expect(run('2 ^ 3 ^ 2')).toBe(512); // right-associative | ||
| expect(run('-2 ^ 2')).toBe(4); // unary binds tighter than ^ | ||
| expect(run('7 % 3')).toBe(1); | ||
| }); | ||
|
|
||
| it('overloads + for string concatenation', () => { | ||
| expect(run("'a' + 'b'")).toBe('ab'); | ||
| expect(run("'x' + 1")).toBe('x1'); | ||
| }); | ||
|
|
||
| it('rejects arithmetic on non-numbers', () => { | ||
| expect(run("'a' - 1")).toBeInstanceOf(Error); | ||
| }); | ||
| }); | ||
|
|
||
| describe('comparisons and equality', () => { | ||
| it('compares numbers and strings', () => { | ||
| expect(run('float < 0.5')).toBe(true); | ||
| expect(run('float >= 0.2')).toBe(true); | ||
| expect(run("'a' < 'b'")).toBe(true); | ||
| }); | ||
|
|
||
| it('uses strict equality with no coercion', () => { | ||
| expect(run('5 == 5')).toBe(true); | ||
| expect(run("5 == '5'")).toBe(false); | ||
| expect(run('5 != 6')).toBe(true); | ||
| expect(run('5 = 5')).toBe(true); // = is an alias for == | ||
| }); | ||
|
|
||
| it('errors when comparing across types', () => { | ||
| expect(run("1 < 'a'")).toBeInstanceOf(Error); | ||
| }); | ||
| }); | ||
|
|
||
| describe('logical operators', () => { | ||
| it('evaluates and/or/not', () => { | ||
| expect(run('true and false')).toBe(false); | ||
| expect(run('true or false')).toBe(true); | ||
| expect(run('not false')).toBe(true); | ||
| }); | ||
|
|
||
| it('short-circuits so the right side is not required to be valid', () => { | ||
| expect(run('false and nope')).toBe(false); | ||
| expect(run('true or nope')).toBe(true); | ||
| }); | ||
| }); | ||
|
|
||
| describe('membership', () => { | ||
| it('handles in and not in', () => { | ||
| expect(run('seed in (1, 555, 999)')).toBe(true); | ||
| expect(run('seed not in (1, 2, 3)')).toBe(true); | ||
| expect(run("phase in ('Emerald', 'Ruby')")).toBe(true); | ||
| }); | ||
|
|
||
| it('rejects an empty list', () => { | ||
| expect(run('seed in ()')).toBeInstanceOf(Error); | ||
| }); | ||
| }); | ||
|
|
||
| describe('functions', () => { | ||
| it('supports built-in math functions', () => { | ||
| expect(run('abs(-3)')).toBe(3); | ||
| expect(run('max(1, 5, 2)')).toBe(5); | ||
| expect(run('round(1.6)')).toBe(2); | ||
| }); | ||
|
|
||
| it('supports injected extra functions', () => { | ||
| const runner = compileExpression('double(seed)', { | ||
| extraFunctions: {double: (n: number) => n * 2}, | ||
| }); | ||
| expect(runner(VARS)).toBe(1110); | ||
| }); | ||
|
|
||
| it('errors on unknown functions', () => { | ||
| expect(run('bogus(1)')).toBeInstanceOf(Error); | ||
| }); | ||
| }); | ||
|
|
||
| describe('error handling', () => { | ||
| it('never throws, returning an Error for malformed input', () => { | ||
| expect(() => compileExpression('1 +')).not.toThrow(); | ||
| expect(run('1 +')).toBeInstanceOf(Error); | ||
| expect(run('(1 + 2')).toBeInstanceOf(Error); | ||
| expect(run("'unterminated")).toBeInstanceOf(Error); | ||
| expect(run('1 @ 2')).toBeInstanceOf(Error); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.