diff --git a/src/art/search.js b/src/art/search.js new file mode 100644 index 0000000..38d6d96 --- /dev/null +++ b/src/art/search.js @@ -0,0 +1,15 @@ +export function searchArt(manifest, query) { + const terms = String(query ?? '').toLowerCase().split(/\s+/).filter(Boolean); + if (terms.length === 0) return [...manifest]; + + const scored = manifest.map((asset, index) => { + const hay = [asset.label, ...(asset.tags || [])].join(' ').toLowerCase(); + const score = terms.reduce((s, term) => s + (hay.includes(term) ? 1 : 0), 0); + return { asset, index, score }; + }); + + return scored + .filter((r) => r.score > 0) + .sort((a, b) => b.score - a.score || a.index - b.index) + .map((r) => r.asset); +} diff --git a/tests/art/search.test.js b/tests/art/search.test.js new file mode 100644 index 0000000..0fafb6c --- /dev/null +++ b/tests/art/search.test.js @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest'; +import { searchArt } from '../../src/art/search.js'; + +const M = [ + { id: 'a', src: 'a.png', label: 'Detective in alley', tags: ['detective', 'alley', 'rain'] }, + { id: 'b', src: 'b.png', label: 'Neon street', tags: ['street', 'neon', 'rain'] }, + { id: 'c', src: 'c.png', label: 'Worn portrait', tags: ['portrait', 'face'] }, +]; + +describe('searchArt', () => { + it('returns the whole manifest for a blank query, in order', () => { + expect(searchArt(M, '').map((x) => x.id)).toEqual(['a', 'b', 'c']); + expect(searchArt(M, ' ').map((x) => x.id)).toEqual(['a', 'b', 'c']); + }); + + it('ranks by number of matched terms', () => { + // "rain alley" -> a matches both (2), b matches rain (1), c matches none + expect(searchArt(M, 'rain alley').map((x) => x.id)).toEqual(['a', 'b']); + }); + + it('matches substrings in tags or label, case-insensitive', () => { + expect(searchArt(M, 'DETECT').map((x) => x.id)).toEqual(['a']); + expect(searchArt(M, 'portrait').map((x) => x.id)).toEqual(['c']); + }); + + it('breaks rank ties by original manifest order', () => { + // "rain" -> a and b both match once; a comes first in manifest + expect(searchArt(M, 'rain').map((x) => x.id)).toEqual(['a', 'b']); + }); +});