This commit is contained in:
1957
node_modules/minisearch/src/MiniSearch.test.js
generated
vendored
Normal file
1957
node_modules/minisearch/src/MiniSearch.test.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2227
node_modules/minisearch/src/MiniSearch.ts
generated
vendored
Normal file
2227
node_modules/minisearch/src/MiniSearch.ts
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
314
node_modules/minisearch/src/SearchableMap/SearchableMap.test.js
generated
vendored
Normal file
314
node_modules/minisearch/src/SearchableMap/SearchableMap.test.js
generated
vendored
Normal file
@@ -0,0 +1,314 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import SearchableMap from './SearchableMap'
|
||||
import * as fc from 'fast-check'
|
||||
|
||||
describe('SearchableMap', () => {
|
||||
const strings = ['bin', 'border', 'acqua', 'aqua', 'poisson', 'parachute',
|
||||
'parapendio', 'acquamarina', 'summertime', 'summer', 'join', 'mediterraneo',
|
||||
'perciò', 'borderline', 'bo']
|
||||
const keyValues = strings.map((key, i) => [key, i])
|
||||
const object = keyValues.reduce((obj, [key, value]) => ({ ...obj, [key]: value }))
|
||||
|
||||
const editDistance = function (a, b, mem = [[0]]) {
|
||||
mem[a.length] = mem[a.length] || [a.length]
|
||||
if (mem[a.length][b.length] !== undefined) { return mem[a.length][b.length] }
|
||||
const d = (a[a.length - 1] === b[b.length - 1]) ? 0 : 1
|
||||
const distance = (a.length === 1 && b.length === 1)
|
||||
? d
|
||||
: Math.min(
|
||||
((a.length > 0) ? editDistance(a.slice(0, -1), b, mem) + 1 : Infinity),
|
||||
((b.length > 0) ? editDistance(a, b.slice(0, -1), mem) + 1 : Infinity),
|
||||
((a.length > 0 && b.length > 0) ? editDistance(a.slice(0, -1), b.slice(0, -1), mem) + d : Infinity)
|
||||
)
|
||||
mem[a.length][b.length] = distance
|
||||
return distance
|
||||
}
|
||||
|
||||
describe('clear', () => {
|
||||
it('empties the map', () => {
|
||||
const map = SearchableMap.from(keyValues)
|
||||
map.clear()
|
||||
expect(Array.from(map.entries())).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete', () => {
|
||||
it('deletes the entry at the given key', () => {
|
||||
const map = SearchableMap.from(keyValues)
|
||||
map.delete('border')
|
||||
expect(map.has('border')).toBe(false)
|
||||
expect(map.has('summer')).toBe(true)
|
||||
expect(map.has('borderline')).toBe(true)
|
||||
expect(map.has('bo')).toBe(true)
|
||||
})
|
||||
|
||||
it('changes the size of the map', () => {
|
||||
const map = SearchableMap.from(keyValues)
|
||||
const sizeBefore = map.size
|
||||
map.delete('summertime')
|
||||
expect(map.size).toEqual(sizeBefore - 1)
|
||||
})
|
||||
|
||||
it('does nothing if the entry did not exist', () => {
|
||||
const map = new SearchableMap()
|
||||
expect(() => map.delete('something')).not.toThrow()
|
||||
})
|
||||
|
||||
it('leaves the radix tree in the same state as before the entry was added', () => {
|
||||
const map = new SearchableMap()
|
||||
|
||||
map.set('hello', 1)
|
||||
const before = new SearchableMap(new Map(map._tree))
|
||||
|
||||
map.set('help', 2)
|
||||
map.delete('help')
|
||||
|
||||
expect(map).toEqual(before)
|
||||
})
|
||||
})
|
||||
|
||||
describe('entries', () => {
|
||||
it('returns an iterator of entries', () => {
|
||||
const map = SearchableMap.from(keyValues)
|
||||
const entries = Array.from({ [Symbol.iterator]: () => map.entries() })
|
||||
expect(entries.sort()).toEqual(keyValues.sort())
|
||||
})
|
||||
|
||||
it('returns an iterable of entries', () => {
|
||||
const map = SearchableMap.from(keyValues)
|
||||
const entries = Array.from(map.entries())
|
||||
expect(entries.sort()).toEqual(keyValues.sort())
|
||||
})
|
||||
|
||||
it('returns empty iterator, if the map is empty', () => {
|
||||
const map = new SearchableMap()
|
||||
const entries = Array.from(map.entries())
|
||||
expect(entries).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('forEach', () => {
|
||||
it('iterates through each entry', () => {
|
||||
const entries = []
|
||||
const fn = (key, value) => entries.push([key, value])
|
||||
const map = SearchableMap.from(keyValues)
|
||||
map.forEach(fn)
|
||||
expect(entries).toEqual(Array.from(map.entries()))
|
||||
})
|
||||
})
|
||||
|
||||
describe('get', () => {
|
||||
it('gets the value at key', () => {
|
||||
const key = 'foo'
|
||||
const value = 42
|
||||
const map = SearchableMap.fromObject({ [key]: value })
|
||||
expect(map.get(key)).toBe(value)
|
||||
})
|
||||
|
||||
it('returns undefined if the key is not present', () => {
|
||||
const map = new SearchableMap()
|
||||
expect(map.get('not-existent')).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe('has', () => {
|
||||
it('returns true if the given key exists in the map', () => {
|
||||
const map = new SearchableMap()
|
||||
map.set('something', 42)
|
||||
expect(map.has('something')).toBe(true)
|
||||
|
||||
map.set('something else', null)
|
||||
expect(map.has('something else')).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false if the given key does not exist in the map', () => {
|
||||
const map = SearchableMap.fromObject({ something: 42 })
|
||||
expect(map.has('not-existing')).toBe(false)
|
||||
expect(map.has('some')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('keys', () => {
|
||||
it('returns an iterator of keys', () => {
|
||||
const map = SearchableMap.from(keyValues)
|
||||
const keys = Array.from({ [Symbol.iterator]: () => map.keys() })
|
||||
expect(keys.sort()).toEqual(strings.sort())
|
||||
})
|
||||
|
||||
it('returns an iterable of keys', () => {
|
||||
const map = SearchableMap.from(keyValues)
|
||||
const keys = Array.from(map.keys())
|
||||
expect(keys.sort()).toEqual(strings.sort())
|
||||
})
|
||||
|
||||
it('returns empty iterator, if the map is empty', () => {
|
||||
const map = new SearchableMap()
|
||||
const keys = Array.from(map.keys())
|
||||
expect(keys).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('set', () => {
|
||||
it('sets a value at key', () => {
|
||||
const map = new SearchableMap()
|
||||
const key = 'foo'
|
||||
const value = 42
|
||||
map.set(key, value)
|
||||
expect(map.get(key)).toBe(value)
|
||||
})
|
||||
|
||||
it('overrides a value at key if it already exists', () => {
|
||||
const map = SearchableMap.fromObject({ foo: 123 })
|
||||
const key = 'foo'
|
||||
const value = 42
|
||||
map.set(key, value)
|
||||
expect(map.get(key)).toBe(value)
|
||||
})
|
||||
|
||||
it('throws error if the given key is not a string', () => {
|
||||
const map = new SearchableMap()
|
||||
expect(() => map.set(123, 'foo')).toThrow('key must be a string')
|
||||
})
|
||||
})
|
||||
|
||||
describe('size', () => {
|
||||
it('is a property containing the size of the map', () => {
|
||||
const map = SearchableMap.from(keyValues)
|
||||
expect(map.size).toEqual(keyValues.length)
|
||||
map.set('foo', 42)
|
||||
expect(map.size).toEqual(keyValues.length + 1)
|
||||
map.delete('border')
|
||||
expect(map.size).toEqual(keyValues.length)
|
||||
map.clear()
|
||||
expect(map.size).toEqual(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('update', () => {
|
||||
it('sets a value at key applying a function to the previous value', () => {
|
||||
const map = new SearchableMap()
|
||||
const key = 'foo'
|
||||
const fn = jest.fn(x => (x || 0) + 1)
|
||||
map.update(key, fn)
|
||||
expect(fn).toHaveBeenCalledWith(undefined)
|
||||
expect(map.get(key)).toBe(1)
|
||||
map.update(key, fn)
|
||||
expect(fn).toHaveBeenCalledWith(1)
|
||||
expect(map.get(key)).toBe(2)
|
||||
})
|
||||
|
||||
it('throws error if the given key is not a string', () => {
|
||||
const map = new SearchableMap()
|
||||
expect(() => map.update(123, () => {})).toThrow('key must be a string')
|
||||
})
|
||||
})
|
||||
|
||||
describe('values', () => {
|
||||
it('returns an iterator of values', () => {
|
||||
const map = SearchableMap.fromObject(object)
|
||||
const values = Array.from({ [Symbol.iterator]: () => map.values() })
|
||||
expect(values.sort()).toEqual(Object.values(object).sort())
|
||||
})
|
||||
|
||||
it('returns an iterable of values', () => {
|
||||
const map = SearchableMap.fromObject(object)
|
||||
const values = Array.from(map.values())
|
||||
expect(values.sort()).toEqual(Object.values(object).sort())
|
||||
})
|
||||
|
||||
it('returns empty iterator, if the map is empty', () => {
|
||||
const map = new SearchableMap()
|
||||
const values = Array.from(map.values())
|
||||
expect(values).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('atPrefix', () => {
|
||||
it('returns the submap at the given prefix', () => {
|
||||
const map = SearchableMap.from(keyValues)
|
||||
|
||||
const sum = map.atPrefix('sum')
|
||||
expect(Array.from(sum.keys()).sort()).toEqual(strings.filter(string => string.startsWith('sum')).sort())
|
||||
|
||||
const summer = sum.atPrefix('summer')
|
||||
expect(Array.from(summer.keys()).sort()).toEqual(strings.filter(string => string.startsWith('summer')).sort())
|
||||
|
||||
const xyz = map.atPrefix('xyz')
|
||||
expect(Array.from(xyz.keys())).toEqual([])
|
||||
|
||||
expect(() => sum.atPrefix('xyz')).toThrow()
|
||||
})
|
||||
|
||||
it('correctly computes the size', () => {
|
||||
const map = SearchableMap.from(keyValues)
|
||||
const sum = map.atPrefix('sum')
|
||||
expect(sum.size).toEqual(strings.filter(string => string.startsWith('sum')).length)
|
||||
})
|
||||
})
|
||||
|
||||
describe('fuzzyGet', () => {
|
||||
const terms = ['summer', 'acqua', 'aqua', 'acquire', 'poisson', 'qua']
|
||||
const keyValues = terms.map((key, i) => [key, i])
|
||||
const map = SearchableMap.from(keyValues)
|
||||
|
||||
it('returns all entries having the given maximum edit distance from the given key', () => {
|
||||
[0, 1, 2, 3].forEach(distance => {
|
||||
const results = map.fuzzyGet('acqua', distance)
|
||||
const entries = Array.from(results)
|
||||
expect(entries.map(([key, [value, dist]]) => [key, dist]).sort())
|
||||
.toEqual(terms.map(term => [term, editDistance('acqua', term)]).filter(([, d]) => d <= distance).sort())
|
||||
expect(entries.every(([key, [value]]) => map.get(key) === value)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
it('returns an empty object if no matching entries are found', () => {
|
||||
expect(map.fuzzyGet('winter', 1)).toEqual(new Map())
|
||||
})
|
||||
|
||||
it('returns entries if edit distance is longer than key', () => {
|
||||
const map = SearchableMap.from([['x', 1], [' x', 2]])
|
||||
expect(Array.from(map.fuzzyGet('x', 2).values())).toEqual([[1, 0], [2, 1]])
|
||||
})
|
||||
})
|
||||
|
||||
describe('with generated test data', () => {
|
||||
it('adds and removes entries', () => {
|
||||
const arrayOfStrings = fc.array(fc.oneof(fc.unicodeString(), fc.string()), { maxLength: 70 })
|
||||
const string = fc.oneof(fc.unicodeString({ minLength: 0, maxLength: 4 }), fc.string({ minLength: 0, maxLength: 4 }))
|
||||
const int = fc.integer({ min: 1, max: 4 })
|
||||
|
||||
fc.assert(fc.property(arrayOfStrings, string, int, (terms, prefix, maxDist) => {
|
||||
const map = new SearchableMap()
|
||||
const standardMap = new Map()
|
||||
const uniqueTerms = [...new Set(terms)]
|
||||
|
||||
terms.forEach((term, i) => {
|
||||
map.set(term, i)
|
||||
standardMap.set(term, i)
|
||||
expect(map.has(term)).toBe(true)
|
||||
expect(standardMap.get(term)).toEqual(i)
|
||||
})
|
||||
|
||||
expect(map.size).toEqual(standardMap.size)
|
||||
expect(Array.from(map.entries()).sort()).toEqual(Array.from(standardMap.entries()).sort())
|
||||
|
||||
expect(Array.from(map.atPrefix(prefix).keys()).sort())
|
||||
.toEqual(Array.from(new Set(terms)).filter(t => t.startsWith(prefix)).sort())
|
||||
|
||||
const fuzzy = map.fuzzyGet(terms[0], maxDist)
|
||||
expect(Array.from(fuzzy, ([key, [value, dist]]) => [key, dist]).sort())
|
||||
.toEqual(uniqueTerms.map(term => [term, editDistance(terms[0], term)])
|
||||
.filter(([, dist]) => dist <= maxDist).sort())
|
||||
|
||||
terms.forEach(term => {
|
||||
map.delete(term)
|
||||
expect(map.has(term)).toBe(false)
|
||||
expect(map.get(term)).toEqual(undefined)
|
||||
})
|
||||
|
||||
expect(map.size).toEqual(0)
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
424
node_modules/minisearch/src/SearchableMap/SearchableMap.ts
generated
vendored
Normal file
424
node_modules/minisearch/src/SearchableMap/SearchableMap.ts
generated
vendored
Normal file
@@ -0,0 +1,424 @@
|
||||
/* eslint-disable no-labels */
|
||||
import { TreeIterator, ENTRIES, KEYS, VALUES, LEAF } from './TreeIterator'
|
||||
import fuzzySearch, { type FuzzyResults } from './fuzzySearch'
|
||||
import type { RadixTree, Entry, Path } from './types'
|
||||
|
||||
/**
|
||||
* A class implementing the same interface as a standard JavaScript
|
||||
* [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)
|
||||
* with string keys, but adding support for efficiently searching entries with
|
||||
* prefix or fuzzy search. This class is used internally by {@link MiniSearch}
|
||||
* as the inverted index data structure. The implementation is a radix tree
|
||||
* (compressed prefix tree).
|
||||
*
|
||||
* Since this class can be of general utility beyond _MiniSearch_, it is
|
||||
* exported by the `minisearch` package and can be imported (or required) as
|
||||
* `minisearch/SearchableMap`.
|
||||
*
|
||||
* @typeParam T The type of the values stored in the map.
|
||||
*/
|
||||
export default class SearchableMap<T = any> {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_tree: RadixTree<T>
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_prefix: string
|
||||
|
||||
private _size: number | undefined = undefined
|
||||
|
||||
/**
|
||||
* The constructor is normally called without arguments, creating an empty
|
||||
* map. In order to create a {@link SearchableMap} from an iterable or from an
|
||||
* object, check {@link SearchableMap.from} and {@link
|
||||
* SearchableMap.fromObject}.
|
||||
*
|
||||
* The constructor arguments are for internal use, when creating derived
|
||||
* mutable views of a map at a prefix.
|
||||
*/
|
||||
constructor (tree: RadixTree<T> = new Map(), prefix = '') {
|
||||
this._tree = tree
|
||||
this._prefix = prefix
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a mutable view of this {@link SearchableMap},
|
||||
* containing only entries that share the given prefix.
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* ```javascript
|
||||
* let map = new SearchableMap()
|
||||
* map.set("unicorn", 1)
|
||||
* map.set("universe", 2)
|
||||
* map.set("university", 3)
|
||||
* map.set("unique", 4)
|
||||
* map.set("hello", 5)
|
||||
*
|
||||
* let uni = map.atPrefix("uni")
|
||||
* uni.get("unique") // => 4
|
||||
* uni.get("unicorn") // => 1
|
||||
* uni.get("hello") // => undefined
|
||||
*
|
||||
* let univer = map.atPrefix("univer")
|
||||
* univer.get("unique") // => undefined
|
||||
* univer.get("universe") // => 2
|
||||
* univer.get("university") // => 3
|
||||
* ```
|
||||
*
|
||||
* @param prefix The prefix
|
||||
* @return A {@link SearchableMap} representing a mutable view of the original
|
||||
* Map at the given prefix
|
||||
*/
|
||||
atPrefix (prefix: string): SearchableMap<T> {
|
||||
if (!prefix.startsWith(this._prefix)) { throw new Error('Mismatched prefix') }
|
||||
|
||||
const [node, path] = trackDown(this._tree, prefix.slice(this._prefix.length))
|
||||
|
||||
if (node === undefined) {
|
||||
const [parentNode, key] = last(path)
|
||||
|
||||
for (const k of parentNode!.keys()) {
|
||||
if (k !== LEAF && k.startsWith(key)) {
|
||||
const node = new Map()
|
||||
node.set(k.slice(key.length), parentNode!.get(k)!)
|
||||
return new SearchableMap(node, prefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new SearchableMap<T>(node, prefix)
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/clear
|
||||
*/
|
||||
clear (): void {
|
||||
this._size = undefined
|
||||
this._tree.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/delete
|
||||
* @param key Key to delete
|
||||
*/
|
||||
delete (key: string): void {
|
||||
this._size = undefined
|
||||
return remove(this._tree, key)
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/entries
|
||||
* @return An iterator iterating through `[key, value]` entries.
|
||||
*/
|
||||
entries () {
|
||||
return new TreeIterator(this, ENTRIES)
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/forEach
|
||||
* @param fn Iteration function
|
||||
*/
|
||||
forEach (fn: (key: string, value: T, map: SearchableMap) => void): void {
|
||||
for (const [key, value] of this) {
|
||||
fn(key, value, this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Map of all the entries that have a key within the given edit
|
||||
* distance from the search key. The keys of the returned Map are the matching
|
||||
* keys, while the values are two-element arrays where the first element is
|
||||
* the value associated to the key, and the second is the edit distance of the
|
||||
* key to the search key.
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* ```javascript
|
||||
* let map = new SearchableMap()
|
||||
* map.set('hello', 'world')
|
||||
* map.set('hell', 'yeah')
|
||||
* map.set('ciao', 'mondo')
|
||||
*
|
||||
* // Get all entries that match the key 'hallo' with a maximum edit distance of 2
|
||||
* map.fuzzyGet('hallo', 2)
|
||||
* // => Map(2) { 'hello' => ['world', 1], 'hell' => ['yeah', 2] }
|
||||
*
|
||||
* // In the example, the "hello" key has value "world" and edit distance of 1
|
||||
* // (change "e" to "a"), the key "hell" has value "yeah" and edit distance of 2
|
||||
* // (change "e" to "a", delete "o")
|
||||
* ```
|
||||
*
|
||||
* @param key The search key
|
||||
* @param maxEditDistance The maximum edit distance (Levenshtein)
|
||||
* @return A Map of the matching keys to their value and edit distance
|
||||
*/
|
||||
fuzzyGet (key: string, maxEditDistance: number): FuzzyResults<T> {
|
||||
return fuzzySearch<T>(this._tree, key, maxEditDistance)
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get
|
||||
* @param key Key to get
|
||||
* @return Value associated to the key, or `undefined` if the key is not
|
||||
* found.
|
||||
*/
|
||||
get (key: string): T | undefined {
|
||||
const node = lookup<T>(this._tree, key)
|
||||
return node !== undefined ? node.get(LEAF) : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/has
|
||||
* @param key Key
|
||||
* @return True if the key is in the map, false otherwise
|
||||
*/
|
||||
has (key: string): boolean {
|
||||
const node = lookup(this._tree, key)
|
||||
return node !== undefined && node.has(LEAF)
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/keys
|
||||
* @return An `Iterable` iterating through keys
|
||||
*/
|
||||
keys () {
|
||||
return new TreeIterator(this, KEYS)
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/set
|
||||
* @param key Key to set
|
||||
* @param value Value to associate to the key
|
||||
* @return The {@link SearchableMap} itself, to allow chaining
|
||||
*/
|
||||
set (key: string, value: T): SearchableMap<T> {
|
||||
if (typeof key !== 'string') { throw new Error('key must be a string') }
|
||||
this._size = undefined
|
||||
const node = createPath(this._tree, key)
|
||||
node.set(LEAF, value)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/size
|
||||
*/
|
||||
get size (): number {
|
||||
if (this._size) { return this._size }
|
||||
/** @ignore */
|
||||
this._size = 0
|
||||
|
||||
const iter = this.entries()
|
||||
while (!iter.next().done) this._size! += 1
|
||||
|
||||
return this._size
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the value at the given key using the provided function. The function
|
||||
* is called with the current value at the key, and its return value is used as
|
||||
* the new value to be set.
|
||||
*
|
||||
* ### Example:
|
||||
*
|
||||
* ```javascript
|
||||
* // Increment the current value by one
|
||||
* searchableMap.update('somekey', (currentValue) => currentValue == null ? 0 : currentValue + 1)
|
||||
* ```
|
||||
*
|
||||
* If the value at the given key is or will be an object, it might not require
|
||||
* re-assignment. In that case it is better to use `fetch()`, because it is
|
||||
* faster.
|
||||
*
|
||||
* @param key The key to update
|
||||
* @param fn The function used to compute the new value from the current one
|
||||
* @return The {@link SearchableMap} itself, to allow chaining
|
||||
*/
|
||||
update (key: string, fn: (value: T | undefined) => T): SearchableMap<T> {
|
||||
if (typeof key !== 'string') { throw new Error('key must be a string') }
|
||||
this._size = undefined
|
||||
const node = createPath(this._tree, key)
|
||||
node.set(LEAF, fn(node.get(LEAF)))
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the value of the given key. If the value does not exist, calls the
|
||||
* given function to create a new value, which is inserted at the given key
|
||||
* and subsequently returned.
|
||||
*
|
||||
* ### Example:
|
||||
*
|
||||
* ```javascript
|
||||
* const map = searchableMap.fetch('somekey', () => new Map())
|
||||
* map.set('foo', 'bar')
|
||||
* ```
|
||||
*
|
||||
* @param key The key to update
|
||||
* @param initial A function that creates a new value if the key does not exist
|
||||
* @return The existing or new value at the given key
|
||||
*/
|
||||
fetch (key: string, initial: () => T): T {
|
||||
if (typeof key !== 'string') { throw new Error('key must be a string') }
|
||||
this._size = undefined
|
||||
const node = createPath(this._tree, key)
|
||||
|
||||
let value = node.get(LEAF)
|
||||
if (value === undefined) {
|
||||
node.set(LEAF, value = initial())
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/values
|
||||
* @return An `Iterable` iterating through values.
|
||||
*/
|
||||
values () {
|
||||
return new TreeIterator(this, VALUES)
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/@@iterator
|
||||
*/
|
||||
[Symbol.iterator] () {
|
||||
return this.entries()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link SearchableMap} from an `Iterable` of entries
|
||||
*
|
||||
* @param entries Entries to be inserted in the {@link SearchableMap}
|
||||
* @return A new {@link SearchableMap} with the given entries
|
||||
*/
|
||||
static from<T = any> (entries: Iterable<Entry<T>> | Entry<T>[]) {
|
||||
const tree = new SearchableMap()
|
||||
for (const [key, value] of entries) {
|
||||
tree.set(key, value)
|
||||
}
|
||||
return tree
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link SearchableMap} from the iterable properties of a JavaScript object
|
||||
*
|
||||
* @param object Object of entries for the {@link SearchableMap}
|
||||
* @return A new {@link SearchableMap} with the given entries
|
||||
*/
|
||||
static fromObject<T = any> (object: { [key: string]: T }) {
|
||||
return SearchableMap.from<T>(Object.entries(object))
|
||||
}
|
||||
}
|
||||
|
||||
const trackDown = <T = any>(tree: RadixTree<T> | undefined, key: string, path: Path<T> = []): [RadixTree<T> | undefined, Path<T>] => {
|
||||
if (key.length === 0 || tree == null) { return [tree, path] }
|
||||
|
||||
for (const k of tree.keys()) {
|
||||
if (k !== LEAF && key.startsWith(k)) {
|
||||
path.push([tree, k]) // performance: update in place
|
||||
return trackDown(tree.get(k)!, key.slice(k.length), path)
|
||||
}
|
||||
}
|
||||
|
||||
path.push([tree, key]) // performance: update in place
|
||||
return trackDown(undefined, '', path)
|
||||
}
|
||||
|
||||
const lookup = <T = any>(tree: RadixTree<T>, key: string): RadixTree<T> | undefined => {
|
||||
if (key.length === 0 || tree == null) { return tree }
|
||||
|
||||
for (const k of tree.keys()) {
|
||||
if (k !== LEAF && key.startsWith(k)) {
|
||||
return lookup(tree.get(k)!, key.slice(k.length))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a path in the radix tree for the given key, and returns the deepest
|
||||
// node. This function is in the hot path for indexing. It avoids unnecessary
|
||||
// string operations and recursion for performance.
|
||||
const createPath = <T = any>(node: RadixTree<T>, key: string): RadixTree<T> => {
|
||||
const keyLength = key.length
|
||||
|
||||
outer: for (let pos = 0; node && pos < keyLength;) {
|
||||
for (const k of node.keys()) {
|
||||
// Check whether this key is a candidate: the first characters must match.
|
||||
if (k !== LEAF && key[pos] === k[0]) {
|
||||
const len = Math.min(keyLength - pos, k.length)
|
||||
|
||||
// Advance offset to the point where key and k no longer match.
|
||||
let offset = 1
|
||||
while (offset < len && key[pos + offset] === k[offset]) ++offset
|
||||
|
||||
const child = node.get(k)!
|
||||
if (offset === k.length) {
|
||||
// The existing key is shorter than the key we need to create.
|
||||
node = child
|
||||
} else {
|
||||
// Partial match: we need to insert an intermediate node to contain
|
||||
// both the existing subtree and the new node.
|
||||
const intermediate = new Map()
|
||||
intermediate.set(k.slice(offset), child)
|
||||
node.set(key.slice(pos, pos + offset), intermediate)
|
||||
node.delete(k)
|
||||
node = intermediate
|
||||
}
|
||||
|
||||
pos += offset
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
|
||||
// Create a final child node to contain the final suffix of the key.
|
||||
const child = new Map()
|
||||
node.set(key.slice(pos), child)
|
||||
return child
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
const remove = <T = any>(tree: RadixTree<T>, key: string): void => {
|
||||
const [node, path] = trackDown(tree, key)
|
||||
if (node === undefined) { return }
|
||||
node.delete(LEAF)
|
||||
|
||||
if (node.size === 0) {
|
||||
cleanup(path)
|
||||
} else if (node.size === 1) {
|
||||
const [key, value] = node.entries().next().value!
|
||||
merge(path, key as string, value as RadixTree<T>)
|
||||
}
|
||||
}
|
||||
|
||||
const cleanup = <T = any>(path: Path<T>): void => {
|
||||
if (path.length === 0) { return }
|
||||
|
||||
const [node, key] = last(path)
|
||||
node!.delete(key)
|
||||
|
||||
if (node!.size === 0) {
|
||||
cleanup(path.slice(0, -1))
|
||||
} else if (node!.size === 1) {
|
||||
const [key, value] = node!.entries().next().value!
|
||||
if (key !== LEAF) {
|
||||
merge(path.slice(0, -1), key as string, value as RadixTree<T>)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const merge = <T = any>(path: Path<T>, key: string, value: RadixTree<T>): void => {
|
||||
if (path.length === 0) { return }
|
||||
|
||||
const [node, nodeKey] = last(path)
|
||||
node!.set(nodeKey + key, value)
|
||||
node!.delete(nodeKey)
|
||||
}
|
||||
|
||||
const last = <T = any>(array: T[]): T => {
|
||||
return array[array.length - 1]
|
||||
}
|
103
node_modules/minisearch/src/SearchableMap/TreeIterator.ts
generated
vendored
Normal file
103
node_modules/minisearch/src/SearchableMap/TreeIterator.ts
generated
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
import type { RadixTree, Entry, LeafType } from './types'
|
||||
|
||||
/** @ignore */
|
||||
const ENTRIES = 'ENTRIES'
|
||||
|
||||
/** @ignore */
|
||||
const KEYS = 'KEYS'
|
||||
|
||||
/** @ignore */
|
||||
const VALUES = 'VALUES'
|
||||
|
||||
/** @ignore */
|
||||
const LEAF = '' as LeafType
|
||||
|
||||
interface Iterators<T> {
|
||||
ENTRIES: Entry<T>
|
||||
KEYS: string
|
||||
VALUES: T
|
||||
}
|
||||
|
||||
type Kind<T> = keyof Iterators<T>
|
||||
type Result<T, K extends keyof Iterators<T>> = Iterators<T>[K]
|
||||
|
||||
type IteratorPath<T> = {
|
||||
node: RadixTree<T>,
|
||||
keys: string[]
|
||||
}[]
|
||||
|
||||
export type IterableSet<T> = {
|
||||
_tree: RadixTree<T>,
|
||||
_prefix: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
class TreeIterator<T, K extends Kind<T>> implements Iterator<Result<T, K>> {
|
||||
set: IterableSet<T>
|
||||
_type: K
|
||||
_path: IteratorPath<T>
|
||||
|
||||
constructor (set: IterableSet<T>, type: K) {
|
||||
const node = set._tree
|
||||
const keys = Array.from(node.keys())
|
||||
this.set = set
|
||||
this._type = type
|
||||
this._path = keys.length > 0 ? [{ node, keys }] : []
|
||||
}
|
||||
|
||||
next (): IteratorResult<Result<T, K>> {
|
||||
const value = this.dive()
|
||||
this.backtrack()
|
||||
return value
|
||||
}
|
||||
|
||||
dive (): IteratorResult<Result<T, K>> {
|
||||
if (this._path.length === 0) { return { done: true, value: undefined } }
|
||||
const { node, keys } = last(this._path)!
|
||||
if (last(keys) === LEAF) { return { done: false, value: this.result() } }
|
||||
|
||||
const child = node.get(last(keys)!)!
|
||||
this._path.push({ node: child, keys: Array.from(child.keys()) })
|
||||
return this.dive()
|
||||
}
|
||||
|
||||
backtrack (): void {
|
||||
if (this._path.length === 0) { return }
|
||||
const keys = last(this._path)!.keys
|
||||
keys.pop()
|
||||
if (keys.length > 0) { return }
|
||||
this._path.pop()
|
||||
this.backtrack()
|
||||
}
|
||||
|
||||
key (): string {
|
||||
return this.set._prefix + this._path
|
||||
.map(({ keys }) => last(keys))
|
||||
.filter(key => key !== LEAF)
|
||||
.join('')
|
||||
}
|
||||
|
||||
value (): T {
|
||||
return last(this._path)!.node.get(LEAF)!
|
||||
}
|
||||
|
||||
result (): Result<T, K> {
|
||||
switch (this._type) {
|
||||
case VALUES: return this.value() as Result<T, K>
|
||||
case KEYS: return this.key() as Result<T, K>
|
||||
default: return [this.key(), this.value()] as Result<T, K>
|
||||
}
|
||||
}
|
||||
|
||||
[Symbol.iterator] () {
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
const last = <T>(array: T[]): T | undefined => {
|
||||
return array[array.length - 1]
|
||||
}
|
||||
|
||||
export { TreeIterator, ENTRIES, KEYS, VALUES, LEAF }
|
130
node_modules/minisearch/src/SearchableMap/fuzzySearch.ts
generated
vendored
Normal file
130
node_modules/minisearch/src/SearchableMap/fuzzySearch.ts
generated
vendored
Normal file
@@ -0,0 +1,130 @@
|
||||
/* eslint-disable no-labels */
|
||||
import { LEAF } from './TreeIterator'
|
||||
import type { RadixTree } from './types'
|
||||
|
||||
export type FuzzyResult<T> = [T, number]
|
||||
|
||||
export type FuzzyResults<T> = Map<string, FuzzyResult<T>>
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export const fuzzySearch = <T = any>(node: RadixTree<T>, query: string, maxDistance: number): FuzzyResults<T> => {
|
||||
const results: FuzzyResults<T> = new Map()
|
||||
if (query === undefined) return results
|
||||
|
||||
// Number of columns in the Levenshtein matrix.
|
||||
const n = query.length + 1
|
||||
|
||||
// Matching terms can never be longer than N + maxDistance.
|
||||
const m = n + maxDistance
|
||||
|
||||
// Fill first matrix row and column with numbers: 0 1 2 3 ...
|
||||
const matrix = new Uint8Array(m * n).fill(maxDistance + 1)
|
||||
for (let j = 0; j < n; ++j) matrix[j] = j
|
||||
for (let i = 1; i < m; ++i) matrix[i * n] = i
|
||||
|
||||
recurse(
|
||||
node,
|
||||
query,
|
||||
maxDistance,
|
||||
results,
|
||||
matrix,
|
||||
1,
|
||||
n,
|
||||
''
|
||||
)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// Modified version of http://stevehanov.ca/blog/?id=114
|
||||
|
||||
// This builds a Levenshtein matrix for a given query and continuously updates
|
||||
// it for nodes in the radix tree that fall within the given maximum edit
|
||||
// distance. Keeping the same matrix around is beneficial especially for larger
|
||||
// edit distances.
|
||||
//
|
||||
// k a t e <-- query
|
||||
// 0 1 2 3 4
|
||||
// c 1 1 2 3 4
|
||||
// a 2 2 1 2 3
|
||||
// t 3 3 2 1 [2] <-- edit distance
|
||||
// ^
|
||||
// ^ term in radix tree, rows are added and removed as needed
|
||||
|
||||
const recurse = <T = any>(
|
||||
node: RadixTree<T>,
|
||||
query: string,
|
||||
maxDistance: number,
|
||||
results: FuzzyResults<T>,
|
||||
matrix: Uint8Array,
|
||||
m: number,
|
||||
n: number,
|
||||
prefix: string
|
||||
): void => {
|
||||
const offset = m * n
|
||||
|
||||
key: for (const key of node.keys()) {
|
||||
if (key === LEAF) {
|
||||
// We've reached a leaf node. Check if the edit distance acceptable and
|
||||
// store the result if it is.
|
||||
const distance = matrix[offset - 1]
|
||||
if (distance <= maxDistance) {
|
||||
results.set(prefix, [node.get(key)!, distance])
|
||||
}
|
||||
} else {
|
||||
// Iterate over all characters in the key. Update the Levenshtein matrix
|
||||
// and check if the minimum distance in the last row is still within the
|
||||
// maximum edit distance. If it is, we can recurse over all child nodes.
|
||||
let i = m
|
||||
for (let pos = 0; pos < key.length; ++pos, ++i) {
|
||||
const char = key[pos]
|
||||
const thisRowOffset = n * i
|
||||
const prevRowOffset = thisRowOffset - n
|
||||
|
||||
// Set the first column based on the previous row, and initialize the
|
||||
// minimum distance in the current row.
|
||||
let minDistance = matrix[thisRowOffset]
|
||||
|
||||
const jmin = Math.max(0, i - maxDistance - 1)
|
||||
const jmax = Math.min(n - 1, i + maxDistance)
|
||||
|
||||
// Iterate over remaining columns (characters in the query).
|
||||
for (let j = jmin; j < jmax; ++j) {
|
||||
const different = char !== query[j]
|
||||
|
||||
// It might make sense to only read the matrix positions used for
|
||||
// deletion/insertion if the characters are different. But we want to
|
||||
// avoid conditional reads for performance reasons.
|
||||
const rpl = matrix[prevRowOffset + j] + +different
|
||||
const del = matrix[prevRowOffset + j + 1] + 1
|
||||
const ins = matrix[thisRowOffset + j] + 1
|
||||
|
||||
const dist = matrix[thisRowOffset + j + 1] = Math.min(rpl, del, ins)
|
||||
|
||||
if (dist < minDistance) minDistance = dist
|
||||
}
|
||||
|
||||
// Because distance will never decrease, we can stop. There will be no
|
||||
// matching child nodes.
|
||||
if (minDistance > maxDistance) {
|
||||
continue key
|
||||
}
|
||||
}
|
||||
|
||||
recurse(
|
||||
node.get(key)!,
|
||||
query,
|
||||
maxDistance,
|
||||
results,
|
||||
matrix,
|
||||
i,
|
||||
n,
|
||||
prefix + key
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default fuzzySearch
|
18
node_modules/minisearch/src/SearchableMap/types.ts
generated
vendored
Normal file
18
node_modules/minisearch/src/SearchableMap/types.ts
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
export type LeafType = '' & { readonly __tag: unique symbol }
|
||||
|
||||
export interface RadixTree<T> extends Map<string, T | RadixTree<T>> {
|
||||
// Distinguish between an empty string indicating a leaf node and a non-empty
|
||||
// string indicating a subtree. Overriding these types avoids a lot of type
|
||||
// assertions elsewhere in the code. It is not 100% foolproof because you can
|
||||
// still pass in a blank string '' disguised as `string` and potentially get a
|
||||
// leaf value.
|
||||
get(key: LeafType): T | undefined
|
||||
get(key: string): RadixTree<T> | undefined
|
||||
|
||||
set(key: LeafType, value: T): this
|
||||
set(key: string, value: RadixTree<T>): this
|
||||
}
|
||||
|
||||
export type Entry<T> = [string, T]
|
||||
|
||||
export type Path<T> = [RadixTree<T> | undefined, string][]
|
4
node_modules/minisearch/src/index.ts
generated
vendored
Normal file
4
node_modules/minisearch/src/index.ts
generated
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
import MiniSearch from './MiniSearch'
|
||||
|
||||
export * from './MiniSearch'
|
||||
export default MiniSearch
|
1
node_modules/minisearch/src/testSetup/jest.js
generated
vendored
Normal file
1
node_modules/minisearch/src/testSetup/jest.js
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/* eslint-env jest */
|
Reference in New Issue
Block a user