spotr 0.1.3 → 1.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +129 -141
- package/dist/Spotr.cjs +2 -0
- package/dist/Spotr.cjs.map +1 -0
- package/dist/Spotr.js +163 -0
- package/dist/Spotr.js.map +1 -0
- package/dist/errors.cjs +2 -0
- package/dist/errors.cjs.map +1 -0
- package/dist/errors.js +17 -0
- package/dist/errors.js.map +1 -0
- package/dist/fuzzy/levenshtein.cjs +2 -0
- package/dist/fuzzy/levenshtein.cjs.map +1 -0
- package/dist/fuzzy/levenshtein.js +29 -0
- package/dist/fuzzy/levenshtein.js.map +1 -0
- package/dist/fuzzy/scorer.cjs +2 -0
- package/dist/fuzzy/scorer.cjs.map +1 -0
- package/dist/fuzzy/scorer.js +36 -0
- package/dist/fuzzy/scorer.js.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/preact.cjs +2 -0
- package/dist/preact.cjs.map +1 -0
- package/dist/preact.d.ts +71 -0
- package/dist/preact.js +24 -0
- package/dist/preact.js.map +1 -0
- package/dist/react.cjs +2 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.ts +71 -0
- package/dist/react.js +24 -0
- package/dist/react.js.map +1 -0
- package/dist/solid.cjs +2 -0
- package/dist/solid.cjs.map +1 -0
- package/dist/solid.d.ts +95 -0
- package/dist/solid.js +11 -0
- package/dist/solid.js.map +1 -0
- package/dist/svelte.cjs +2 -0
- package/dist/svelte.cjs.map +1 -0
- package/dist/svelte.d.ts +95 -0
- package/dist/svelte.js +11 -0
- package/dist/svelte.js.map +1 -0
- package/dist/utils/nested.cjs +2 -0
- package/dist/utils/nested.cjs.map +1 -0
- package/dist/utils/nested.js +15 -0
- package/dist/utils/nested.js.map +1 -0
- package/dist/utils/tokenize.cjs +2 -0
- package/dist/utils/tokenize.cjs.map +1 -0
- package/dist/utils/tokenize.js +7 -0
- package/dist/utils/tokenize.js.map +1 -0
- package/dist/utils/validate.cjs +2 -0
- package/dist/utils/validate.cjs.map +1 -0
- package/dist/utils/validate.js +97 -0
- package/dist/utils/validate.js.map +1 -0
- package/dist/vue.cjs +2 -0
- package/dist/vue.cjs.map +1 -0
- package/dist/vue.d.ts +74 -0
- package/dist/vue.js +17 -0
- package/dist/vue.js.map +1 -0
- package/package.json +99 -26
- package/.babelrc +0 -6
- package/.npmignore +0 -37
- package/dist/spotr.min.js +0 -1
- package/dist/spotr.min.js.map +0 -1
- package/examples/basic/.babelrc +0 -8
- package/examples/basic/.npmignore +0 -5
- package/examples/basic/README.md +0 -19
- package/examples/basic/companies.json +0 -101
- package/examples/basic/index.html +0 -11
- package/examples/basic/index.js +0 -2
- package/examples/basic/package.json +0 -29
- package/examples/basic/src/App.vue +0 -104
- package/examples/basic/src/main.js +0 -11
- package/examples/basic/src/search.js +0 -15
- package/examples/basic/webpack.config.js +0 -80
- package/examples/basic/yarn.lock +0 -3960
- package/index.js +0 -1
- package/src/spotr.js +0 -120
- package/webpack.config.js +0 -63
- package/yarn.lock +0 -3125
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
> A powerful and minimal search library for static websites.
|
|
1
|
+
<img src="./examples/shared/spotr.svg" alt="Spotr logo" width="160" />
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
# Spotr
|
|
4
|
+
|
|
5
|
+
A powerful and minimal client-side fuzzy search library with zero dependencies.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Zero Dependencies** - Custom Levenshtein implementation, no external deps
|
|
10
|
+
- **Fuzzy Matching** - Configurable threshold (0-1) for fuzzy string matching
|
|
11
|
+
- **Nested Field Support** - Dot notation for deeply nested object properties
|
|
12
|
+
- **Keyword Filtering** - Pre-filter collections with custom keyword handlers
|
|
13
|
+
- **Framework Integrations** - React hooks and Vue composables
|
|
14
|
+
- **TypeScript** - Full type definitions included
|
|
5
15
|
|
|
6
16
|
## Installation
|
|
7
17
|
|
|
@@ -9,189 +19,167 @@ Great for prototyping search systems during development, or for apps and sites w
|
|
|
9
19
|
npm install spotr
|
|
10
20
|
```
|
|
11
21
|
|
|
12
|
-
##
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
## Basic Usage
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { Spotr } from 'spotr';
|
|
26
|
+
|
|
27
|
+
type Game = {
|
|
28
|
+
title: string;
|
|
29
|
+
genres: string[];
|
|
30
|
+
releaseYear: number;
|
|
31
|
+
completed: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const games = new Spotr<Game>({
|
|
35
|
+
collection: gamesArray,
|
|
36
|
+
threshold: 0.3,
|
|
37
|
+
fields: [
|
|
38
|
+
{ name: 'title', weight: 1 },
|
|
39
|
+
{ name: 'releaseYear', weight: 0.8 },
|
|
40
|
+
],
|
|
41
|
+
limit: 20,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const { results, matchedKeywords, tokens, warnings } = games.query('witcher');
|
|
45
|
+
// results: Array<{ item: Game, score: number }>
|
|
46
|
+
```
|
|
19
47
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
// Collection of objects to search on
|
|
23
|
-
collection: products,
|
|
48
|
+
## Options
|
|
24
49
|
|
|
25
|
-
|
|
26
|
-
fields: {
|
|
50
|
+
### `collection` (required)
|
|
27
51
|
|
|
28
|
-
|
|
29
|
-
name: {
|
|
30
|
-
fuzzy: 0,
|
|
31
|
-
boost: 9
|
|
32
|
-
},
|
|
52
|
+
Array or Set of objects to search.
|
|
33
53
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
fuzzy: 0,
|
|
37
|
-
boost: 8
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
|
|
41
|
-
// Keyword definitions
|
|
42
|
-
keywords: {
|
|
43
|
-
isAwesome: {
|
|
44
|
-
|
|
45
|
-
// Specific queries to match. Queries with multiple words possible too!
|
|
46
|
-
queries: ['awesome', 'amazing', 'incredible', 'terrific', 'super cool'],
|
|
47
|
-
|
|
48
|
-
// Filter to run on the collection when any of the above queries exist in the search query
|
|
49
|
-
filter: collection => collection.filter(item => item.isAwesome)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
})
|
|
54
|
+
```typescript
|
|
55
|
+
collection: gamesArray // or new Set(gamesArray)
|
|
53
56
|
```
|
|
54
57
|
|
|
55
|
-
###
|
|
56
|
-
The **collection** is the array of objects you want to search against. For static websites with a small set of things to search for, using `webpack` to load a JSON or CSV file makes it easy to bundle your collection with the final build.
|
|
58
|
+
### `fields` (required)
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
import products from 'products.csv'
|
|
60
|
+
Properties to search against with weight configuration. Supports dot notation for nested objects.
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
```typescript
|
|
63
|
+
fields: [
|
|
64
|
+
'title', // string shorthand, weight: 1
|
|
65
|
+
{ name: 'title', weight: 1 }, // full config
|
|
66
|
+
{ name: 'email', weight: 0.7 }, // lower priority
|
|
67
|
+
{ name: 'address.city', weight: 0.8 }, // nested field
|
|
68
|
+
]
|
|
64
69
|
```
|
|
65
70
|
|
|
71
|
+
### `keywords` (optional)
|
|
66
72
|
|
|
67
|
-
|
|
68
|
-
**Fields** tell Spotr to search against a specific string property on each item in the collection. Results are scored and sorted based on them.
|
|
73
|
+
Keywords that trigger specific collection filtering before fuzzy matching.
|
|
69
74
|
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
```typescript
|
|
76
|
+
keywords: {
|
|
77
|
+
mode: 'and',
|
|
78
|
+
definitions: [
|
|
79
|
+
{
|
|
80
|
+
name: 'completed',
|
|
81
|
+
triggers: ['done', 'complete', 'finished'],
|
|
82
|
+
handler: (collection) => collection.filter(item => item.completed),
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'platform',
|
|
86
|
+
triggers: ['ps4', 'ps5', 'xbox', 'pc', 'switch'],
|
|
87
|
+
handler: (collection, matchedTerms) =>
|
|
88
|
+
collection.filter(item =>
|
|
89
|
+
matchedTerms.some(term =>
|
|
90
|
+
item.platforms.some(p => p.toLowerCase().includes(term))
|
|
91
|
+
)
|
|
92
|
+
),
|
|
93
|
+
},
|
|
94
|
+
],
|
|
79
95
|
}
|
|
80
96
|
```
|
|
81
97
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
`boost` is a multiplier applied to the final string score to prioritize the result if the field has a positive match. For instance, you may want queries matching closest to the name or title of an item to rank higher than if it matches its category.
|
|
85
|
-
|
|
86
|
-
**Example**
|
|
98
|
+
### `threshold` (optional)
|
|
87
99
|
|
|
88
|
-
|
|
89
|
-
| --- | --- |
|
|
90
|
-
| title | 9 |
|
|
91
|
-
| category | 8 |
|
|
92
|
-
| label | 7 |
|
|
93
|
-
| author | 6 |
|
|
100
|
+
Global fuzzy matching threshold. Default: `0.3`
|
|
94
101
|
|
|
95
|
-
|
|
102
|
+
- `0` = match anything (no filtering)
|
|
103
|
+
- `0.3` = recommended default
|
|
104
|
+
- `1` = exact match required
|
|
96
105
|
|
|
106
|
+
### `limit` (optional)
|
|
97
107
|
|
|
98
|
-
|
|
99
|
-
**Keywords** tell Spotr to look for specific queries within the search query to further filter the collection.
|
|
108
|
+
Maximum number of results to return. Default: `Infinity`
|
|
100
109
|
|
|
101
|
-
|
|
102
|
-
keywords: {
|
|
103
|
-
completed: {
|
|
104
|
-
queries: ['complete', 'completed', 'is done'],
|
|
105
|
-
filter: collection => collection.filter(item => item.completed)
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
```
|
|
110
|
+
### `caseSensitive` (optional)
|
|
109
111
|
|
|
110
|
-
|
|
112
|
+
Enable case-sensitive matching. Default: `false`
|
|
111
113
|
|
|
112
|
-
|
|
114
|
+
### `minMatchCharLength` (optional)
|
|
113
115
|
|
|
114
|
-
|
|
115
|
-
Keyword filters are applied to the collection additively, so if multiple filters are a positive match, it will result in a narrower set of results. The filtered collection is finally passed to our `fields` before returning the final result.
|
|
116
|
+
Minimum query length to trigger matching. Default: `1`
|
|
116
117
|
|
|
117
|
-
|
|
118
|
-
When `keyword` definitions are being used, matching queries are removed from the search query before being tested against each field. For example, if we wanted to filter shirts on clearance:
|
|
118
|
+
## Result Type
|
|
119
119
|
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
120
|
+
```typescript
|
|
121
|
+
interface SpotrResult<T> {
|
|
122
|
+
results: ScoredResult<T>[]; // Array of { item, score }
|
|
123
|
+
matchedKeywords: MatchedKeyword[]; // Keywords that matched
|
|
124
|
+
tokens: string[]; // Non-keyword search terms
|
|
125
|
+
warnings: string[]; // Warnings (e.g., missing nested paths)
|
|
126
126
|
}
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
## React Hook
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
```typescript
|
|
132
|
+
import { useSpotr } from 'spotr/react';
|
|
132
133
|
|
|
133
|
-
|
|
134
|
+
function GameSearch({ games, searchQuery }) {
|
|
135
|
+
const spotr = useSpotr({
|
|
136
|
+
collection: games,
|
|
137
|
+
fields: [{ name: 'title', weight: 1 }],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const { results } = useMemo(
|
|
141
|
+
() => spotr.query(searchQuery),
|
|
142
|
+
[spotr, searchQuery]
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return <ResultsTable results={results} />;
|
|
146
|
+
}
|
|
147
|
+
```
|
|
134
148
|
|
|
135
|
-
|
|
149
|
+
## Vue Composable
|
|
136
150
|
|
|
137
|
-
|
|
151
|
+
```typescript
|
|
152
|
+
import { useSpotr } from 'spotr/vue';
|
|
138
153
|
|
|
139
|
-
|
|
154
|
+
const games = ref<Game[]>([]);
|
|
155
|
+
const query = ref('');
|
|
140
156
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
fields: {
|
|
144
|
-
|
|
145
|
-
boost: 9
|
|
146
|
-
}
|
|
147
|
-
},
|
|
148
|
-
keywords: {
|
|
149
|
-
allAges: {
|
|
150
|
-
queries: ['all ages'],
|
|
151
|
-
filter: collection => collection.filter(item => item.allAges)
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
})
|
|
157
|
+
const spotr = useSpotr(() => ({
|
|
158
|
+
collection: games.value,
|
|
159
|
+
fields: [{ name: 'title', weight: 1 }],
|
|
160
|
+
}));
|
|
155
161
|
|
|
156
|
-
|
|
157
|
-
// => Array of games for all ages
|
|
162
|
+
const results = computed(() => spotr.value?.query(query.value));
|
|
158
163
|
```
|
|
159
164
|
|
|
160
|
-
|
|
165
|
+
## API Methods
|
|
161
166
|
|
|
162
|
-
|
|
167
|
+
### `query(search: string): SpotrResult<T>`
|
|
163
168
|
|
|
164
|
-
|
|
169
|
+
Search the collection with a string query.
|
|
165
170
|
|
|
166
|
-
|
|
167
|
-
const search = new Spotr({ ... })
|
|
171
|
+
### `queryAsync(search: string): Promise<SpotrResult<T>>`
|
|
168
172
|
|
|
169
|
-
|
|
170
|
-
search.collection // [ item, item ...]
|
|
173
|
+
Async search with optional debouncing.
|
|
171
174
|
|
|
172
|
-
|
|
173
|
-
search.collection = [
|
|
174
|
-
{ name: 'Watermelon', rating: 5 },
|
|
175
|
-
{ name: 'Prune', rating: 4 },
|
|
176
|
-
{ name: 'Durian', rating: 1}
|
|
177
|
-
]
|
|
178
|
-
// Nothing against durians... well, they just smell bad.
|
|
179
|
-
```
|
|
175
|
+
### `setCollection(collection: T[] | Set<T>): void`
|
|
180
176
|
|
|
181
|
-
|
|
177
|
+
Update the collection.
|
|
182
178
|
|
|
183
|
-
###
|
|
184
|
-
```sh
|
|
185
|
-
git clone https://github.com/docmars/spotr
|
|
186
|
-
cd spotr
|
|
187
|
-
npm install
|
|
188
|
-
npm run dev
|
|
189
|
-
```
|
|
179
|
+
### `collection: T[]` (getter)
|
|
190
180
|
|
|
191
|
-
|
|
192
|
-
```sh
|
|
193
|
-
npm run build
|
|
194
|
-
```
|
|
181
|
+
Access the current collection.
|
|
195
182
|
|
|
196
183
|
## License
|
|
184
|
+
|
|
197
185
|
MIT
|
package/dist/Spotr.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var k=Object.defineProperty;var T=(u,e,t)=>e in u?k(u,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):u[e]=t;var c=(u,e,t)=>T(u,typeof e!="symbol"?e+"":e,t);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const m=require("./errors.cjs"),y=require("./utils/validate.cjs"),f=require("./fuzzy/scorer.cjs"),S=require("./utils/tokenize.cjs");class K{constructor(e){c(this,"_collection");c(this,"_fields");c(this,"_keywords");c(this,"_keywordTriggerMap");c(this,"_threshold");c(this,"_limit");c(this,"_debounce");c(this,"_caseSensitive");c(this,"_minMatchCharLength");c(this,"_debounceTimer",null);c(this,"_optionsSnapshot");this._optionsSnapshot=e;const t=y.validateOptions(e);this._collection=t.collection,this._threshold=t.threshold,this._limit=t.limit,this._debounce=t.debounce,this._caseSensitive=t.caseSensitive,this._minMatchCharLength=t.minMatchCharLength,this._fields=f.normalizeFieldConfig(e.fields,this._threshold),this._keywords=e.keywords?y.validateKeywords(e.keywords):null,this._keywordTriggerMap=this._buildKeywordTriggerMap()}_buildKeywordTriggerMap(){const e=new Map;if(!this._keywords)return e;for(const t of this._keywords.definitions){const i=Array.isArray(t.triggers)?t.triggers:[t.triggers];for(const r of i)e.set(r.toLowerCase(),t)}return e}get collection(){return this._collection}get options(){return this._optionsSnapshot}setCollection(e){this._collection=e instanceof Set?Array.from(e):e}query(e){const t=S.tokenize(e);if(t.length===0)return{results:[],matchedKeywords:[],tokens:[],warnings:[]};const{keywordTokens:i,searchTokens:r}=this._extractKeywords(t);let a=this._collection;const s=[];if(i.size>0){const o=this._applyKeywords(a,i);a=o.collection,s.push(...o.matchedKeywords)}if(r.length===0){const o=[];return{results:a.map(_=>{const{score:g,warnings:p}=f.scoreItem(_,[],this._fields,this._caseSensitive);return o.push(...p),{item:_,score:g}}),matchedKeywords:s,tokens:r,warnings:[...new Set(o)]}}const n=this._minMatchCharLength>1?r.filter(o=>o.length>=this._minMatchCharLength):r;if(n.length===0)return{results:[],matchedKeywords:s,tokens:r,warnings:[]};const h=[],l=[];for(const o of a){const{score:d,warnings:_}=f.scoreItem(o,n,this._fields,this._caseSensitive);h.push(..._),d>0&&l.push({item:o,score:d})}return l.sort((o,d)=>d.score-o.score),{results:this._limit<1/0?l.slice(0,this._limit):l,matchedKeywords:s,tokens:r,warnings:[...new Set(h)]}}queryAsync(e){return new Promise(t=>{this._debounce>0?(this._debounceTimer&&clearTimeout(this._debounceTimer),this._debounceTimer=setTimeout(()=>{t(this.query(e))},this._debounce)):t(this.query(e))})}_extractKeywords(e){const t=new Map,i=[];for(const r of e){const a=this._caseSensitive?r:r.toLowerCase(),s=this._keywordTriggerMap.get(a);if(s){const n=t.get(s.name)||[];n.push(r),t.set(s.name,n)}else i.push(r)}return{keywordTokens:t,searchTokens:i}}_applyKeywords(e,t){if(!this._keywords)return{collection:e,matchedKeywords:[]};let i=e;const r=[],a=new Map;for(const[s,n]of t)a.set(s,n);if(this._keywords.mode==="and")for(const s of this._keywords.definitions){const n=a.get(s.name);if(n){const h=s.handler;if(i=h(i,n),r.push({name:s.name,terms:n}),!Array.isArray(i))throw new m.SpotrError(`Keyword handler "${s.name}" must return an array`,m.ErrorCodes.INVALID_HANDLER_RETURN)}}else{const s=[],n=new Set;for(const h of this._keywords.definitions){const l=a.get(h.name);if(l){const w=h.handler,o=w(this._collection,l);if(r.push({name:h.name,terms:l}),!Array.isArray(o))throw new m.SpotrError(`Keyword handler "${h.name}" must return an array`,m.ErrorCodes.INVALID_HANDLER_RETURN);for(const d of o)n.has(d)||(n.add(d),s.push(d))}}i=s}return{collection:i,matchedKeywords:r}}}exports.Spotr=K;
|
|
2
|
+
//# sourceMappingURL=Spotr.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Spotr.cjs","sources":["../src/Spotr.ts"],"sourcesContent":["import { SpotrError, ErrorCodes } from './errors';\nimport { normalizeFieldConfig, scoreItem } from './fuzzy';\nimport { tokenize, validateOptions, validateKeywords } from './utils';\nimport type {\n SpotrOptions,\n SpotrResult,\n ScoredResult,\n MatchedKeyword,\n NormalizedFieldConfig,\n NormalizedKeywordsConfig,\n KeywordDefinition,\n} from './types';\n\nexport class Spotr<T extends object> {\n private _collection: T[];\n private _fields: NormalizedFieldConfig[];\n private _keywords: NormalizedKeywordsConfig | null;\n private _keywordTriggerMap: Map<string, KeywordDefinition<unknown>>;\n private _threshold: number;\n private _limit: number;\n private _debounce: number;\n private _caseSensitive: boolean;\n private _minMatchCharLength: number;\n private _debounceTimer: ReturnType<typeof setTimeout> | null = null;\n private _optionsSnapshot: SpotrOptions<T>;\n\n constructor(options: SpotrOptions<T>) {\n this._optionsSnapshot = options;\n const validated = validateOptions(options);\n this._collection = validated.collection;\n this._threshold = validated.threshold;\n this._limit = validated.limit;\n this._debounce = validated.debounce;\n this._caseSensitive = validated.caseSensitive;\n this._minMatchCharLength = validated.minMatchCharLength;\n\n this._fields = normalizeFieldConfig(options.fields, this._threshold);\n\n this._keywords = options.keywords ? validateKeywords(options.keywords) : null;\n this._keywordTriggerMap = this._buildKeywordTriggerMap();\n }\n\n private _buildKeywordTriggerMap(): Map<string, KeywordDefinition<unknown>> {\n const map = new Map<string, KeywordDefinition<unknown>>();\n if (!this._keywords) return map;\n\n for (const def of this._keywords.definitions) {\n const triggers = Array.isArray(def.triggers) ? def.triggers : [def.triggers];\n for (const trigger of triggers) {\n map.set(trigger.toLowerCase(), def);\n }\n }\n return map;\n }\n\n get collection(): T[] {\n return this._collection;\n }\n\n get options(): SpotrOptions<T> {\n return this._optionsSnapshot;\n }\n\n setCollection(collection: T[] | Set<T>): void {\n this._collection = collection instanceof Set ? Array.from(collection) : collection;\n }\n\n query(search: string): SpotrResult<T> {\n const tokens = tokenize(search);\n \n if (tokens.length === 0) {\n return {\n results: [],\n matchedKeywords: [],\n tokens: [],\n warnings: [],\n };\n }\n\n const { keywordTokens, searchTokens } = this._extractKeywords(tokens);\n\n let filteredCollection = this._collection;\n const matchedKeywords: MatchedKeyword[] = [];\n\n if (keywordTokens.size > 0) {\n const result = this._applyKeywords(filteredCollection, keywordTokens);\n filteredCollection = result.collection;\n matchedKeywords.push(...result.matchedKeywords);\n }\n\n if (searchTokens.length === 0) {\n const allWarnings: string[] = [];\n const results: ScoredResult<T>[] = filteredCollection.map((item) => {\n const { score, warnings } = scoreItem(\n item,\n [],\n this._fields,\n this._caseSensitive\n );\n allWarnings.push(...warnings);\n return { item, score };\n });\n\n return {\n results,\n matchedKeywords,\n tokens: searchTokens,\n warnings: [...new Set(allWarnings)],\n };\n }\n\n const validTokens = this._minMatchCharLength > 1\n ? searchTokens.filter((t) => t.length >= this._minMatchCharLength)\n : searchTokens;\n\n if (validTokens.length === 0) {\n return {\n results: [],\n matchedKeywords,\n tokens: searchTokens,\n warnings: [],\n };\n }\n\n const allWarnings: string[] = [];\n const scoredResults: ScoredResult<T>[] = [];\n\n for (const item of filteredCollection) {\n const { score, warnings } = scoreItem(\n item,\n validTokens,\n this._fields,\n this._caseSensitive\n );\n allWarnings.push(...warnings);\n\n if (score > 0) {\n scoredResults.push({ item, score });\n }\n }\n\n scoredResults.sort((a, b) => b.score - a.score);\n\n const limitedResults = this._limit < Infinity\n ? scoredResults.slice(0, this._limit)\n : scoredResults;\n\n return {\n results: limitedResults,\n matchedKeywords,\n tokens: searchTokens,\n warnings: [...new Set(allWarnings)],\n };\n }\n\n queryAsync(search: string): Promise<SpotrResult<T>> {\n return new Promise((resolve) => {\n if (this._debounce > 0) {\n if (this._debounceTimer) {\n clearTimeout(this._debounceTimer);\n }\n this._debounceTimer = setTimeout(() => {\n resolve(this.query(search));\n }, this._debounce);\n } else {\n resolve(this.query(search));\n }\n });\n }\n\n private _extractKeywords(tokens: string[]): {\n keywordTokens: Map<string, string[]>;\n searchTokens: string[];\n } {\n const keywordTokens = new Map<string, string[]>();\n const searchTokens: string[] = [];\n\n for (const token of tokens) {\n const normalizedToken = this._caseSensitive ? token : token.toLowerCase();\n const keywordDef = this._keywordTriggerMap.get(normalizedToken);\n\n if (keywordDef) {\n const existing = keywordTokens.get(keywordDef.name) || [];\n existing.push(token);\n keywordTokens.set(keywordDef.name, existing);\n } else {\n searchTokens.push(token);\n }\n }\n\n return { keywordTokens, searchTokens };\n }\n\n private _applyKeywords(\n collection: T[],\n keywordTokens: Map<string, string[]>\n ): { collection: T[]; matchedKeywords: MatchedKeyword[] } {\n if (!this._keywords) {\n return { collection, matchedKeywords: [] };\n }\n\n let result = collection;\n const matchedKeywords: MatchedKeyword[] = [];\n\n const keywordNameToTerms = new Map<string, string[]>();\n for (const [name, terms] of keywordTokens) {\n keywordNameToTerms.set(name, terms);\n }\n\n if (this._keywords.mode === 'and') {\n for (const def of this._keywords.definitions) {\n const terms = keywordNameToTerms.get(def.name);\n if (terms) {\n const handler = def.handler as (collection: T[], matchedTerms: string[]) => T[];\n result = handler(result, terms);\n matchedKeywords.push({ name: def.name, terms });\n\n if (!Array.isArray(result)) {\n throw new SpotrError(\n `Keyword handler \"${def.name}\" must return an array`,\n ErrorCodes.INVALID_HANDLER_RETURN\n );\n }\n }\n }\n } else {\n const mergedResults: T[] = [];\n const processedItems = new Set<T>();\n\n for (const def of this._keywords.definitions) {\n const terms = keywordNameToTerms.get(def.name);\n if (terms) {\n const handler = def.handler as (collection: T[], matchedTerms: string[]) => T[];\n const keywordResults = handler(this._collection, terms);\n matchedKeywords.push({ name: def.name, terms });\n\n if (!Array.isArray(keywordResults)) {\n throw new SpotrError(\n `Keyword handler \"${def.name}\" must return an array`,\n ErrorCodes.INVALID_HANDLER_RETURN\n );\n }\n\n for (const item of keywordResults) {\n if (!processedItems.has(item)) {\n processedItems.add(item);\n mergedResults.push(item);\n }\n }\n }\n }\n result = mergedResults;\n }\n\n return { collection: result, matchedKeywords };\n }\n}\n"],"names":["Spotr","options","__publicField","validated","validateOptions","normalizeFieldConfig","validateKeywords","map","def","triggers","trigger","collection","search","tokens","tokenize","keywordTokens","searchTokens","filteredCollection","matchedKeywords","result","allWarnings","item","score","warnings","scoreItem","validTokens","t","scoredResults","a","b","resolve","token","normalizedToken","keywordDef","existing","keywordNameToTerms","name","terms","handler","SpotrError","ErrorCodes","mergedResults","processedItems","keywordResults"],"mappings":"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"}
|
package/dist/Spotr.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
var k = Object.defineProperty;
|
|
2
|
+
var T = (m, e, t) => e in m ? k(m, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : m[e] = t;
|
|
3
|
+
var a = (m, e, t) => T(m, typeof e != "symbol" ? e + "" : e, t);
|
|
4
|
+
import { SpotrError as f, ErrorCodes as w } from "./errors.js";
|
|
5
|
+
import { validateOptions as S, validateKeywords as K } from "./utils/validate.js";
|
|
6
|
+
import { normalizeFieldConfig as M, scoreItem as y } from "./fuzzy/scorer.js";
|
|
7
|
+
import { tokenize as A } from "./utils/tokenize.js";
|
|
8
|
+
class I {
|
|
9
|
+
constructor(e) {
|
|
10
|
+
a(this, "_collection");
|
|
11
|
+
a(this, "_fields");
|
|
12
|
+
a(this, "_keywords");
|
|
13
|
+
a(this, "_keywordTriggerMap");
|
|
14
|
+
a(this, "_threshold");
|
|
15
|
+
a(this, "_limit");
|
|
16
|
+
a(this, "_debounce");
|
|
17
|
+
a(this, "_caseSensitive");
|
|
18
|
+
a(this, "_minMatchCharLength");
|
|
19
|
+
a(this, "_debounceTimer", null);
|
|
20
|
+
a(this, "_optionsSnapshot");
|
|
21
|
+
this._optionsSnapshot = e;
|
|
22
|
+
const t = S(e);
|
|
23
|
+
this._collection = t.collection, this._threshold = t.threshold, this._limit = t.limit, this._debounce = t.debounce, this._caseSensitive = t.caseSensitive, this._minMatchCharLength = t.minMatchCharLength, this._fields = M(e.fields, this._threshold), this._keywords = e.keywords ? K(e.keywords) : null, this._keywordTriggerMap = this._buildKeywordTriggerMap();
|
|
24
|
+
}
|
|
25
|
+
_buildKeywordTriggerMap() {
|
|
26
|
+
const e = /* @__PURE__ */ new Map();
|
|
27
|
+
if (!this._keywords) return e;
|
|
28
|
+
for (const t of this._keywords.definitions) {
|
|
29
|
+
const i = Array.isArray(t.triggers) ? t.triggers : [t.triggers];
|
|
30
|
+
for (const r of i)
|
|
31
|
+
e.set(r.toLowerCase(), t);
|
|
32
|
+
}
|
|
33
|
+
return e;
|
|
34
|
+
}
|
|
35
|
+
get collection() {
|
|
36
|
+
return this._collection;
|
|
37
|
+
}
|
|
38
|
+
get options() {
|
|
39
|
+
return this._optionsSnapshot;
|
|
40
|
+
}
|
|
41
|
+
setCollection(e) {
|
|
42
|
+
this._collection = e instanceof Set ? Array.from(e) : e;
|
|
43
|
+
}
|
|
44
|
+
query(e) {
|
|
45
|
+
const t = A(e);
|
|
46
|
+
if (t.length === 0)
|
|
47
|
+
return {
|
|
48
|
+
results: [],
|
|
49
|
+
matchedKeywords: [],
|
|
50
|
+
tokens: [],
|
|
51
|
+
warnings: []
|
|
52
|
+
};
|
|
53
|
+
const { keywordTokens: i, searchTokens: r } = this._extractKeywords(t);
|
|
54
|
+
let h = this._collection;
|
|
55
|
+
const s = [];
|
|
56
|
+
if (i.size > 0) {
|
|
57
|
+
const o = this._applyKeywords(h, i);
|
|
58
|
+
h = o.collection, s.push(...o.matchedKeywords);
|
|
59
|
+
}
|
|
60
|
+
if (r.length === 0) {
|
|
61
|
+
const o = [];
|
|
62
|
+
return {
|
|
63
|
+
results: h.map((u) => {
|
|
64
|
+
const { score: g, warnings: p } = y(
|
|
65
|
+
u,
|
|
66
|
+
[],
|
|
67
|
+
this._fields,
|
|
68
|
+
this._caseSensitive
|
|
69
|
+
);
|
|
70
|
+
return o.push(...p), { item: u, score: g };
|
|
71
|
+
}),
|
|
72
|
+
matchedKeywords: s,
|
|
73
|
+
tokens: r,
|
|
74
|
+
warnings: [...new Set(o)]
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const n = this._minMatchCharLength > 1 ? r.filter((o) => o.length >= this._minMatchCharLength) : r;
|
|
78
|
+
if (n.length === 0)
|
|
79
|
+
return {
|
|
80
|
+
results: [],
|
|
81
|
+
matchedKeywords: s,
|
|
82
|
+
tokens: r,
|
|
83
|
+
warnings: []
|
|
84
|
+
};
|
|
85
|
+
const c = [], l = [];
|
|
86
|
+
for (const o of h) {
|
|
87
|
+
const { score: d, warnings: u } = y(
|
|
88
|
+
o,
|
|
89
|
+
n,
|
|
90
|
+
this._fields,
|
|
91
|
+
this._caseSensitive
|
|
92
|
+
);
|
|
93
|
+
c.push(...u), d > 0 && l.push({ item: o, score: d });
|
|
94
|
+
}
|
|
95
|
+
return l.sort((o, d) => d.score - o.score), {
|
|
96
|
+
results: this._limit < 1 / 0 ? l.slice(0, this._limit) : l,
|
|
97
|
+
matchedKeywords: s,
|
|
98
|
+
tokens: r,
|
|
99
|
+
warnings: [...new Set(c)]
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
queryAsync(e) {
|
|
103
|
+
return new Promise((t) => {
|
|
104
|
+
this._debounce > 0 ? (this._debounceTimer && clearTimeout(this._debounceTimer), this._debounceTimer = setTimeout(() => {
|
|
105
|
+
t(this.query(e));
|
|
106
|
+
}, this._debounce)) : t(this.query(e));
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
_extractKeywords(e) {
|
|
110
|
+
const t = /* @__PURE__ */ new Map(), i = [];
|
|
111
|
+
for (const r of e) {
|
|
112
|
+
const h = this._caseSensitive ? r : r.toLowerCase(), s = this._keywordTriggerMap.get(h);
|
|
113
|
+
if (s) {
|
|
114
|
+
const n = t.get(s.name) || [];
|
|
115
|
+
n.push(r), t.set(s.name, n);
|
|
116
|
+
} else
|
|
117
|
+
i.push(r);
|
|
118
|
+
}
|
|
119
|
+
return { keywordTokens: t, searchTokens: i };
|
|
120
|
+
}
|
|
121
|
+
_applyKeywords(e, t) {
|
|
122
|
+
if (!this._keywords)
|
|
123
|
+
return { collection: e, matchedKeywords: [] };
|
|
124
|
+
let i = e;
|
|
125
|
+
const r = [], h = /* @__PURE__ */ new Map();
|
|
126
|
+
for (const [s, n] of t)
|
|
127
|
+
h.set(s, n);
|
|
128
|
+
if (this._keywords.mode === "and")
|
|
129
|
+
for (const s of this._keywords.definitions) {
|
|
130
|
+
const n = h.get(s.name);
|
|
131
|
+
if (n) {
|
|
132
|
+
const c = s.handler;
|
|
133
|
+
if (i = c(i, n), r.push({ name: s.name, terms: n }), !Array.isArray(i))
|
|
134
|
+
throw new f(
|
|
135
|
+
`Keyword handler "${s.name}" must return an array`,
|
|
136
|
+
w.INVALID_HANDLER_RETURN
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
const s = [], n = /* @__PURE__ */ new Set();
|
|
142
|
+
for (const c of this._keywords.definitions) {
|
|
143
|
+
const l = h.get(c.name);
|
|
144
|
+
if (l) {
|
|
145
|
+
const _ = c.handler, o = _(this._collection, l);
|
|
146
|
+
if (r.push({ name: c.name, terms: l }), !Array.isArray(o))
|
|
147
|
+
throw new f(
|
|
148
|
+
`Keyword handler "${c.name}" must return an array`,
|
|
149
|
+
w.INVALID_HANDLER_RETURN
|
|
150
|
+
);
|
|
151
|
+
for (const d of o)
|
|
152
|
+
n.has(d) || (n.add(d), s.push(d));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
i = s;
|
|
156
|
+
}
|
|
157
|
+
return { collection: i, matchedKeywords: r };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export {
|
|
161
|
+
I as Spotr
|
|
162
|
+
};
|
|
163
|
+
//# sourceMappingURL=Spotr.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Spotr.js","sources":["../src/Spotr.ts"],"sourcesContent":["import { SpotrError, ErrorCodes } from './errors';\nimport { normalizeFieldConfig, scoreItem } from './fuzzy';\nimport { tokenize, validateOptions, validateKeywords } from './utils';\nimport type {\n SpotrOptions,\n SpotrResult,\n ScoredResult,\n MatchedKeyword,\n NormalizedFieldConfig,\n NormalizedKeywordsConfig,\n KeywordDefinition,\n} from './types';\n\nexport class Spotr<T extends object> {\n private _collection: T[];\n private _fields: NormalizedFieldConfig[];\n private _keywords: NormalizedKeywordsConfig | null;\n private _keywordTriggerMap: Map<string, KeywordDefinition<unknown>>;\n private _threshold: number;\n private _limit: number;\n private _debounce: number;\n private _caseSensitive: boolean;\n private _minMatchCharLength: number;\n private _debounceTimer: ReturnType<typeof setTimeout> | null = null;\n private _optionsSnapshot: SpotrOptions<T>;\n\n constructor(options: SpotrOptions<T>) {\n this._optionsSnapshot = options;\n const validated = validateOptions(options);\n this._collection = validated.collection;\n this._threshold = validated.threshold;\n this._limit = validated.limit;\n this._debounce = validated.debounce;\n this._caseSensitive = validated.caseSensitive;\n this._minMatchCharLength = validated.minMatchCharLength;\n\n this._fields = normalizeFieldConfig(options.fields, this._threshold);\n\n this._keywords = options.keywords ? validateKeywords(options.keywords) : null;\n this._keywordTriggerMap = this._buildKeywordTriggerMap();\n }\n\n private _buildKeywordTriggerMap(): Map<string, KeywordDefinition<unknown>> {\n const map = new Map<string, KeywordDefinition<unknown>>();\n if (!this._keywords) return map;\n\n for (const def of this._keywords.definitions) {\n const triggers = Array.isArray(def.triggers) ? def.triggers : [def.triggers];\n for (const trigger of triggers) {\n map.set(trigger.toLowerCase(), def);\n }\n }\n return map;\n }\n\n get collection(): T[] {\n return this._collection;\n }\n\n get options(): SpotrOptions<T> {\n return this._optionsSnapshot;\n }\n\n setCollection(collection: T[] | Set<T>): void {\n this._collection = collection instanceof Set ? Array.from(collection) : collection;\n }\n\n query(search: string): SpotrResult<T> {\n const tokens = tokenize(search);\n \n if (tokens.length === 0) {\n return {\n results: [],\n matchedKeywords: [],\n tokens: [],\n warnings: [],\n };\n }\n\n const { keywordTokens, searchTokens } = this._extractKeywords(tokens);\n\n let filteredCollection = this._collection;\n const matchedKeywords: MatchedKeyword[] = [];\n\n if (keywordTokens.size > 0) {\n const result = this._applyKeywords(filteredCollection, keywordTokens);\n filteredCollection = result.collection;\n matchedKeywords.push(...result.matchedKeywords);\n }\n\n if (searchTokens.length === 0) {\n const allWarnings: string[] = [];\n const results: ScoredResult<T>[] = filteredCollection.map((item) => {\n const { score, warnings } = scoreItem(\n item,\n [],\n this._fields,\n this._caseSensitive\n );\n allWarnings.push(...warnings);\n return { item, score };\n });\n\n return {\n results,\n matchedKeywords,\n tokens: searchTokens,\n warnings: [...new Set(allWarnings)],\n };\n }\n\n const validTokens = this._minMatchCharLength > 1\n ? searchTokens.filter((t) => t.length >= this._minMatchCharLength)\n : searchTokens;\n\n if (validTokens.length === 0) {\n return {\n results: [],\n matchedKeywords,\n tokens: searchTokens,\n warnings: [],\n };\n }\n\n const allWarnings: string[] = [];\n const scoredResults: ScoredResult<T>[] = [];\n\n for (const item of filteredCollection) {\n const { score, warnings } = scoreItem(\n item,\n validTokens,\n this._fields,\n this._caseSensitive\n );\n allWarnings.push(...warnings);\n\n if (score > 0) {\n scoredResults.push({ item, score });\n }\n }\n\n scoredResults.sort((a, b) => b.score - a.score);\n\n const limitedResults = this._limit < Infinity\n ? scoredResults.slice(0, this._limit)\n : scoredResults;\n\n return {\n results: limitedResults,\n matchedKeywords,\n tokens: searchTokens,\n warnings: [...new Set(allWarnings)],\n };\n }\n\n queryAsync(search: string): Promise<SpotrResult<T>> {\n return new Promise((resolve) => {\n if (this._debounce > 0) {\n if (this._debounceTimer) {\n clearTimeout(this._debounceTimer);\n }\n this._debounceTimer = setTimeout(() => {\n resolve(this.query(search));\n }, this._debounce);\n } else {\n resolve(this.query(search));\n }\n });\n }\n\n private _extractKeywords(tokens: string[]): {\n keywordTokens: Map<string, string[]>;\n searchTokens: string[];\n } {\n const keywordTokens = new Map<string, string[]>();\n const searchTokens: string[] = [];\n\n for (const token of tokens) {\n const normalizedToken = this._caseSensitive ? token : token.toLowerCase();\n const keywordDef = this._keywordTriggerMap.get(normalizedToken);\n\n if (keywordDef) {\n const existing = keywordTokens.get(keywordDef.name) || [];\n existing.push(token);\n keywordTokens.set(keywordDef.name, existing);\n } else {\n searchTokens.push(token);\n }\n }\n\n return { keywordTokens, searchTokens };\n }\n\n private _applyKeywords(\n collection: T[],\n keywordTokens: Map<string, string[]>\n ): { collection: T[]; matchedKeywords: MatchedKeyword[] } {\n if (!this._keywords) {\n return { collection, matchedKeywords: [] };\n }\n\n let result = collection;\n const matchedKeywords: MatchedKeyword[] = [];\n\n const keywordNameToTerms = new Map<string, string[]>();\n for (const [name, terms] of keywordTokens) {\n keywordNameToTerms.set(name, terms);\n }\n\n if (this._keywords.mode === 'and') {\n for (const def of this._keywords.definitions) {\n const terms = keywordNameToTerms.get(def.name);\n if (terms) {\n const handler = def.handler as (collection: T[], matchedTerms: string[]) => T[];\n result = handler(result, terms);\n matchedKeywords.push({ name: def.name, terms });\n\n if (!Array.isArray(result)) {\n throw new SpotrError(\n `Keyword handler \"${def.name}\" must return an array`,\n ErrorCodes.INVALID_HANDLER_RETURN\n );\n }\n }\n }\n } else {\n const mergedResults: T[] = [];\n const processedItems = new Set<T>();\n\n for (const def of this._keywords.definitions) {\n const terms = keywordNameToTerms.get(def.name);\n if (terms) {\n const handler = def.handler as (collection: T[], matchedTerms: string[]) => T[];\n const keywordResults = handler(this._collection, terms);\n matchedKeywords.push({ name: def.name, terms });\n\n if (!Array.isArray(keywordResults)) {\n throw new SpotrError(\n `Keyword handler \"${def.name}\" must return an array`,\n ErrorCodes.INVALID_HANDLER_RETURN\n );\n }\n\n for (const item of keywordResults) {\n if (!processedItems.has(item)) {\n processedItems.add(item);\n mergedResults.push(item);\n }\n }\n }\n }\n result = mergedResults;\n }\n\n return { collection: result, matchedKeywords };\n }\n}\n"],"names":["Spotr","options","__publicField","validated","validateOptions","normalizeFieldConfig","validateKeywords","map","def","triggers","trigger","collection","search","tokens","tokenize","keywordTokens","searchTokens","filteredCollection","matchedKeywords","result","allWarnings","item","score","warnings","scoreItem","validTokens","t","scoredResults","a","b","resolve","token","normalizedToken","keywordDef","existing","keywordNameToTerms","name","terms","handler","SpotrError","ErrorCodes","mergedResults","processedItems","keywordResults"],"mappings":"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"}
|
package/dist/errors.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class E extends Error{constructor(I,r){super(I),this.code=r,this.name="SpotrError"}}const L={INVALID_COLLECTION:"INVALID_COLLECTION",INVALID_FIELD_CONFIG:"INVALID_FIELD_CONFIG",INVALID_FIELD_WEIGHT:"INVALID_FIELD_WEIGHT",INVALID_KEYWORD:"INVALID_KEYWORD",INVALID_HANDLER_RETURN:"INVALID_HANDLER_RETURN"};exports.ErrorCodes=L;exports.SpotrError=E;
|
|
2
|
+
//# sourceMappingURL=errors.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.cjs","sources":["../src/errors.ts"],"sourcesContent":["export class SpotrError extends Error {\n constructor(\n message: string,\n public code: string\n ) {\n super(message);\n this.name = 'SpotrError';\n }\n}\n\nexport const ErrorCodes = {\n INVALID_COLLECTION: 'INVALID_COLLECTION',\n INVALID_FIELD_CONFIG: 'INVALID_FIELD_CONFIG',\n INVALID_FIELD_WEIGHT: 'INVALID_FIELD_WEIGHT',\n INVALID_KEYWORD: 'INVALID_KEYWORD',\n INVALID_HANDLER_RETURN: 'INVALID_HANDLER_RETURN',\n} as const;\n\nexport type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];\n"],"names":["SpotrError","message","code","ErrorCodes"],"mappings":"gFAAO,MAAMA,UAAmB,KAAM,CACpC,YACEC,EACOC,EACP,CACA,MAAMD,CAAO,EAFN,KAAA,KAAAC,EAGP,KAAK,KAAO,YACd,CACF,CAEO,MAAMC,EAAa,CACxB,mBAAoB,qBACpB,qBAAsB,uBACtB,qBAAsB,uBACtB,gBAAiB,kBACjB,uBAAwB,wBAC1B"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class D extends Error {
|
|
2
|
+
constructor(I, L) {
|
|
3
|
+
super(I), this.code = L, this.name = "SpotrError";
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
const E = {
|
|
7
|
+
INVALID_COLLECTION: "INVALID_COLLECTION",
|
|
8
|
+
INVALID_FIELD_CONFIG: "INVALID_FIELD_CONFIG",
|
|
9
|
+
INVALID_FIELD_WEIGHT: "INVALID_FIELD_WEIGHT",
|
|
10
|
+
INVALID_KEYWORD: "INVALID_KEYWORD",
|
|
11
|
+
INVALID_HANDLER_RETURN: "INVALID_HANDLER_RETURN"
|
|
12
|
+
};
|
|
13
|
+
export {
|
|
14
|
+
E as ErrorCodes,
|
|
15
|
+
D as SpotrError
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sources":["../src/errors.ts"],"sourcesContent":["export class SpotrError extends Error {\n constructor(\n message: string,\n public code: string\n ) {\n super(message);\n this.name = 'SpotrError';\n }\n}\n\nexport const ErrorCodes = {\n INVALID_COLLECTION: 'INVALID_COLLECTION',\n INVALID_FIELD_CONFIG: 'INVALID_FIELD_CONFIG',\n INVALID_FIELD_WEIGHT: 'INVALID_FIELD_WEIGHT',\n INVALID_KEYWORD: 'INVALID_KEYWORD',\n INVALID_HANDLER_RETURN: 'INVALID_HANDLER_RETURN',\n} as const;\n\nexport type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];\n"],"names":["SpotrError","message","code","ErrorCodes"],"mappings":"AAAO,MAAMA,UAAmB,MAAM;AAAA,EACpC,YACEC,GACOC,GACP;AACA,UAAMD,CAAO,GAFN,KAAA,OAAAC,GAGP,KAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAMC,IAAa;AAAA,EACxB,oBAAoB;AAAA,EACpB,sBAAsB;AAAA,EACtB,sBAAsB;AAAA,EACtB,iBAAiB;AAAA,EACjB,wBAAwB;AAC1B;"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function h(l,r){const n=[];for(let t=0;t<=r.length;t++)n[t]=[t];for(let t=0;t<=l.length;t++)n[0][t]=t;for(let t=1;t<=r.length;t++)for(let e=1;e<=l.length;e++)r.charAt(t-1)===l.charAt(e-1)?n[t][e]=n[t-1][e-1]:n[t][e]=Math.min(n[t-1][e-1]+1,n[t][e-1]+1,n[t-1][e]+1);return n[r.length][l.length]}function f(l,r,n=.3,t=!1){const e=t?l:l.toLowerCase(),o=t?r:r.toLowerCase();if(e===o)return 1;if(e.length===0||o.length===0)return 0;if(o.includes(e))return .9+.1*e.length/o.length;const i=h(e,o),s=Math.max(e.length,o.length),c=1-i/s;return c>=n?c:0}exports.fuzzyScore=f;exports.levenshteinDistance=h;
|
|
2
|
+
//# sourceMappingURL=levenshtein.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"levenshtein.cjs","sources":["../../src/fuzzy/levenshtein.ts"],"sourcesContent":["export function levenshteinDistance(a: string, b: string): number {\n const matrix: number[][] = [];\n\n for (let i = 0; i <= b.length; i++) {\n matrix[i] = [i];\n }\n for (let j = 0; j <= a.length; j++) {\n matrix[0][j] = j;\n }\n\n for (let i = 1; i <= b.length; i++) {\n for (let j = 1; j <= a.length; j++) {\n if (b.charAt(i - 1) === a.charAt(j - 1)) {\n matrix[i][j] = matrix[i - 1][j - 1];\n } else {\n matrix[i][j] = Math.min(\n matrix[i - 1][j - 1] + 1,\n matrix[i][j - 1] + 1,\n matrix[i - 1][j] + 1\n );\n }\n }\n }\n\n return matrix[b.length][a.length];\n}\n\nexport function fuzzyScore(\n query: string,\n target: string,\n threshold: number = 0.3,\n caseSensitive: boolean = false\n): number {\n const q = caseSensitive ? query : query.toLowerCase();\n const t = caseSensitive ? target : target.toLowerCase();\n\n if (q === t) return 1;\n if (q.length === 0 || t.length === 0) return 0;\n\n if (t.includes(q)) {\n return 0.9 + (0.1 * q.length / t.length);\n }\n\n const distance = levenshteinDistance(q, t);\n const maxLen = Math.max(q.length, t.length);\n const score = 1 - (distance / maxLen);\n\n return score >= threshold ? score : 0;\n}\n"],"names":["levenshteinDistance","a","b","matrix","i","j","fuzzyScore","query","target","threshold","caseSensitive","q","t","distance","maxLen","score"],"mappings":"gFAAO,SAASA,EAAoBC,EAAWC,EAAmB,CAChE,MAAMC,EAAqB,CAAA,EAE3B,QAASC,EAAI,EAAGA,GAAKF,EAAE,OAAQE,IAC7BD,EAAOC,CAAC,EAAI,CAACA,CAAC,EAEhB,QAASC,EAAI,EAAGA,GAAKJ,EAAE,OAAQI,IAC7BF,EAAO,CAAC,EAAEE,CAAC,EAAIA,EAGjB,QAASD,EAAI,EAAGA,GAAKF,EAAE,OAAQE,IAC7B,QAASC,EAAI,EAAGA,GAAKJ,EAAE,OAAQI,IACzBH,EAAE,OAAOE,EAAI,CAAC,IAAMH,EAAE,OAAOI,EAAI,CAAC,EACpCF,EAAOC,CAAC,EAAEC,CAAC,EAAIF,EAAOC,EAAI,CAAC,EAAEC,EAAI,CAAC,EAElCF,EAAOC,CAAC,EAAEC,CAAC,EAAI,KAAK,IAClBF,EAAOC,EAAI,CAAC,EAAEC,EAAI,CAAC,EAAI,EACvBF,EAAOC,CAAC,EAAEC,EAAI,CAAC,EAAI,EACnBF,EAAOC,EAAI,CAAC,EAAEC,CAAC,EAAI,CAAA,EAM3B,OAAOF,EAAOD,EAAE,MAAM,EAAED,EAAE,MAAM,CAClC,CAEO,SAASK,EACdC,EACAC,EACAC,EAAoB,GACpBC,EAAyB,GACjB,CACR,MAAMC,EAAID,EAAgBH,EAAQA,EAAM,YAAA,EAClCK,EAAIF,EAAgBF,EAASA,EAAO,YAAA,EAE1C,GAAIG,IAAMC,EAAG,MAAO,GACpB,GAAID,EAAE,SAAW,GAAKC,EAAE,SAAW,EAAG,MAAO,GAE7C,GAAIA,EAAE,SAASD,CAAC,EACd,MAAO,IAAO,GAAMA,EAAE,OAASC,EAAE,OAGnC,MAAMC,EAAWb,EAAoBW,EAAGC,CAAC,EACnCE,EAAS,KAAK,IAAIH,EAAE,OAAQC,EAAE,MAAM,EACpCG,EAAQ,EAAKF,EAAWC,EAE9B,OAAOC,GAASN,EAAYM,EAAQ,CACtC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
function f(l, r) {
|
|
2
|
+
const n = [];
|
|
3
|
+
for (let t = 0; t <= r.length; t++)
|
|
4
|
+
n[t] = [t];
|
|
5
|
+
for (let t = 0; t <= l.length; t++)
|
|
6
|
+
n[0][t] = t;
|
|
7
|
+
for (let t = 1; t <= r.length; t++)
|
|
8
|
+
for (let e = 1; e <= l.length; e++)
|
|
9
|
+
r.charAt(t - 1) === l.charAt(e - 1) ? n[t][e] = n[t - 1][e - 1] : n[t][e] = Math.min(
|
|
10
|
+
n[t - 1][e - 1] + 1,
|
|
11
|
+
n[t][e - 1] + 1,
|
|
12
|
+
n[t - 1][e] + 1
|
|
13
|
+
);
|
|
14
|
+
return n[r.length][l.length];
|
|
15
|
+
}
|
|
16
|
+
function i(l, r, n = 0.3, t = !1) {
|
|
17
|
+
const e = t ? l : l.toLowerCase(), o = t ? r : r.toLowerCase();
|
|
18
|
+
if (e === o) return 1;
|
|
19
|
+
if (e.length === 0 || o.length === 0) return 0;
|
|
20
|
+
if (o.includes(e))
|
|
21
|
+
return 0.9 + 0.1 * e.length / o.length;
|
|
22
|
+
const c = f(e, o), s = Math.max(e.length, o.length), h = 1 - c / s;
|
|
23
|
+
return h >= n ? h : 0;
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
i as fuzzyScore,
|
|
27
|
+
f as levenshteinDistance
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=levenshtein.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"levenshtein.js","sources":["../../src/fuzzy/levenshtein.ts"],"sourcesContent":["export function levenshteinDistance(a: string, b: string): number {\n const matrix: number[][] = [];\n\n for (let i = 0; i <= b.length; i++) {\n matrix[i] = [i];\n }\n for (let j = 0; j <= a.length; j++) {\n matrix[0][j] = j;\n }\n\n for (let i = 1; i <= b.length; i++) {\n for (let j = 1; j <= a.length; j++) {\n if (b.charAt(i - 1) === a.charAt(j - 1)) {\n matrix[i][j] = matrix[i - 1][j - 1];\n } else {\n matrix[i][j] = Math.min(\n matrix[i - 1][j - 1] + 1,\n matrix[i][j - 1] + 1,\n matrix[i - 1][j] + 1\n );\n }\n }\n }\n\n return matrix[b.length][a.length];\n}\n\nexport function fuzzyScore(\n query: string,\n target: string,\n threshold: number = 0.3,\n caseSensitive: boolean = false\n): number {\n const q = caseSensitive ? query : query.toLowerCase();\n const t = caseSensitive ? target : target.toLowerCase();\n\n if (q === t) return 1;\n if (q.length === 0 || t.length === 0) return 0;\n\n if (t.includes(q)) {\n return 0.9 + (0.1 * q.length / t.length);\n }\n\n const distance = levenshteinDistance(q, t);\n const maxLen = Math.max(q.length, t.length);\n const score = 1 - (distance / maxLen);\n\n return score >= threshold ? score : 0;\n}\n"],"names":["levenshteinDistance","a","b","matrix","i","j","fuzzyScore","query","target","threshold","caseSensitive","q","t","distance","maxLen","score"],"mappings":"AAAO,SAASA,EAAoBC,GAAWC,GAAmB;AAChE,QAAMC,IAAqB,CAAA;AAE3B,WAASC,IAAI,GAAGA,KAAKF,EAAE,QAAQE;AAC7B,IAAAD,EAAOC,CAAC,IAAI,CAACA,CAAC;AAEhB,WAASC,IAAI,GAAGA,KAAKJ,EAAE,QAAQI;AAC7B,IAAAF,EAAO,CAAC,EAAEE,CAAC,IAAIA;AAGjB,WAASD,IAAI,GAAGA,KAAKF,EAAE,QAAQE;AAC7B,aAASC,IAAI,GAAGA,KAAKJ,EAAE,QAAQI;AAC7B,MAAIH,EAAE,OAAOE,IAAI,CAAC,MAAMH,EAAE,OAAOI,IAAI,CAAC,IACpCF,EAAOC,CAAC,EAAEC,CAAC,IAAIF,EAAOC,IAAI,CAAC,EAAEC,IAAI,CAAC,IAElCF,EAAOC,CAAC,EAAEC,CAAC,IAAI,KAAK;AAAA,QAClBF,EAAOC,IAAI,CAAC,EAAEC,IAAI,CAAC,IAAI;AAAA,QACvBF,EAAOC,CAAC,EAAEC,IAAI,CAAC,IAAI;AAAA,QACnBF,EAAOC,IAAI,CAAC,EAAEC,CAAC,IAAI;AAAA,MAAA;AAM3B,SAAOF,EAAOD,EAAE,MAAM,EAAED,EAAE,MAAM;AAClC;AAEO,SAASK,EACdC,GACAC,GACAC,IAAoB,KACpBC,IAAyB,IACjB;AACR,QAAMC,IAAID,IAAgBH,IAAQA,EAAM,YAAA,GAClCK,IAAIF,IAAgBF,IAASA,EAAO,YAAA;AAE1C,MAAIG,MAAMC,EAAG,QAAO;AACpB,MAAID,EAAE,WAAW,KAAKC,EAAE,WAAW,EAAG,QAAO;AAE7C,MAAIA,EAAE,SAASD,CAAC;AACd,WAAO,MAAO,MAAMA,EAAE,SAASC,EAAE;AAGnC,QAAMC,IAAWb,EAAoBW,GAAGC,CAAC,GACnCE,IAAS,KAAK,IAAIH,EAAE,QAAQC,EAAE,MAAM,GACpCG,IAAQ,IAAKF,IAAWC;AAE9B,SAAOC,KAASN,IAAYM,IAAQ;AACtC;"}
|