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.
Files changed (81) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +129 -141
  3. package/dist/Spotr.cjs +2 -0
  4. package/dist/Spotr.cjs.map +1 -0
  5. package/dist/Spotr.js +163 -0
  6. package/dist/Spotr.js.map +1 -0
  7. package/dist/errors.cjs +2 -0
  8. package/dist/errors.cjs.map +1 -0
  9. package/dist/errors.js +17 -0
  10. package/dist/errors.js.map +1 -0
  11. package/dist/fuzzy/levenshtein.cjs +2 -0
  12. package/dist/fuzzy/levenshtein.cjs.map +1 -0
  13. package/dist/fuzzy/levenshtein.js +29 -0
  14. package/dist/fuzzy/levenshtein.js.map +1 -0
  15. package/dist/fuzzy/scorer.cjs +2 -0
  16. package/dist/fuzzy/scorer.cjs.map +1 -0
  17. package/dist/fuzzy/scorer.js +36 -0
  18. package/dist/fuzzy/scorer.js.map +1 -0
  19. package/dist/index.cjs +2 -0
  20. package/dist/index.cjs.map +1 -0
  21. package/dist/index.d.ts +93 -0
  22. package/dist/index.js +8 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/preact.cjs +2 -0
  25. package/dist/preact.cjs.map +1 -0
  26. package/dist/preact.d.ts +71 -0
  27. package/dist/preact.js +24 -0
  28. package/dist/preact.js.map +1 -0
  29. package/dist/react.cjs +2 -0
  30. package/dist/react.cjs.map +1 -0
  31. package/dist/react.d.ts +71 -0
  32. package/dist/react.js +24 -0
  33. package/dist/react.js.map +1 -0
  34. package/dist/solid.cjs +2 -0
  35. package/dist/solid.cjs.map +1 -0
  36. package/dist/solid.d.ts +95 -0
  37. package/dist/solid.js +11 -0
  38. package/dist/solid.js.map +1 -0
  39. package/dist/svelte.cjs +2 -0
  40. package/dist/svelte.cjs.map +1 -0
  41. package/dist/svelte.d.ts +95 -0
  42. package/dist/svelte.js +11 -0
  43. package/dist/svelte.js.map +1 -0
  44. package/dist/utils/nested.cjs +2 -0
  45. package/dist/utils/nested.cjs.map +1 -0
  46. package/dist/utils/nested.js +15 -0
  47. package/dist/utils/nested.js.map +1 -0
  48. package/dist/utils/tokenize.cjs +2 -0
  49. package/dist/utils/tokenize.cjs.map +1 -0
  50. package/dist/utils/tokenize.js +7 -0
  51. package/dist/utils/tokenize.js.map +1 -0
  52. package/dist/utils/validate.cjs +2 -0
  53. package/dist/utils/validate.cjs.map +1 -0
  54. package/dist/utils/validate.js +97 -0
  55. package/dist/utils/validate.js.map +1 -0
  56. package/dist/vue.cjs +2 -0
  57. package/dist/vue.cjs.map +1 -0
  58. package/dist/vue.d.ts +74 -0
  59. package/dist/vue.js +17 -0
  60. package/dist/vue.js.map +1 -0
  61. package/package.json +99 -26
  62. package/.babelrc +0 -6
  63. package/.npmignore +0 -37
  64. package/dist/spotr.min.js +0 -1
  65. package/dist/spotr.min.js.map +0 -1
  66. package/examples/basic/.babelrc +0 -8
  67. package/examples/basic/.npmignore +0 -5
  68. package/examples/basic/README.md +0 -19
  69. package/examples/basic/companies.json +0 -101
  70. package/examples/basic/index.html +0 -11
  71. package/examples/basic/index.js +0 -2
  72. package/examples/basic/package.json +0 -29
  73. package/examples/basic/src/App.vue +0 -104
  74. package/examples/basic/src/main.js +0 -11
  75. package/examples/basic/src/search.js +0 -15
  76. package/examples/basic/webpack.config.js +0 -80
  77. package/examples/basic/yarn.lock +0 -3960
  78. package/index.js +0 -1
  79. package/src/spotr.js +0 -120
  80. package/webpack.config.js +0 -63
  81. package/yarn.lock +0 -3125
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2017 Andy Merskin
3
+ Copyright (c) 2025 Andy Merskin
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,7 +1,17 @@
1
- # spotr
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
- Great for prototyping search systems during development, or for apps and sites with smaller collections that are being requested and filtered up front.
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
- ## Getting Started
13
-
14
- When creating a new instance of Spotr, it accepts a `collection`, `field` filters, and `keyword` filters.
15
-
16
- ```javascript
17
- import Spotr from 'spotr'
18
- import products from 'products.csv'
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
- const search = new Spotr({
21
-
22
- // Collection of objects to search on
23
- collection: products,
48
+ ## Options
24
49
 
25
- // Full-text fields to match search query against
26
- fields: {
50
+ ### `collection` (required)
27
51
 
28
- // item.name
29
- name: {
30
- fuzzy: 0,
31
- boost: 9
32
- },
52
+ Array or Set of objects to search.
33
53
 
34
- // item.category
35
- category: {
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
- ### Collection
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
- ```javascript
59
- import products from 'products.csv'
60
+ Properties to search against with weight configuration. Supports dot notation for nested objects.
60
61
 
61
- const search = new Spotr({
62
- collection: products
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
- ### Fields
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
- ```javascript
71
- fields: {
72
- title: {
73
- boost: 9,
74
- fuzzy: 0.3
75
- },
76
- category: {
77
- boost: 8
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
- `fuzzy` is number between 0 and 1 passed to the `string-score` library, which determines how relaxed to be while scoring. 0 means exact matches, and 1 allows for misspellings.
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
- | property | boost value |
89
- | --- | --- |
90
- | title | 9 |
91
- | category | 8 |
92
- | label | 7 |
93
- | author | 6 |
100
+ Global fuzzy matching threshold. Default: `0.3`
94
101
 
95
- When multiple fields on an item have positive matches, their scores are added together. Finally, the items' total scores are sorted to return the most relevant results.
102
+ - `0` = match anything (no filtering)
103
+ - `0.3` = recommended default
104
+ - `1` = exact match required
96
105
 
106
+ ### `limit` (optional)
97
107
 
98
- ### Keywords
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
- ```javascript
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
- `queries` is an array of query strings to match. They can be one word, or multiple words, but must match exactly for the filter to be applied.
112
+ Enable case-sensitive matching. Default: `false`
111
113
 
112
- `filter` is a function that takes in the `collection` where mutations can safely be applied and returned.
114
+ ### `minMatchCharLength` (optional)
113
115
 
114
- #### Keywords are applied additively
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
- #### Search query refinement
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
- ```javascript
121
- keywords: {
122
- clearance: {
123
- queries: ['clearance', 'sale', 'for sale', 'on sale'],
124
- filter: collection => collection.filter(item => item.clearance)
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
- ...and we searched for **red flannel shirts on sale**, our search query used in our fields would be **red flannel shirts**, because "on sale" was matched, since we know "on sale" won't show up in the names or categories of our products.
129
+ ## React Hook
130
130
 
131
- ## Instance Methods
131
+ ```typescript
132
+ import { useSpotr } from 'spotr/react';
132
133
 
133
- ### query(searchString)
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
- Query the collection with a search string.
149
+ ## Vue Composable
136
150
 
137
- - `searchString` - The string query to match against the [`keywords`](#keywords) and [`fields`](#fields) definitions.
151
+ ```typescript
152
+ import { useSpotr } from 'spotr/vue';
138
153
 
139
- Returns the filtered collection after being processed by keyword and field filters.
154
+ const games = ref<Game[]>([]);
155
+ const query = ref('');
140
156
 
141
- ```javascript
142
- const search = new Spotr({
143
- fields: {
144
- gameTitle: {
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
- search.query('party games for all ages')
157
- // => Array of games for all ages
162
+ const results = computed(() => spotr.value?.query(query.value));
158
163
  ```
159
164
 
160
- ### collection
165
+ ## API Methods
161
166
 
162
- Get or set the collection Spotr is searching against.
167
+ ### `query(search: string): SpotrResult<T>`
163
168
 
164
- - `collection` - An array of objects.
169
+ Search the collection with a string query.
165
170
 
166
- ```javascript
167
- const search = new Spotr({ ... })
171
+ ### `queryAsync(search: string): Promise<SpotrResult<T>>`
168
172
 
169
- // Get the collection
170
- search.collection // [ item, item ...]
173
+ Async search with optional debouncing.
171
174
 
172
- // Set the collection
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
- ## Contributors
177
+ Update the collection.
182
178
 
183
- ### Developing
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
- ### Building
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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"}
@@ -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;"}