@vielzeug/scout 1.1.1
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/README.md +70 -0
- package/dist/_dev.cjs +2 -0
- package/dist/_dev.cjs.map +1 -0
- package/dist/_dev.d.ts +2 -0
- package/dist/_dev.d.ts.map +1 -0
- package/dist/_dev.js +9 -0
- package/dist/_dev.js.map +1 -0
- package/dist/adapters.cjs +2 -0
- package/dist/adapters.cjs.map +1 -0
- package/dist/adapters.d.ts +40 -0
- package/dist/adapters.d.ts.map +1 -0
- package/dist/adapters.js +12 -0
- package/dist/adapters.js.map +1 -0
- package/dist/devtools.cjs +2 -0
- package/dist/devtools.cjs.map +1 -0
- package/dist/devtools.d.ts +38 -0
- package/dist/devtools.d.ts.map +1 -0
- package/dist/devtools.js +21 -0
- package/dist/devtools.js.map +1 -0
- package/dist/errors.cjs +2 -0
- package/dist/errors.cjs.map +1 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +13 -0
- package/dist/errors.js.map +1 -0
- package/dist/highlight.cjs +2 -0
- package/dist/highlight.cjs.map +1 -0
- package/dist/highlight.d.ts +68 -0
- package/dist/highlight.d.ts.map +1 -0
- package/dist/highlight.js +45 -0
- package/dist/highlight.js.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/reactive.cjs +2 -0
- package/dist/reactive.cjs.map +1 -0
- package/dist/reactive.d.ts +74 -0
- package/dist/reactive.d.ts.map +1 -0
- package/dist/reactive.js +71 -0
- package/dist/reactive.js.map +1 -0
- package/dist/scout-index.cjs +2 -0
- package/dist/scout-index.cjs.map +1 -0
- package/dist/scout-index.d.ts +64 -0
- package/dist/scout-index.d.ts.map +1 -0
- package/dist/scout-index.js +162 -0
- package/dist/scout-index.js.map +1 -0
- package/dist/scout.cjs +2 -0
- package/dist/scout.cjs.map +1 -0
- package/dist/scout.iife.js +2 -0
- package/dist/scout.iife.js.map +1 -0
- package/dist/scout.js +2 -0
- package/dist/scout.js.map +1 -0
- package/dist/segment.cjs +2 -0
- package/dist/segment.cjs.map +1 -0
- package/dist/segment.d.ts +26 -0
- package/dist/segment.d.ts.map +1 -0
- package/dist/segment.js +13 -0
- package/dist/segment.js.map +1 -0
- package/dist/tokenize.cjs +2 -0
- package/dist/tokenize.cjs.map +1 -0
- package/dist/tokenize.d.ts +2 -0
- package/dist/tokenize.d.ts.map +1 -0
- package/dist/tokenize.js +12 -0
- package/dist/tokenize.js.map +1 -0
- package/dist/trigram.cjs +2 -0
- package/dist/trigram.cjs.map +1 -0
- package/dist/trigram.d.ts +2 -0
- package/dist/trigram.d.ts.map +1 -0
- package/dist/trigram.js +16 -0
- package/dist/trigram.js.map +1 -0
- package/dist/types.d.ts +130 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# @vielzeug/scout
|
|
2
|
+
|
|
3
|
+
Fast fuzzy-search. Builds a trigram inverted index at construction — O(candidates) per query instead of O(corpus × field_length).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Trigram index** — fast candidate lookup; Dice coefficient scoring
|
|
8
|
+
- **Multi-field weighted ranking** — per-field weights, custom stringifiers
|
|
9
|
+
- **Match highlighting** — character-range offsets for UI rendering
|
|
10
|
+
- **Reactive layer** — `createSearch()` wraps any index in `ripple` signals with debounce
|
|
11
|
+
- **Framework adapters** — `toSearchFn()` for sourcerer, `toFilterPredicate()` for filter pipelines
|
|
12
|
+
- **Incremental updates** — `add()`, `remove()`, `reindex()` patch the index in O(field_length)
|
|
13
|
+
- **Unsegmented-script helper** — `segmentWords()` pre-splits CJK/Thai text into words via `Intl.Segmenter`
|
|
14
|
+
- **Devtools** — `@vielzeug/scout/devtools`'s `debugSearch()` logs query/results transitions
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
pnpm add @vielzeug/scout
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { createIndex } from '@vielzeug/scout';
|
|
26
|
+
|
|
27
|
+
const index = createIndex(users, {
|
|
28
|
+
fields: [
|
|
29
|
+
{ field: 'name', weight: 2 },
|
|
30
|
+
{ field: 'email' },
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const results = index.search('alice');
|
|
35
|
+
// [{ item: { name: 'Alice', email: '...' }, score: 0.85, matches: [...] }]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Reactive search
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { createIndex, createSearch } from '@vielzeug/scout';
|
|
42
|
+
import { effect } from '@vielzeug/ripple';
|
|
43
|
+
|
|
44
|
+
const index = createIndex(users, { fields: ['name'] });
|
|
45
|
+
const search = createSearch(index, { debounce: 150 });
|
|
46
|
+
|
|
47
|
+
effect(() => {
|
|
48
|
+
console.log(search.results.value);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
search.query.value = 'alice';
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Sourcerer integration
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { createIndex, toSearchFn } from '@vielzeug/scout';
|
|
58
|
+
|
|
59
|
+
const index = createIndex(users, { fields: ['name', 'email'] });
|
|
60
|
+
const source = createLocalSource(users, { searchFn: toSearchFn(index) });
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Highlighting
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { highlight } from '@vielzeug/scout';
|
|
67
|
+
|
|
68
|
+
const parts = highlight('Hello World', [[0, 5]]);
|
|
69
|
+
// [{ text: 'Hello', highlighted: true }, { text: ' World', highlighted: false }]
|
|
70
|
+
```
|
package/dist/_dev.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_dev.cjs","names":[],"sources":["../src/_dev.ts"],"sourcesContent":["const isDev = !(globalThis as { __SCOUT_PROD__?: boolean }).__SCOUT_PROD__;\n\n/** @internal */\nexport function warn(msg: string): void {\n if (isDev) console.warn(`[@vielzeug/scout] ${msg}`);\n}\n"],"mappings":"AAAA,IAAM,EAAQ,CAAE,WAA4C,eAG5D,SAAgB,EAAK,EAAmB,CAClC,GAAO,QAAQ,KAAK,qBAAqB,GAAK,CACpD"}
|
package/dist/_dev.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_dev.d.ts","sourceRoot":"","sources":["../src/_dev.ts"],"names":[],"mappings":""}
|
package/dist/_dev.js
ADDED
package/dist/_dev.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_dev.js","names":[],"sources":["../src/_dev.ts"],"sourcesContent":["const isDev = !(globalThis as { __SCOUT_PROD__?: boolean }).__SCOUT_PROD__;\n\n/** @internal */\nexport function warn(msg: string): void {\n if (isDev) console.warn(`[@vielzeug/scout] ${msg}`);\n}\n"],"mappings":";AAAA,IAAM,IAAQ,CAAE,WAA4C;AAG5D,SAAgB,EAAK,GAAmB;CACtC,AAAI,KAAO,QAAQ,KAAK,qBAAqB,GAAK;AACpD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapters.cjs","names":[],"sources":["../src/adapters.ts"],"sourcesContent":["import type { ScoutIndex } from './scout-index';\nimport type { SearchConstraints } from './types';\n\n/**\n * Wraps a `ScoutIndex` as a `searchFn` compatible with `sourcerer`'s `LocalSourceConfig`.\n *\n * The returned function ignores the `items` argument (the index is the source of truth)\n * and delegates entirely to `index.search()`, returning plain items in score order.\n *\n * @example\n * ```ts\n * const index = createIndex(users, { fields: ['name', 'email'] });\n *\n * // Pass directly to sourcerer's searchFn option\n * const source = createLocalSource(users, { searchFn: toSearchFn(index) });\n * ```\n */\nexport function toSearchFn<T>(\n index: ScoutIndex<T>,\n options?: SearchConstraints,\n): (items: readonly T[], query: string) => readonly T[] {\n return (_items, query) => index.search(query, options).map((r) => r.item);\n}\n\n/**\n * Returns a predicate that returns `true` for items matching `query` in the given index.\n *\n * The predicate is computed once at call time — call `toFilterPredicate` again if the\n * query or corpus changes.\n *\n * Compatible with `Array.filter`, `vault`'s `query.filter()`, or any predicate pipeline.\n *\n * @example\n * ```ts\n * const index = createIndex(products, { fields: ['title', 'sku'] });\n *\n * // Array filter\n * const results = products.filter(toFilterPredicate(index, 'widget'));\n *\n * // vault query builder\n * const rows = await db.query('products')\n * .filter(toFilterPredicate(index, searchTerm))\n * .toArray();\n * ```\n */\nexport function toFilterPredicate<T>(\n index: ScoutIndex<T>,\n query: string,\n options?: SearchConstraints,\n): (item: T) => boolean {\n const matchSet = new Set(index.search(query, options).map((r) => r.item));\n\n return (item) => matchSet.has(item);\n}\n"],"mappings":"AAiBA,SAAgB,EACd,EACA,EACsD,CACtD,OAAQ,EAAQ,IAAU,EAAM,OAAO,EAAO,CAAO,CAAC,CAAC,IAAK,GAAM,EAAE,IAAI,CAC1E,CAuBA,SAAgB,EACd,EACA,EACA,EACsB,CACtB,IAAM,EAAW,IAAI,IAAI,EAAM,OAAO,EAAO,CAAO,CAAC,CAAC,IAAK,GAAM,EAAE,IAAI,CAAC,EAExE,MAAQ,IAAS,EAAS,IAAI,CAAI,CACpC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ScoutIndex } from './scout-index';
|
|
2
|
+
import type { SearchConstraints } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Wraps a `ScoutIndex` as a `searchFn` compatible with `sourcerer`'s `LocalSourceConfig`.
|
|
5
|
+
*
|
|
6
|
+
* The returned function ignores the `items` argument (the index is the source of truth)
|
|
7
|
+
* and delegates entirely to `index.search()`, returning plain items in score order.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const index = createIndex(users, { fields: ['name', 'email'] });
|
|
12
|
+
*
|
|
13
|
+
* // Pass directly to sourcerer's searchFn option
|
|
14
|
+
* const source = createLocalSource(users, { searchFn: toSearchFn(index) });
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare function toSearchFn<T>(index: ScoutIndex<T>, options?: SearchConstraints): (items: readonly T[], query: string) => readonly T[];
|
|
18
|
+
/**
|
|
19
|
+
* Returns a predicate that returns `true` for items matching `query` in the given index.
|
|
20
|
+
*
|
|
21
|
+
* The predicate is computed once at call time — call `toFilterPredicate` again if the
|
|
22
|
+
* query or corpus changes.
|
|
23
|
+
*
|
|
24
|
+
* Compatible with `Array.filter`, `vault`'s `query.filter()`, or any predicate pipeline.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* const index = createIndex(products, { fields: ['title', 'sku'] });
|
|
29
|
+
*
|
|
30
|
+
* // Array filter
|
|
31
|
+
* const results = products.filter(toFilterPredicate(index, 'widget'));
|
|
32
|
+
*
|
|
33
|
+
* // vault query builder
|
|
34
|
+
* const rows = await db.query('products')
|
|
35
|
+
* .filter(toFilterPredicate(index, searchTerm))
|
|
36
|
+
* .toArray();
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare function toFilterPredicate<T>(index: ScoutIndex<T>, query: string, options?: SearchConstraints): (item: T) => boolean;
|
|
40
|
+
//# sourceMappingURL=adapters.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapters.d.ts","sourceRoot":"","sources":["../src/adapters.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEjD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAC1B,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,EACpB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,EAAE,KAAK,EAAE,MAAM,KAAK,SAAS,CAAC,EAAE,CAEtD;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,EACpB,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,iBAAiB,GAC1B,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,CAItB"}
|
package/dist/adapters.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/adapters.ts
|
|
2
|
+
function e(e, t) {
|
|
3
|
+
return (n, r) => e.search(r, t).map((e) => e.item);
|
|
4
|
+
}
|
|
5
|
+
function t(e, t, n) {
|
|
6
|
+
let r = new Set(e.search(t, n).map((e) => e.item));
|
|
7
|
+
return (e) => r.has(e);
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { t as toFilterPredicate, e as toSearchFn };
|
|
11
|
+
|
|
12
|
+
//# sourceMappingURL=adapters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapters.js","names":[],"sources":["../src/adapters.ts"],"sourcesContent":["import type { ScoutIndex } from './scout-index';\nimport type { SearchConstraints } from './types';\n\n/**\n * Wraps a `ScoutIndex` as a `searchFn` compatible with `sourcerer`'s `LocalSourceConfig`.\n *\n * The returned function ignores the `items` argument (the index is the source of truth)\n * and delegates entirely to `index.search()`, returning plain items in score order.\n *\n * @example\n * ```ts\n * const index = createIndex(users, { fields: ['name', 'email'] });\n *\n * // Pass directly to sourcerer's searchFn option\n * const source = createLocalSource(users, { searchFn: toSearchFn(index) });\n * ```\n */\nexport function toSearchFn<T>(\n index: ScoutIndex<T>,\n options?: SearchConstraints,\n): (items: readonly T[], query: string) => readonly T[] {\n return (_items, query) => index.search(query, options).map((r) => r.item);\n}\n\n/**\n * Returns a predicate that returns `true` for items matching `query` in the given index.\n *\n * The predicate is computed once at call time — call `toFilterPredicate` again if the\n * query or corpus changes.\n *\n * Compatible with `Array.filter`, `vault`'s `query.filter()`, or any predicate pipeline.\n *\n * @example\n * ```ts\n * const index = createIndex(products, { fields: ['title', 'sku'] });\n *\n * // Array filter\n * const results = products.filter(toFilterPredicate(index, 'widget'));\n *\n * // vault query builder\n * const rows = await db.query('products')\n * .filter(toFilterPredicate(index, searchTerm))\n * .toArray();\n * ```\n */\nexport function toFilterPredicate<T>(\n index: ScoutIndex<T>,\n query: string,\n options?: SearchConstraints,\n): (item: T) => boolean {\n const matchSet = new Set(index.search(query, options).map((r) => r.item));\n\n return (item) => matchSet.has(item);\n}\n"],"mappings":";AAiBA,SAAgB,EACd,GACA,GACsD;CACtD,QAAQ,GAAQ,MAAU,EAAM,OAAO,GAAO,CAAO,CAAC,CAAC,KAAK,MAAM,EAAE,IAAI;AAC1E;AAuBA,SAAgB,EACd,GACA,GACA,GACsB;CACtB,IAAM,IAAW,IAAI,IAAI,EAAM,OAAO,GAAO,CAAO,CAAC,CAAC,KAAK,MAAM,EAAE,IAAI,CAAC;CAExE,QAAQ,MAAS,EAAS,IAAI,CAAI;AACpC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});function e(e){let t=`scout:search`,n=[e.query.subscribe(()=>{console.debug(`[${t}] query -> ${JSON.stringify(e.query.peek())}`)}),e.isSearching.subscribe(()=>{console.debug(`[${t}] isSearching -> ${e.isSearching.peek()}`)}),e.results.subscribe(()=>{console.debug(`[${t}] results -> ${e.results.peek().length} item(s)`)})];return()=>{for(let e of n)e.dispose()}}exports.debugSearch=e;
|
|
2
|
+
//# sourceMappingURL=devtools.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devtools.cjs","names":[],"sources":["../src/devtools.ts"],"sourcesContent":["/**\n * @vielzeug/scout — debug utilities for the reactive search layer.\n *\n * Import from the dedicated sub-path so it is tree-shaken from production bundles:\n * ```ts\n * import { debugSearch } from '@vielzeug/scout/devtools';\n * ```\n */\n\nimport type { SearchState } from './types';\n\n/**\n * Logs `query` → `isSearching` → `results` transitions of a `SearchState` to\n * `console.debug`. Development only — helps visualize debounce timing and result\n * churn without stepping through `createSearch()` internals.\n *\n * **Logs the full, literal search query string.** If your queries may carry PII (names,\n * emails, medical/financial terms typed by end users), don't enable this in production —\n * it's opt-in dev tooling imported from a dedicated sub-path for exactly that reason.\n *\n * Returns a function that unsubscribes all listeners installed by this call.\n *\n * @example\n * ```ts\n * import { debugSearch } from '@vielzeug/scout/devtools';\n *\n * const search = createSearch(index);\n * const stopDebugging = debugSearch(search);\n *\n * search.query.value = 'alice';\n * // [scout:search] query -> \"alice\"\n * // [scout:search] isSearching -> true\n * // [scout:search] isSearching -> false\n * // [scout:search] results -> 1 item(s)\n *\n * stopDebugging();\n * ```\n */\nexport function debugSearch<T>(search: SearchState<T>): () => void {\n const label = 'scout:search';\n\n const subscriptions = [\n search.query.subscribe(() => {\n console.debug(`[${label}] query -> ${JSON.stringify(search.query.peek())}`);\n }),\n search.isSearching.subscribe(() => {\n console.debug(`[${label}] isSearching -> ${search.isSearching.peek()}`);\n }),\n search.results.subscribe(() => {\n console.debug(`[${label}] results -> ${search.results.peek().length} item(s)`);\n }),\n ];\n\n return () => {\n for (const subscription of subscriptions) subscription.dispose();\n };\n}\n"],"mappings":"mEAsCA,SAAgB,EAAe,EAAoC,CACjE,IAAM,EAAQ,eAER,EAAgB,CACpB,EAAO,MAAM,cAAgB,CAC3B,QAAQ,MAAM,IAAI,EAAM,aAAa,KAAK,UAAU,EAAO,MAAM,KAAK,CAAC,GAAG,CAC5E,CAAC,EACD,EAAO,YAAY,cAAgB,CACjC,QAAQ,MAAM,IAAI,EAAM,mBAAmB,EAAO,YAAY,KAAK,GAAG,CACxE,CAAC,EACD,EAAO,QAAQ,cAAgB,CAC7B,QAAQ,MAAM,IAAI,EAAM,eAAe,EAAO,QAAQ,KAAK,CAAC,CAAC,OAAO,SAAS,CAC/E,CAAC,CACH,EAEA,UAAa,CACX,IAAK,IAAM,KAAgB,EAAe,EAAa,QAAQ,CACjE,CACF"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vielzeug/scout — debug utilities for the reactive search layer.
|
|
3
|
+
*
|
|
4
|
+
* Import from the dedicated sub-path so it is tree-shaken from production bundles:
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { debugSearch } from '@vielzeug/scout/devtools';
|
|
7
|
+
* ```
|
|
8
|
+
*/
|
|
9
|
+
import type { SearchState } from './types';
|
|
10
|
+
/**
|
|
11
|
+
* Logs `query` → `isSearching` → `results` transitions of a `SearchState` to
|
|
12
|
+
* `console.debug`. Development only — helps visualize debounce timing and result
|
|
13
|
+
* churn without stepping through `createSearch()` internals.
|
|
14
|
+
*
|
|
15
|
+
* **Logs the full, literal search query string.** If your queries may carry PII (names,
|
|
16
|
+
* emails, medical/financial terms typed by end users), don't enable this in production —
|
|
17
|
+
* it's opt-in dev tooling imported from a dedicated sub-path for exactly that reason.
|
|
18
|
+
*
|
|
19
|
+
* Returns a function that unsubscribes all listeners installed by this call.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { debugSearch } from '@vielzeug/scout/devtools';
|
|
24
|
+
*
|
|
25
|
+
* const search = createSearch(index);
|
|
26
|
+
* const stopDebugging = debugSearch(search);
|
|
27
|
+
*
|
|
28
|
+
* search.query.value = 'alice';
|
|
29
|
+
* // [scout:search] query -> "alice"
|
|
30
|
+
* // [scout:search] isSearching -> true
|
|
31
|
+
* // [scout:search] isSearching -> false
|
|
32
|
+
* // [scout:search] results -> 1 item(s)
|
|
33
|
+
*
|
|
34
|
+
* stopDebugging();
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function debugSearch<T>(search: SearchState<T>): () => void;
|
|
38
|
+
//# sourceMappingURL=devtools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devtools.d.ts","sourceRoot":"","sources":["../src/devtools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,CAkBjE"}
|
package/dist/devtools.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/devtools.ts
|
|
2
|
+
function e(e) {
|
|
3
|
+
let t = "scout:search", n = [
|
|
4
|
+
e.query.subscribe(() => {
|
|
5
|
+
console.debug(`[${t}] query -> ${JSON.stringify(e.query.peek())}`);
|
|
6
|
+
}),
|
|
7
|
+
e.isSearching.subscribe(() => {
|
|
8
|
+
console.debug(`[${t}] isSearching -> ${e.isSearching.peek()}`);
|
|
9
|
+
}),
|
|
10
|
+
e.results.subscribe(() => {
|
|
11
|
+
console.debug(`[${t}] results -> ${e.results.peek().length} item(s)`);
|
|
12
|
+
})
|
|
13
|
+
];
|
|
14
|
+
return () => {
|
|
15
|
+
for (let e of n) e.dispose();
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
export { e as debugSearch };
|
|
20
|
+
|
|
21
|
+
//# sourceMappingURL=devtools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devtools.js","names":[],"sources":["../src/devtools.ts"],"sourcesContent":["/**\n * @vielzeug/scout — debug utilities for the reactive search layer.\n *\n * Import from the dedicated sub-path so it is tree-shaken from production bundles:\n * ```ts\n * import { debugSearch } from '@vielzeug/scout/devtools';\n * ```\n */\n\nimport type { SearchState } from './types';\n\n/**\n * Logs `query` → `isSearching` → `results` transitions of a `SearchState` to\n * `console.debug`. Development only — helps visualize debounce timing and result\n * churn without stepping through `createSearch()` internals.\n *\n * **Logs the full, literal search query string.** If your queries may carry PII (names,\n * emails, medical/financial terms typed by end users), don't enable this in production —\n * it's opt-in dev tooling imported from a dedicated sub-path for exactly that reason.\n *\n * Returns a function that unsubscribes all listeners installed by this call.\n *\n * @example\n * ```ts\n * import { debugSearch } from '@vielzeug/scout/devtools';\n *\n * const search = createSearch(index);\n * const stopDebugging = debugSearch(search);\n *\n * search.query.value = 'alice';\n * // [scout:search] query -> \"alice\"\n * // [scout:search] isSearching -> true\n * // [scout:search] isSearching -> false\n * // [scout:search] results -> 1 item(s)\n *\n * stopDebugging();\n * ```\n */\nexport function debugSearch<T>(search: SearchState<T>): () => void {\n const label = 'scout:search';\n\n const subscriptions = [\n search.query.subscribe(() => {\n console.debug(`[${label}] query -> ${JSON.stringify(search.query.peek())}`);\n }),\n search.isSearching.subscribe(() => {\n console.debug(`[${label}] isSearching -> ${search.isSearching.peek()}`);\n }),\n search.results.subscribe(() => {\n console.debug(`[${label}] results -> ${search.results.peek().length} item(s)`);\n }),\n ];\n\n return () => {\n for (const subscription of subscriptions) subscription.dispose();\n };\n}\n"],"mappings":";AAsCA,SAAgB,EAAe,GAAoC;CACjE,IAAM,IAAQ,gBAER,IAAgB;EACpB,EAAO,MAAM,gBAAgB;GAC3B,QAAQ,MAAM,IAAI,EAAM,aAAa,KAAK,UAAU,EAAO,MAAM,KAAK,CAAC,GAAG;EAC5E,CAAC;EACD,EAAO,YAAY,gBAAgB;GACjC,QAAQ,MAAM,IAAI,EAAM,mBAAmB,EAAO,YAAY,KAAK,GAAG;EACxE,CAAC;EACD,EAAO,QAAQ,gBAAgB;GAC7B,QAAQ,MAAM,IAAI,EAAM,eAAe,EAAO,QAAQ,KAAK,CAAC,CAAC,OAAO,SAAS;EAC/E,CAAC;CACH;CAEA,aAAa;EACX,KAAK,IAAM,KAAgB,GAAe,EAAa,QAAQ;CACjE;AACF"}
|
package/dist/errors.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var e=class e extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}static is(t){return t instanceof e}},t=class extends e{},n=class extends e{};exports.ScoutDisposedError=t,exports.ScoutError=e,exports.ScoutIndexError=n;
|
|
2
|
+
//# sourceMappingURL=errors.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.cjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/** Base class for all scout errors. Use `instanceof ScoutError` to catch any scout-originated error. */\nexport class ScoutError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n static is(err: unknown): err is ScoutError {\n return err instanceof ScoutError;\n }\n}\n\n/** Thrown when a method is called on a disposed search state instance. */\nexport class ScoutDisposedError extends ScoutError {}\n\n/** Thrown when an index is built or queried with an invalid configuration (e.g. zero fields defined). */\nexport class ScoutIndexError extends ScoutError {}\n"],"mappings":"AACA,IAAa,EAAb,MAAa,UAAmB,KAAM,CACpC,YAAY,EAAiB,EAAqB,CAChD,MAAM,EAAS,CAAI,EACnB,KAAK,KAAO,IAAI,OAAO,KACvB,OAAO,eAAe,KAAM,IAAI,OAAO,SAAS,CAClD,CAEA,OAAO,GAAG,EAAiC,CACzC,OAAO,aAAe,CACxB,CACF,EAGa,EAAb,cAAwC,CAAW,CAAC,EAGvC,EAAb,cAAqC,CAAW,CAAC"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Base class for all scout errors. Use `instanceof ScoutError` to catch any scout-originated error. */
|
|
2
|
+
export declare class ScoutError extends Error {
|
|
3
|
+
constructor(message: string, opts?: ErrorOptions);
|
|
4
|
+
static is(err: unknown): err is ScoutError;
|
|
5
|
+
}
|
|
6
|
+
/** Thrown when a method is called on a disposed search state instance. */
|
|
7
|
+
export declare class ScoutDisposedError extends ScoutError {
|
|
8
|
+
}
|
|
9
|
+
/** Thrown when an index is built or queried with an invalid configuration (e.g. zero fields defined). */
|
|
10
|
+
export declare class ScoutIndexError extends ScoutError {
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,wGAAwG;AACxG,qBAAa,UAAW,SAAQ,KAAK;gBACvB,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,YAAY;IAMhD,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,UAAU;CAG3C;AAED,0EAA0E;AAC1E,qBAAa,kBAAmB,SAAQ,UAAU;CAAG;AAErD,yGAAyG;AACzG,qBAAa,eAAgB,SAAQ,UAAU;CAAG"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/errors.ts
|
|
2
|
+
var e = class e extends Error {
|
|
3
|
+
constructor(e, t) {
|
|
4
|
+
super(e, t), this.name = new.target.name, Object.setPrototypeOf(this, new.target.prototype);
|
|
5
|
+
}
|
|
6
|
+
static is(t) {
|
|
7
|
+
return t instanceof e;
|
|
8
|
+
}
|
|
9
|
+
}, t = class extends e {}, n = class extends e {};
|
|
10
|
+
//#endregion
|
|
11
|
+
export { t as ScoutDisposedError, e as ScoutError, n as ScoutIndexError };
|
|
12
|
+
|
|
13
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/** Base class for all scout errors. Use `instanceof ScoutError` to catch any scout-originated error. */\nexport class ScoutError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n static is(err: unknown): err is ScoutError {\n return err instanceof ScoutError;\n }\n}\n\n/** Thrown when a method is called on a disposed search state instance. */\nexport class ScoutDisposedError extends ScoutError {}\n\n/** Thrown when an index is built or queried with an invalid configuration (e.g. zero fields defined). */\nexport class ScoutIndexError extends ScoutError {}\n"],"mappings":";AACA,IAAa,IAAb,MAAa,UAAmB,MAAM;CACpC,YAAY,GAAiB,GAAqB;EAGhD,AAFA,MAAM,GAAS,CAAI,GACnB,KAAK,OAAO,IAAI,OAAO,MACvB,OAAO,eAAe,MAAM,IAAI,OAAO,SAAS;CAClD;CAEA,OAAO,GAAG,GAAiC;EACzC,OAAO,aAAe;CACxB;AACF,GAGa,IAAb,cAAwC,EAAW,CAAC,GAGvC,IAAb,cAAqC,EAAW,CAAC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function e(e,t){let n=e.toLowerCase(),r=t.split(` `).filter(Boolean),i=[];for(let e of r){let t=0;for(;t<n.length;){let r=n.indexOf(e,t);if(r===-1)break;i.push([r,r+e.length]),t=r+1}}i.sort((e,t)=>e[0]-t[0]);let a=[];for(let e of i){let t=a[a.length-1];t&&e[0]<=t[1]?t[1]=Math.max(t[1],e[1]):a.push([e[0],e[1]])}return a}function t(e,t){if(!e)return[];if(!t.length)return[{highlighted:!1,text:e}];let n=[],r=0;for(let[i,a]of t)i>r&&n.push({highlighted:!1,text:e.slice(r,i)}),a>i&&n.push({highlighted:!0,text:e.slice(i,a)}),r=a;return r<e.length&&n.push({highlighted:!1,text:e.slice(r)}),n}function n(e,n,r){return t(r,e.matches.find(e=>e.field===n)?.ranges??[])}exports.findMatchRanges=e,exports.highlight=t,exports.highlightField=n;
|
|
2
|
+
//# sourceMappingURL=highlight.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"highlight.cjs","names":[],"sources":["../src/highlight.ts"],"sourcesContent":["import type { HighlightPart, SearchResult } from './types';\n\n/**\n * Finds character ranges within `text` where `query` words appear.\n * Ranges are sorted and overlapping ranges are merged.\n *\n * Useful when you need to apply match ranges to a different string than the\n * indexed field value (e.g. a truncated preview or a differently formatted display string).\n *\n * @param text - The string to search within.\n * @param query - The normalized (tokenized) query string. Words are split on spaces.\n * @returns Sorted, non-overlapping `[start, end]` character ranges.\n */\nexport function findMatchRanges(text: string, query: string): [number, number][] {\n const lower = text.toLowerCase();\n const words = query.split(' ').filter(Boolean);\n const ranges: [number, number][] = [];\n\n for (const word of words) {\n let pos = 0;\n\n while (pos < lower.length) {\n const idx = lower.indexOf(word, pos);\n\n if (idx === -1) break;\n\n ranges.push([idx, idx + word.length]);\n pos = idx + 1;\n }\n }\n\n ranges.sort((a, b) => a[0] - b[0]);\n\n const merged: [number, number][] = [];\n\n for (const range of ranges) {\n const last = merged[merged.length - 1];\n\n if (last && range[0] <= last[1]) {\n last[1] = Math.max(last[1], range[1]);\n } else {\n merged.push([range[0], range[1]]);\n }\n }\n\n return merged;\n}\n\n/**\n * Splits `text` into highlighted and unhighlighted fragments using match `ranges`.\n *\n * Ranges must be sorted and non-overlapping (as produced by `SearchResult.matches[n].ranges`).\n * Use the returned parts to render highlighted text in a UI component.\n *\n * **`part.text` is the original, unescaped field value** (e.g. a user's name, bio, or\n * product title) — this function does no HTML escaping. Render each part via safe DOM APIs\n * (`textContent`, a framework's text binding) and wrap `highlighted` parts in your own\n * element (e.g. `<mark>`); never concatenate `part.text` into an HTML string for\n * `innerHTML` — that reintroduces the XSS risk this structured return shape avoids.\n *\n * @example\n * ```ts\n * highlight('Hello World', [[0, 5]]);\n * // [{ text: 'Hello', highlighted: true }, { text: ' World', highlighted: false }]\n *\n * highlight('Hello World', [[0, 5], [6, 11]]);\n * // [\n * // { text: 'Hello', highlighted: true },\n * // { text: ' ', highlighted: false },\n * // { text: 'World', highlighted: true },\n * // ]\n * ```\n *\n * @param text - The original field value to split.\n * @param ranges - Sorted, non-overlapping `[start, end]` ranges from `FieldMatch.ranges`.\n * @returns An array of `HighlightPart` objects. Returns an empty array for an empty `text`.\n */\nexport function highlight(text: string, ranges: [number, number][]): HighlightPart[] {\n if (!text) return [];\n\n if (!ranges.length) return [{ highlighted: false, text }];\n\n const parts: HighlightPart[] = [];\n let cursor = 0;\n\n for (const [start, end] of ranges) {\n if (start > cursor) {\n parts.push({ highlighted: false, text: text.slice(cursor, start) });\n }\n\n if (end > start) {\n parts.push({ highlighted: true, text: text.slice(start, end) });\n }\n\n cursor = end;\n }\n\n if (cursor < text.length) {\n parts.push({ highlighted: false, text: text.slice(cursor) });\n }\n\n return parts;\n}\n\n/**\n * Finds the match ranges for `field` in `result` and splits `text` into\n * highlighted and unhighlighted fragments in one step.\n *\n * This is the ergonomic shorthand for the common pattern:\n * ```ts\n * const match = result.matches.find(m => m.field === 'name');\n * const parts = highlight(item.name, match?.ranges ?? []);\n * ```\n *\n * @example\n * ```ts\n * for (const result of index.search('alice')) {\n * const parts = highlightField(result, 'name', result.item.name);\n * console.log(parts.map(p => p.highlighted ? `[${p.text}]` : p.text).join(''));\n * }\n * ```\n *\n * @param result - A `SearchResult` from `ScoutIndex.search()`.\n * @param field - The field name to look up in `result.matches`.\n * @param text - The original field value string to split.\n * @returns An array of `HighlightPart` objects.\n */\nexport function highlightField<T>(result: SearchResult<T>, field: keyof T & string, text: string): HighlightPart[] {\n const match = result.matches.find((m) => m.field === field);\n\n return highlight(text, match?.ranges ?? []);\n}\n"],"mappings":"AAaA,SAAgB,EAAgB,EAAc,EAAmC,CAC/E,IAAM,EAAQ,EAAK,YAAY,EACzB,EAAQ,EAAM,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO,EACvC,EAA6B,CAAC,EAEpC,IAAK,IAAM,KAAQ,EAAO,CACxB,IAAI,EAAM,EAEV,KAAO,EAAM,EAAM,QAAQ,CACzB,IAAM,EAAM,EAAM,QAAQ,EAAM,CAAG,EAEnC,GAAI,IAAQ,GAAI,MAEhB,EAAO,KAAK,CAAC,EAAK,EAAM,EAAK,MAAM,CAAC,EACpC,EAAM,EAAM,CACd,CACF,CAEA,EAAO,MAAM,EAAG,IAAM,EAAE,GAAK,EAAE,EAAE,EAEjC,IAAM,EAA6B,CAAC,EAEpC,IAAK,IAAM,KAAS,EAAQ,CAC1B,IAAM,EAAO,EAAO,EAAO,OAAS,GAEhC,GAAQ,EAAM,IAAM,EAAK,GAC3B,EAAK,GAAK,KAAK,IAAI,EAAK,GAAI,EAAM,EAAE,EAEpC,EAAO,KAAK,CAAC,EAAM,GAAI,EAAM,EAAE,CAAC,CAEpC,CAEA,OAAO,CACT,CA+BA,SAAgB,EAAU,EAAc,EAA6C,CACnF,GAAI,CAAC,EAAM,MAAO,CAAC,EAEnB,GAAI,CAAC,EAAO,OAAQ,MAAO,CAAC,CAAE,YAAa,GAAO,MAAK,CAAC,EAExD,IAAM,EAAyB,CAAC,EAC5B,EAAS,EAEb,IAAK,GAAM,CAAC,EAAO,KAAQ,EACrB,EAAQ,GACV,EAAM,KAAK,CAAE,YAAa,GAAO,KAAM,EAAK,MAAM,EAAQ,CAAK,CAAE,CAAC,EAGhE,EAAM,GACR,EAAM,KAAK,CAAE,YAAa,GAAM,KAAM,EAAK,MAAM,EAAO,CAAG,CAAE,CAAC,EAGhE,EAAS,EAOX,OAJI,EAAS,EAAK,QAChB,EAAM,KAAK,CAAE,YAAa,GAAO,KAAM,EAAK,MAAM,CAAM,CAAE,CAAC,EAGtD,CACT,CAyBA,SAAgB,EAAkB,EAAyB,EAAyB,EAA+B,CAGjH,OAAO,EAAU,EAFH,EAAO,QAAQ,KAAM,GAAM,EAAE,QAAU,CAE9B,CAAA,EAAO,QAAU,CAAC,CAAC,CAC5C"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { HighlightPart, SearchResult } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Finds character ranges within `text` where `query` words appear.
|
|
4
|
+
* Ranges are sorted and overlapping ranges are merged.
|
|
5
|
+
*
|
|
6
|
+
* Useful when you need to apply match ranges to a different string than the
|
|
7
|
+
* indexed field value (e.g. a truncated preview or a differently formatted display string).
|
|
8
|
+
*
|
|
9
|
+
* @param text - The string to search within.
|
|
10
|
+
* @param query - The normalized (tokenized) query string. Words are split on spaces.
|
|
11
|
+
* @returns Sorted, non-overlapping `[start, end]` character ranges.
|
|
12
|
+
*/
|
|
13
|
+
export declare function findMatchRanges(text: string, query: string): [number, number][];
|
|
14
|
+
/**
|
|
15
|
+
* Splits `text` into highlighted and unhighlighted fragments using match `ranges`.
|
|
16
|
+
*
|
|
17
|
+
* Ranges must be sorted and non-overlapping (as produced by `SearchResult.matches[n].ranges`).
|
|
18
|
+
* Use the returned parts to render highlighted text in a UI component.
|
|
19
|
+
*
|
|
20
|
+
* **`part.text` is the original, unescaped field value** (e.g. a user's name, bio, or
|
|
21
|
+
* product title) — this function does no HTML escaping. Render each part via safe DOM APIs
|
|
22
|
+
* (`textContent`, a framework's text binding) and wrap `highlighted` parts in your own
|
|
23
|
+
* element (e.g. `<mark>`); never concatenate `part.text` into an HTML string for
|
|
24
|
+
* `innerHTML` — that reintroduces the XSS risk this structured return shape avoids.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* highlight('Hello World', [[0, 5]]);
|
|
29
|
+
* // [{ text: 'Hello', highlighted: true }, { text: ' World', highlighted: false }]
|
|
30
|
+
*
|
|
31
|
+
* highlight('Hello World', [[0, 5], [6, 11]]);
|
|
32
|
+
* // [
|
|
33
|
+
* // { text: 'Hello', highlighted: true },
|
|
34
|
+
* // { text: ' ', highlighted: false },
|
|
35
|
+
* // { text: 'World', highlighted: true },
|
|
36
|
+
* // ]
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @param text - The original field value to split.
|
|
40
|
+
* @param ranges - Sorted, non-overlapping `[start, end]` ranges from `FieldMatch.ranges`.
|
|
41
|
+
* @returns An array of `HighlightPart` objects. Returns an empty array for an empty `text`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function highlight(text: string, ranges: [number, number][]): HighlightPart[];
|
|
44
|
+
/**
|
|
45
|
+
* Finds the match ranges for `field` in `result` and splits `text` into
|
|
46
|
+
* highlighted and unhighlighted fragments in one step.
|
|
47
|
+
*
|
|
48
|
+
* This is the ergonomic shorthand for the common pattern:
|
|
49
|
+
* ```ts
|
|
50
|
+
* const match = result.matches.find(m => m.field === 'name');
|
|
51
|
+
* const parts = highlight(item.name, match?.ranges ?? []);
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* for (const result of index.search('alice')) {
|
|
57
|
+
* const parts = highlightField(result, 'name', result.item.name);
|
|
58
|
+
* console.log(parts.map(p => p.highlighted ? `[${p.text}]` : p.text).join(''));
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* @param result - A `SearchResult` from `ScoutIndex.search()`.
|
|
63
|
+
* @param field - The field name to look up in `result.matches`.
|
|
64
|
+
* @param text - The original field value string to split.
|
|
65
|
+
* @returns An array of `HighlightPart` objects.
|
|
66
|
+
*/
|
|
67
|
+
export declare function highlightField<T>(result: SearchResult<T>, field: keyof T & string, text: string): HighlightPart[];
|
|
68
|
+
//# sourceMappingURL=highlight.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"highlight.d.ts","sourceRoot":"","sources":["../src/highlight.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE3D;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAiC/E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,aAAa,EAAE,CAyBnF;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa,EAAE,CAIjH"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/highlight.ts
|
|
2
|
+
function e(e, t) {
|
|
3
|
+
let n = e.toLowerCase(), r = t.split(" ").filter(Boolean), i = [];
|
|
4
|
+
for (let e of r) {
|
|
5
|
+
let t = 0;
|
|
6
|
+
for (; t < n.length;) {
|
|
7
|
+
let r = n.indexOf(e, t);
|
|
8
|
+
if (r === -1) break;
|
|
9
|
+
i.push([r, r + e.length]), t = r + 1;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
i.sort((e, t) => e[0] - t[0]);
|
|
13
|
+
let a = [];
|
|
14
|
+
for (let e of i) {
|
|
15
|
+
let t = a[a.length - 1];
|
|
16
|
+
t && e[0] <= t[1] ? t[1] = Math.max(t[1], e[1]) : a.push([e[0], e[1]]);
|
|
17
|
+
}
|
|
18
|
+
return a;
|
|
19
|
+
}
|
|
20
|
+
function t(e, t) {
|
|
21
|
+
if (!e) return [];
|
|
22
|
+
if (!t.length) return [{
|
|
23
|
+
highlighted: !1,
|
|
24
|
+
text: e
|
|
25
|
+
}];
|
|
26
|
+
let n = [], r = 0;
|
|
27
|
+
for (let [i, a] of t) i > r && n.push({
|
|
28
|
+
highlighted: !1,
|
|
29
|
+
text: e.slice(r, i)
|
|
30
|
+
}), a > i && n.push({
|
|
31
|
+
highlighted: !0,
|
|
32
|
+
text: e.slice(i, a)
|
|
33
|
+
}), r = a;
|
|
34
|
+
return r < e.length && n.push({
|
|
35
|
+
highlighted: !1,
|
|
36
|
+
text: e.slice(r)
|
|
37
|
+
}), n;
|
|
38
|
+
}
|
|
39
|
+
function n(e, n, r) {
|
|
40
|
+
return t(r, e.matches.find((e) => e.field === n)?.ranges ?? []);
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { e as findMatchRanges, t as highlight, n as highlightField };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=highlight.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"highlight.js","names":[],"sources":["../src/highlight.ts"],"sourcesContent":["import type { HighlightPart, SearchResult } from './types';\n\n/**\n * Finds character ranges within `text` where `query` words appear.\n * Ranges are sorted and overlapping ranges are merged.\n *\n * Useful when you need to apply match ranges to a different string than the\n * indexed field value (e.g. a truncated preview or a differently formatted display string).\n *\n * @param text - The string to search within.\n * @param query - The normalized (tokenized) query string. Words are split on spaces.\n * @returns Sorted, non-overlapping `[start, end]` character ranges.\n */\nexport function findMatchRanges(text: string, query: string): [number, number][] {\n const lower = text.toLowerCase();\n const words = query.split(' ').filter(Boolean);\n const ranges: [number, number][] = [];\n\n for (const word of words) {\n let pos = 0;\n\n while (pos < lower.length) {\n const idx = lower.indexOf(word, pos);\n\n if (idx === -1) break;\n\n ranges.push([idx, idx + word.length]);\n pos = idx + 1;\n }\n }\n\n ranges.sort((a, b) => a[0] - b[0]);\n\n const merged: [number, number][] = [];\n\n for (const range of ranges) {\n const last = merged[merged.length - 1];\n\n if (last && range[0] <= last[1]) {\n last[1] = Math.max(last[1], range[1]);\n } else {\n merged.push([range[0], range[1]]);\n }\n }\n\n return merged;\n}\n\n/**\n * Splits `text` into highlighted and unhighlighted fragments using match `ranges`.\n *\n * Ranges must be sorted and non-overlapping (as produced by `SearchResult.matches[n].ranges`).\n * Use the returned parts to render highlighted text in a UI component.\n *\n * **`part.text` is the original, unescaped field value** (e.g. a user's name, bio, or\n * product title) — this function does no HTML escaping. Render each part via safe DOM APIs\n * (`textContent`, a framework's text binding) and wrap `highlighted` parts in your own\n * element (e.g. `<mark>`); never concatenate `part.text` into an HTML string for\n * `innerHTML` — that reintroduces the XSS risk this structured return shape avoids.\n *\n * @example\n * ```ts\n * highlight('Hello World', [[0, 5]]);\n * // [{ text: 'Hello', highlighted: true }, { text: ' World', highlighted: false }]\n *\n * highlight('Hello World', [[0, 5], [6, 11]]);\n * // [\n * // { text: 'Hello', highlighted: true },\n * // { text: ' ', highlighted: false },\n * // { text: 'World', highlighted: true },\n * // ]\n * ```\n *\n * @param text - The original field value to split.\n * @param ranges - Sorted, non-overlapping `[start, end]` ranges from `FieldMatch.ranges`.\n * @returns An array of `HighlightPart` objects. Returns an empty array for an empty `text`.\n */\nexport function highlight(text: string, ranges: [number, number][]): HighlightPart[] {\n if (!text) return [];\n\n if (!ranges.length) return [{ highlighted: false, text }];\n\n const parts: HighlightPart[] = [];\n let cursor = 0;\n\n for (const [start, end] of ranges) {\n if (start > cursor) {\n parts.push({ highlighted: false, text: text.slice(cursor, start) });\n }\n\n if (end > start) {\n parts.push({ highlighted: true, text: text.slice(start, end) });\n }\n\n cursor = end;\n }\n\n if (cursor < text.length) {\n parts.push({ highlighted: false, text: text.slice(cursor) });\n }\n\n return parts;\n}\n\n/**\n * Finds the match ranges for `field` in `result` and splits `text` into\n * highlighted and unhighlighted fragments in one step.\n *\n * This is the ergonomic shorthand for the common pattern:\n * ```ts\n * const match = result.matches.find(m => m.field === 'name');\n * const parts = highlight(item.name, match?.ranges ?? []);\n * ```\n *\n * @example\n * ```ts\n * for (const result of index.search('alice')) {\n * const parts = highlightField(result, 'name', result.item.name);\n * console.log(parts.map(p => p.highlighted ? `[${p.text}]` : p.text).join(''));\n * }\n * ```\n *\n * @param result - A `SearchResult` from `ScoutIndex.search()`.\n * @param field - The field name to look up in `result.matches`.\n * @param text - The original field value string to split.\n * @returns An array of `HighlightPart` objects.\n */\nexport function highlightField<T>(result: SearchResult<T>, field: keyof T & string, text: string): HighlightPart[] {\n const match = result.matches.find((m) => m.field === field);\n\n return highlight(text, match?.ranges ?? []);\n}\n"],"mappings":";AAaA,SAAgB,EAAgB,GAAc,GAAmC;CAC/E,IAAM,IAAQ,EAAK,YAAY,GACzB,IAAQ,EAAM,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO,GACvC,IAA6B,CAAC;CAEpC,KAAK,IAAM,KAAQ,GAAO;EACxB,IAAI,IAAM;EAEV,OAAO,IAAM,EAAM,SAAQ;GACzB,IAAM,IAAM,EAAM,QAAQ,GAAM,CAAG;GAEnC,IAAI,MAAQ,IAAI;GAGhB,AADA,EAAO,KAAK,CAAC,GAAK,IAAM,EAAK,MAAM,CAAC,GACpC,IAAM,IAAM;EACd;CACF;CAEA,EAAO,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,EAAE;CAEjC,IAAM,IAA6B,CAAC;CAEpC,KAAK,IAAM,KAAS,GAAQ;EAC1B,IAAM,IAAO,EAAO,EAAO,SAAS;EAEpC,AAAI,KAAQ,EAAM,MAAM,EAAK,KAC3B,EAAK,KAAK,KAAK,IAAI,EAAK,IAAI,EAAM,EAAE,IAEpC,EAAO,KAAK,CAAC,EAAM,IAAI,EAAM,EAAE,CAAC;CAEpC;CAEA,OAAO;AACT;AA+BA,SAAgB,EAAU,GAAc,GAA6C;CACnF,IAAI,CAAC,GAAM,OAAO,CAAC;CAEnB,IAAI,CAAC,EAAO,QAAQ,OAAO,CAAC;EAAE,aAAa;EAAO;CAAK,CAAC;CAExD,IAAM,IAAyB,CAAC,GAC5B,IAAS;CAEb,KAAK,IAAM,CAAC,GAAO,MAAQ,GASzB,AARI,IAAQ,KACV,EAAM,KAAK;EAAE,aAAa;EAAO,MAAM,EAAK,MAAM,GAAQ,CAAK;CAAE,CAAC,GAGhE,IAAM,KACR,EAAM,KAAK;EAAE,aAAa;EAAM,MAAM,EAAK,MAAM,GAAO,CAAG;CAAE,CAAC,GAGhE,IAAS;CAOX,OAJI,IAAS,EAAK,UAChB,EAAM,KAAK;EAAE,aAAa;EAAO,MAAM,EAAK,MAAM,CAAM;CAAE,CAAC,GAGtD;AACT;AAyBA,SAAgB,EAAkB,GAAyB,GAAyB,GAA+B;CAGjH,OAAO,EAAU,GAFH,EAAO,QAAQ,MAAM,MAAM,EAAE,UAAU,CAE9B,CAAA,EAAO,UAAU,CAAC,CAAC;AAC5C"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./adapters.cjs"),t=require("./errors.cjs"),n=require("./highlight.cjs"),r=require("./scout-index.cjs"),i=require("./reactive.cjs"),a=require("./segment.cjs");exports.ScoutDisposedError=t.ScoutDisposedError,exports.ScoutError=t.ScoutError,exports.ScoutIndexError=t.ScoutIndexError,exports.createIndex=r.createIndex,exports.createReactiveSearch=i.createReactiveSearch,exports.createSearch=i.createSearch,exports.findMatchRanges=n.findMatchRanges,exports.highlight=n.highlight,exports.highlightField=n.highlightField,exports.segmentWords=a.segmentWords,exports.toFilterPredicate=e.toFilterPredicate,exports.toSearchFn=e.toSearchFn;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { toFilterPredicate, toSearchFn } from './adapters';
|
|
2
|
+
export { ScoutDisposedError, ScoutError, ScoutIndexError } from './errors';
|
|
3
|
+
export { findMatchRanges, highlight, highlightField } from './highlight';
|
|
4
|
+
export { createReactiveSearch, createSearch } from './reactive';
|
|
5
|
+
export type { ReactiveSearch } from './reactive';
|
|
6
|
+
export type { ScoutIndex } from './scout-index';
|
|
7
|
+
export { createIndex } from './scout-index';
|
|
8
|
+
export { segmentWords } from './segment';
|
|
9
|
+
export type { CreateSearchOptions, FieldDef, FieldMatch, HighlightPart, ScoutIndexOptions, SearchConstraints, SearchResult, SearchState, } from './types';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAChE,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,YAAY,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,YAAY,EACV,mBAAmB,EACnB,QAAQ,EACR,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,iBAAiB,EACjB,YAAY,EACZ,WAAW,GACZ,MAAM,SAAS,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { toFilterPredicate as e, toSearchFn as t } from "./adapters.js";
|
|
2
|
+
import { ScoutDisposedError as n, ScoutError as r, ScoutIndexError as i } from "./errors.js";
|
|
3
|
+
import { findMatchRanges as a, highlight as o, highlightField as s } from "./highlight.js";
|
|
4
|
+
import { createIndex as c } from "./scout-index.js";
|
|
5
|
+
import { createReactiveSearch as l, createSearch as u } from "./reactive.js";
|
|
6
|
+
import { segmentWords as d } from "./segment.js";
|
|
7
|
+
export { n as ScoutDisposedError, r as ScoutError, i as ScoutIndexError, c as createIndex, l as createReactiveSearch, u as createSearch, a as findMatchRanges, o as highlight, s as highlightField, d as segmentWords, e as toFilterPredicate, t as toSearchFn };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./errors.cjs"),t=require("./scout-index.cjs");let n=require("@vielzeug/ripple");var r=200;function i(t,i={}){let{debounce:a=r,limit:o,minQueryLength:s,threshold:c}=i,l=(0,n.signal)(``,{name:`scout:query`}),u=(0,n.signal)(``,{name:`scout:committedQuery`}),d=(0,n.signal)(0,{name:`scout:indexVersion`}),f=t.onMutate(()=>{d.value++}),p=(0,n.computed)(()=>l.value!==u.value,{name:`scout:isSearching`}),m=(0,n.computed)(()=>(d.value,t.search(u.value,{limit:o,minQueryLength:s,threshold:c})),{name:`scout:results`}),h=null;function g(){h!==null&&(clearTimeout(h),h=null)}let _=l.subscribe(()=>{let e=l.peek();if(g(),e!==u.peek()){if(a===0){u.value=e;return}h=setTimeout(()=>{u.value=e,h=null},a)}});function v(){if(y)throw new e.ScoutDisposedError(`SearchState.clear() called after dispose()`);g(),(0,n.batch)(()=>{l.value=``,u.value=``})}let y=!1,b=new AbortController;function x(){y=!0,b.abort(),g(),_.dispose(),f(),l.dispose(),u.dispose(),p.dispose(),m.dispose(),d.dispose()}return{clear:v,get disposalSignal(){return b.signal},dispose:x,get disposed(){return y},isSearching:p,query:l,results:m,[Symbol.dispose](){x()}}}function a(e,n){let r=t.createIndex(e,{fields:n.fields,limit:n.limit,minQueryLength:n.minQueryLength,threshold:n.threshold});return{...i(r,{debounce:n.debounce}),index:r}}exports.createReactiveSearch=a,exports.createSearch=i;
|
|
2
|
+
//# sourceMappingURL=reactive.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reactive.cjs","names":[],"sources":["../src/reactive.ts"],"sourcesContent":["import { batch, computed, signal } from '@vielzeug/ripple';\n\nimport type { CreateSearchOptions, ScoutIndexOptions, SearchResult, SearchState } from './types';\n\nimport { ScoutDisposedError } from './errors';\nimport { createIndex, type ScoutIndex } from './scout-index';\n\n/**\n * Combined index + reactive search state returned by `createReactiveSearch()`.\n * Exposes the underlying `ScoutIndex` for incremental mutations (`add`, `remove`, `reindex`).\n */\nexport type ReactiveSearch<T> = SearchState<T> & {\n readonly index: ScoutIndex<T>;\n};\n\nconst DEFAULT_DEBOUNCE = 200;\n\n/**\n * Creates a reactive search state backed by a `ScoutIndex`.\n *\n * - Set `state.query.value` to trigger a (debounced) search.\n * - Read `state.results.value` inside an `effect` or `computed` to consume results reactively.\n * - `state.isSearching.value` is `true` while debouncing, `false` otherwise.\n * - Call `state.dispose()` (or `using state = createSearch(...)`) to release subscriptions.\n *\n * @example\n * ```ts\n * const index = createIndex(users, { fields: ['name', 'email'] });\n * const search = createSearch(index, { debounce: 150 });\n *\n * effect(() => {\n * const results = search.results.value;\n * renderList(results.map(r => r.item));\n * });\n *\n * // Wire to an input\n * input.addEventListener('input', e => {\n * search.query.value = e.currentTarget.value;\n * });\n *\n * // Clean up\n * search.dispose();\n * ```\n *\n * `results` also updates when the index is mutated directly via `index.add()` / `.remove()`\n * / `.reindex()` (not just when `query` changes), by subscribing to `index.onMutate()`.\n *\n * @param index - A `ScoutIndex` built with `createIndex()`.\n * @param options.debounce - Milliseconds to wait before committing query changes. Default: `200`.\n * @param options.limit - Override the index-level result limit.\n * @param options.minQueryLength - Override the index-level minimum query length.\n * @param options.threshold - Override the index-level score threshold.\n */\nexport function createSearch<T>(index: ScoutIndex<T>, options: CreateSearchOptions = {}): SearchState<T> {\n const { debounce: debounceMs = DEFAULT_DEBOUNCE, limit, minQueryLength, threshold } = options;\n\n const query = signal<string>('', { name: 'scout:query' });\n const committedQuery = signal<string>('', { name: 'scout:committedQuery' });\n const indexVersion = signal(0, { name: 'scout:indexVersion' });\n\n const unsubscribeMutations = index.onMutate(() => {\n indexVersion.value++;\n });\n\n const isSearching = computed(() => query.value !== committedQuery.value, { name: 'scout:isSearching' });\n\n const results = computed<SearchResult<T>[]>(\n () => {\n // Reading .value establishes a dependency so index mutations trigger a recompute.\n void indexVersion.value;\n\n return index.search(committedQuery.value, { limit, minQueryLength, threshold });\n },\n { name: 'scout:results' },\n );\n\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n function cancelTimer(): void {\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n }\n\n const subscription = query.subscribe(() => {\n const q = query.peek();\n\n cancelTimer();\n\n if (q === committedQuery.peek()) return;\n\n if (debounceMs === 0) {\n committedQuery.value = q;\n\n return;\n }\n\n timer = setTimeout(() => {\n committedQuery.value = q;\n timer = null;\n }, debounceMs);\n });\n\n function clear(): void {\n if (isDisposed) throw new ScoutDisposedError('SearchState.clear() called after dispose()');\n\n cancelTimer();\n\n batch(() => {\n query.value = '';\n committedQuery.value = '';\n });\n }\n\n let isDisposed = false;\n const ac = new AbortController();\n\n function dispose(): void {\n isDisposed = true;\n ac.abort();\n cancelTimer();\n subscription.dispose();\n unsubscribeMutations();\n query.dispose();\n committedQuery.dispose();\n isSearching.dispose();\n results.dispose();\n indexVersion.dispose();\n }\n\n return {\n clear,\n get disposalSignal(): AbortSignal {\n return ac.signal;\n },\n dispose,\n get disposed(): boolean {\n return isDisposed;\n },\n isSearching,\n query,\n results,\n [Symbol.dispose](): void {\n dispose();\n },\n };\n}\n\n/**\n * Creates a `ScoutIndex` and a reactive search state in one call — the shorthand\n * for the common pattern of `createIndex` + `createSearch`.\n *\n * The returned `ReactiveSearch` exposes the underlying index via `.index` for\n * incremental mutations (`add`, `remove`, `reindex`) after construction.\n *\n * @example\n * ```ts\n * const search = createReactiveSearch(users, {\n * fields: [{ field: 'name', weight: 2 }, 'email'],\n * debounce: 150,\n * });\n *\n * effect(() => renderList(search.results.value.map(r => r.item)));\n *\n * // Wire to an input\n * input.addEventListener('input', e => { search.query.value = e.currentTarget.value; });\n *\n * // Add a new item at runtime\n * search.index.add(newUser);\n * ```\n *\n * @param items - Initial corpus to index.\n * @param options - Index options (`fields`, `limit`, `minQueryLength`, `threshold`) plus optional `debounce`.\n */\nexport function createReactiveSearch<T>(\n items: T[],\n options: ScoutIndexOptions<T> & Pick<CreateSearchOptions, 'debounce'>,\n): ReactiveSearch<T> {\n const index = createIndex(items, {\n fields: options.fields,\n limit: options.limit,\n minQueryLength: options.minQueryLength,\n threshold: options.threshold,\n });\n const state = createSearch(index, { debounce: options.debounce });\n\n return { ...state, index };\n}\n"],"mappings":"iGAeA,IAAM,EAAmB,IAsCzB,SAAgB,EAAgB,EAAsB,EAA+B,CAAC,EAAmB,CACvG,GAAM,CAAE,SAAU,EAAa,EAAkB,QAAO,iBAAgB,aAAc,EAEhF,GAAA,EAAA,EAAA,OAAA,CAAuB,GAAI,CAAE,KAAM,aAAc,CAAC,EAClD,GAAA,EAAA,EAAA,OAAA,CAAgC,GAAI,CAAE,KAAM,sBAAuB,CAAC,EACpE,GAAA,EAAA,EAAA,OAAA,CAAsB,EAAG,CAAE,KAAM,oBAAqB,CAAC,EAEvD,EAAuB,EAAM,aAAe,CAChD,EAAa,OACf,CAAC,EAEK,GAAA,EAAA,EAAA,SAAA,KAA6B,EAAM,QAAU,EAAe,MAAO,CAAE,KAAM,mBAAoB,CAAC,EAEhG,GAAA,EAAA,EAAA,SAAA,MAGF,EAAkB,MAEX,EAAM,OAAO,EAAe,MAAO,CAAE,QAAO,iBAAgB,WAAU,CAAC,GAEhF,CAAE,KAAM,eAAgB,CAC1B,EAEI,EAA8C,KAElD,SAAS,GAAoB,CACvB,IAAU,OACZ,aAAa,CAAK,EAClB,EAAQ,KAEZ,CAEA,IAAM,EAAe,EAAM,cAAgB,CACzC,IAAM,EAAI,EAAM,KAAK,EAErB,KAAY,EAER,IAAM,EAAe,KAAK,EAE9B,IAAI,IAAe,EAAG,CACpB,EAAe,MAAQ,EAEvB,MACF,CAEA,EAAQ,eAAiB,CACvB,EAAe,MAAQ,EACvB,EAAQ,IACV,EAAG,CAAU,CALb,CAMF,CAAC,EAED,SAAS,GAAc,CACrB,GAAI,EAAY,MAAM,IAAI,EAAA,mBAAmB,4CAA4C,EAEzF,EAAY,GAEZ,EAAA,EAAA,MAAA,KAAY,CACV,EAAM,MAAQ,GACd,EAAe,MAAQ,EACzB,CAAC,CACH,CAEA,IAAI,EAAa,GACX,EAAK,IAAI,gBAEf,SAAS,GAAgB,CACvB,EAAa,GACb,EAAG,MAAM,EACT,EAAY,EACZ,EAAa,QAAQ,EACrB,EAAqB,EACrB,EAAM,QAAQ,EACd,EAAe,QAAQ,EACvB,EAAY,QAAQ,EACpB,EAAQ,QAAQ,EAChB,EAAa,QAAQ,CACvB,CAEA,MAAO,CACL,QACA,IAAI,gBAA8B,CAChC,OAAO,EAAG,MACZ,EACA,UACA,IAAI,UAAoB,CACtB,OAAO,CACT,EACA,cACA,QACA,UACA,CAAC,OAAO,UAAiB,CACvB,EAAQ,CACV,CACF,CACF,CA4BA,SAAgB,EACd,EACA,EACmB,CACnB,IAAM,EAAQ,EAAA,YAAY,EAAO,CAC/B,OAAQ,EAAQ,OAChB,MAAO,EAAQ,MACf,eAAgB,EAAQ,eACxB,UAAW,EAAQ,SACrB,CAAC,EAGD,MAAO,CAAE,GAFK,EAAa,EAAO,CAAE,SAAU,EAAQ,QAAS,CAEnD,EAAO,OAAM,CAC3B"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { CreateSearchOptions, ScoutIndexOptions, SearchState } from './types';
|
|
2
|
+
import { type ScoutIndex } from './scout-index';
|
|
3
|
+
/**
|
|
4
|
+
* Combined index + reactive search state returned by `createReactiveSearch()`.
|
|
5
|
+
* Exposes the underlying `ScoutIndex` for incremental mutations (`add`, `remove`, `reindex`).
|
|
6
|
+
*/
|
|
7
|
+
export type ReactiveSearch<T> = SearchState<T> & {
|
|
8
|
+
readonly index: ScoutIndex<T>;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Creates a reactive search state backed by a `ScoutIndex`.
|
|
12
|
+
*
|
|
13
|
+
* - Set `state.query.value` to trigger a (debounced) search.
|
|
14
|
+
* - Read `state.results.value` inside an `effect` or `computed` to consume results reactively.
|
|
15
|
+
* - `state.isSearching.value` is `true` while debouncing, `false` otherwise.
|
|
16
|
+
* - Call `state.dispose()` (or `using state = createSearch(...)`) to release subscriptions.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const index = createIndex(users, { fields: ['name', 'email'] });
|
|
21
|
+
* const search = createSearch(index, { debounce: 150 });
|
|
22
|
+
*
|
|
23
|
+
* effect(() => {
|
|
24
|
+
* const results = search.results.value;
|
|
25
|
+
* renderList(results.map(r => r.item));
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* // Wire to an input
|
|
29
|
+
* input.addEventListener('input', e => {
|
|
30
|
+
* search.query.value = e.currentTarget.value;
|
|
31
|
+
* });
|
|
32
|
+
*
|
|
33
|
+
* // Clean up
|
|
34
|
+
* search.dispose();
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* `results` also updates when the index is mutated directly via `index.add()` / `.remove()`
|
|
38
|
+
* / `.reindex()` (not just when `query` changes), by subscribing to `index.onMutate()`.
|
|
39
|
+
*
|
|
40
|
+
* @param index - A `ScoutIndex` built with `createIndex()`.
|
|
41
|
+
* @param options.debounce - Milliseconds to wait before committing query changes. Default: `200`.
|
|
42
|
+
* @param options.limit - Override the index-level result limit.
|
|
43
|
+
* @param options.minQueryLength - Override the index-level minimum query length.
|
|
44
|
+
* @param options.threshold - Override the index-level score threshold.
|
|
45
|
+
*/
|
|
46
|
+
export declare function createSearch<T>(index: ScoutIndex<T>, options?: CreateSearchOptions): SearchState<T>;
|
|
47
|
+
/**
|
|
48
|
+
* Creates a `ScoutIndex` and a reactive search state in one call — the shorthand
|
|
49
|
+
* for the common pattern of `createIndex` + `createSearch`.
|
|
50
|
+
*
|
|
51
|
+
* The returned `ReactiveSearch` exposes the underlying index via `.index` for
|
|
52
|
+
* incremental mutations (`add`, `remove`, `reindex`) after construction.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* const search = createReactiveSearch(users, {
|
|
57
|
+
* fields: [{ field: 'name', weight: 2 }, 'email'],
|
|
58
|
+
* debounce: 150,
|
|
59
|
+
* });
|
|
60
|
+
*
|
|
61
|
+
* effect(() => renderList(search.results.value.map(r => r.item)));
|
|
62
|
+
*
|
|
63
|
+
* // Wire to an input
|
|
64
|
+
* input.addEventListener('input', e => { search.query.value = e.currentTarget.value; });
|
|
65
|
+
*
|
|
66
|
+
* // Add a new item at runtime
|
|
67
|
+
* search.index.add(newUser);
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* @param items - Initial corpus to index.
|
|
71
|
+
* @param options - Index options (`fields`, `limit`, `minQueryLength`, `threshold`) plus optional `debounce`.
|
|
72
|
+
*/
|
|
73
|
+
export declare function createReactiveSearch<T>(items: T[], options: ScoutIndexOptions<T> & Pick<CreateSearchOptions, 'debounce'>): ReactiveSearch<T>;
|
|
74
|
+
//# sourceMappingURL=reactive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reactive.d.ts","sourceRoot":"","sources":["../src/reactive.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAgB,WAAW,EAAE,MAAM,SAAS,CAAC;AAGjG,OAAO,EAAe,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAE7D;;;GAGG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,GAAG;IAC/C,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;CAC/B,CAAC;AAIF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,mBAAwB,GAAG,WAAW,CAAC,CAAC,CAAC,CA8FvG;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EACpC,KAAK,EAAE,CAAC,EAAE,EACV,OAAO,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,mBAAmB,EAAE,UAAU,CAAC,GACpE,cAAc,CAAC,CAAC,CAAC,CAUnB"}
|