From 7c6af4efc514bd4f65ec7fc20e03f03f4f0bd417 Mon Sep 17 00:00:00 2001 From: Kiro Risk <565580+krisk@users.noreply.github.com> Date: Fri, 5 Jun 2026 12:23:47 -0700 Subject: [PATCH] build: replace rollup/babel/terser build with tsdown Collapse the hand-rolled Rollup orchestrator (scripts/configs.cjs + config-types.cjs + build.main.cjs, 373 lines) and 8 build devDeps into one tsdown.config.ts (rolldown engine + built-in dts). Chosen over tsup after a problem-blind Codex assessment and a spike: tsdown emits constructor-direct CJS (require('fuse.js') === Fuse) and named worker CJS automatically, no footer hacks. Behavior-preserving on the public surface: - identical 15-file dist set (12 runtime + 3 dts) - CJS require() contract unchanged; named fuse-worker.cjs export preserved - feature-flag DCE intact (basic build strips logical/extended/token) - dev/prod default-export split preserved - Fuse.version injected via define (bare __VERSION__ + ambient declare; honors process.env.VERSION for releases) Worker-URL resolution no longer depends on a bundler's import.meta.url codegen: a per-format __WORKER_IS_CJS__ define routes browser-CJS to a document base while ESM keeps the literal new URL('./fuse.worker.mjs', import.meta.url) so bundlers still detect and rewrite the worker asset. test/worker-url.test.js gates node-ESM, node-CJS, browser-ESM (Vite), and browser-CJS. test/cjs-interop.test.js gates require() === constructor for every lib cjs plus the named worker cjs. Sizes: minified artifacts are 287-313 bytes smaller raw; gzip is +8..+28 bytes (<=0.4%, accepted) since rolldown's minifier compresses marginally worse than Terser. Build/release now requires Node >=22.18 (tsdown engines): CI builds on 22/24 and runs the Node 20 leg against the committed dist. docs-deploy reconciliation is descoped (deploy-docs.sh is the only path that bumps docs before deploy; needs a release-ordering fix first). Removed devDeps: rollup, @rollup/plugin-babel, @rollup/plugin-node-resolve, @rollup/plugin-replace, @babel/core, @babel/preset-typescript, terser, rollup-plugin-dts. Added: tsdown. --- .github/workflows/nodejs.yml | 4 + .nvmrc | 2 +- DEVELOPERS.md | 10 +- dist/fuse-worker.cjs | 431 ++-- dist/fuse-worker.d.ts | 244 +-- dist/fuse-worker.mjs | 427 ++-- dist/fuse.basic.cjs | 2621 ++++++++++-------------- dist/fuse.basic.min.cjs | 2 +- dist/fuse.basic.min.mjs | 2 +- dist/fuse.basic.mjs | 2620 ++++++++++-------------- dist/fuse.cjs | 3611 ++++++++++++++-------------------- dist/fuse.d.ts | 523 ++--- dist/fuse.min.cjs | 2 +- dist/fuse.min.mjs | 2 +- dist/fuse.mjs | 3610 ++++++++++++++------------------- dist/fuse.worker.min.mjs | 2 +- dist/fuse.worker.mjs | 3606 ++++++++++++++------------------- package-lock.json | 1545 ++++++++------- package.json | 15 +- scripts/build.main.cjs | 118 -- scripts/config-types.cjs | 57 - scripts/configs.cjs | 198 -- src/entry.ts | 2 +- src/env.d.ts | 10 + src/workers/FuseWorker.ts | 52 +- test/cjs-interop.test.js | 53 + test/package-types.test.ts | 59 +- test/worker-url.test.js | 164 ++ tsdown.config.ts | 146 ++ vitest.config.ts | 12 + 30 files changed, 8714 insertions(+), 11436 deletions(-) delete mode 100644 scripts/build.main.cjs delete mode 100644 scripts/config-types.cjs delete mode 100644 scripts/configs.cjs create mode 100644 test/cjs-interop.test.js create mode 100644 test/worker-url.test.js create mode 100644 tsdown.config.ts diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 239c6f69c..dc3438c25 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -26,5 +26,9 @@ jobs: with: node-version: ${{ matrix.node }} - run: npm install --prefer-offline --no-audit --no-fund + # The build tool (tsdown) requires Node >= 22.18. The published library and + # its tests still run on Node 20: that leg tests the committed dist rather + # than rebuilding. Node 22/24 rebuild from source before testing. - run: npm run build + if: matrix.node != '20' - run: npm run test diff --git a/.nvmrc b/.nvmrc index 209e3ef4b..2bd5a0a98 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20 +22 diff --git a/DEVELOPERS.md b/DEVELOPERS.md index 24367ff87..2577421b2 100644 --- a/DEVELOPERS.md +++ b/DEVELOPERS.md @@ -7,6 +7,8 @@ ## Setup +Building and releasing require **Node >= 22.18** (the bundler, [tsdown](https://tsdown.dev), needs it); `nvm use` picks up the pinned `.nvmrc`. The published library runs on far older Node, and the CI test matrix still includes Node 20. + ```shell npm install ``` @@ -19,8 +21,7 @@ npm install | `npm test` | Run the test suite (vitest) | | `npm run typecheck` | Type-check the source without emitting | | `npm run lint` | Lint source and tests (eslint) | -| `npm run dev` | Watch mode for ESM dev build | -| `npm run dev:cjs` | Watch mode for CJS dev build | +| `npm run dev` | Watch mode (rebuilds all dist targets via tsdown) | | `npm run bench` | Build, then run the core search + index regression benchmarks | | `npm run bench:search` | Search benchmark (object/string fuzzy, scaling, remove) | | `npm run bench:index` | Index creation benchmark across dataset sizes | @@ -30,7 +31,7 @@ npm install ## Project Structure -Source is TypeScript. Types are emitted from source via `rollup-plugin-dts`. +Source is TypeScript. The build, including `.d.ts` emit, runs through [tsdown](https://tsdown.dev) (rolldown engine); see `tsdown.config.ts`. ``` src/ @@ -42,7 +43,8 @@ src/ entry.ts — Entry point with static methods and type exports test/ — Tests and fixtures bench/ — Benchmarks (search, index creation, extended, tokens, workers) -scripts/ — Rollup build configs +tsdown.config.ts — Build config (all dist targets + types) +scripts/ — Release + docs helpers (bump-docs, deploy-docs, release) dist/ — Built output (CJS, ESM, .d.ts) ``` diff --git a/dist/fuse-worker.cjs b/dist/fuse-worker.cjs index aa42966f1..499f80c99 100644 --- a/dist/fuse-worker.cjs +++ b/dist/fuse-worker.cjs @@ -6,252 +6,199 @@ * * http://www.apache.org/licenses/LICENSE-2.0 */ +Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); -'use strict'; - -var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null; -const FUSE_WORKER_UNSUPPORTED_FN_OPTION = option => `FuseWorker does not support function-valued option '${option}': ` + `functions cannot be transferred to Web Workers via postMessage. ` + `Remove this option or fall back to Fuse.`; -const FUSE_WORKER_TOKEN_SEARCH_UNSUPPORTED = `FuseWorker does not support useTokenSearch: token search depends on ` + `corpus-level statistics (df, fieldCount) that are computed per shard, ` + `so per-shard scores would diverge from single-thread Fuse. Use Fuse on ` + `the main thread for token search.`; - -/// +//#region src/core/errorMessages.ts +const FUSE_WORKER_UNSUPPORTED_FN_OPTION = (option) => `FuseWorker does not support function-valued option '${option}': functions cannot be transferred to Web Workers via postMessage. Remove this option or fall back to Fuse.`; +const FUSE_WORKER_TOKEN_SEARCH_UNSUPPORTED = "FuseWorker does not support useTokenSearch: token search depends on corpus-level statistics (df, fieldCount) that are computed per shard, so per-shard scores would diverge from single-thread Fuse. Use Fuse on the main thread for token search."; +//#endregion +//#region src/workers/FuseWorker.ts const DEFAULT_MAX_WORKERS = 8; function getDefaultWorkerCount() { - const hw = typeof navigator !== 'undefined' ? navigator.hardwareConcurrency || 4 : 4; - return Math.min(hw, DEFAULT_MAX_WORKERS); + const hw = typeof navigator !== "undefined" ? navigator.hardwareConcurrency || 4 : 4; + return Math.min(hw, DEFAULT_MAX_WORKERS); } -class FuseWorker { - _shards = null; - _addCursor = 0; - _initPromise = null; - _pending = new Map(); - _nextId = 0; - constructor(docs, options, workerOptions) { - this._docs = docs.slice(); - this._options = options || {}; - this._workerOptions = workerOptions || {}; - // Reject function-valued options eagerly. Without this check, postMessage - // throws DataCloneError on first search() rather than at construction. - FuseWorker._assertNoFunctionOptions(this._options); - // Token search needs global corpus statistics, but each shard would build - // its own — scores would diverge from single-thread Fuse. Refuse upfront - // rather than silently returning ordering that doesn't match. - if (this._options.useTokenSearch) { - throw new Error(FUSE_WORKER_TOKEN_SEARCH_UNSUPPORTED); - } - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore -- import.meta.url is resolved by Rollup at build time - this._workerUrl = this._workerOptions.workerUrl || new URL('./fuse.worker.mjs', (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('fuse-worker.cjs', document.baseURI).href))); - } - static _assertNoFunctionOptions(options) { - if (typeof options.sortFn === 'function') { - throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION('sortFn')); - } - if (typeof options.getFn === 'function') { - throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION('getFn')); - } - if (typeof options.tokenize === 'function') { - throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION('tokenize')); - } - const keys = options.keys; - if (Array.isArray(keys)) { - for (let i = 0, len = keys.length; i < len; i += 1) { - const key = keys[i]; - if (key && typeof key === 'object' && !Array.isArray(key)) { - if (typeof key.getFn === 'function') { - const name = key.name; - const label = Array.isArray(name) ? name.join('.') : name ?? String(i); - throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION(`keys[${label}].getFn`)); - } - } - } - } - } - _getNumWorkers() { - return this._workerOptions.numWorkers || getDefaultWorkerCount(); - } - _ensureInit() { - if (this._initPromise) return this._initPromise; - this._initPromise = this._init(); - return this._initPromise; - } - _spawnWorker() { - const worker = new Worker(this._workerUrl, { - type: 'module' - }); - worker.onmessage = e => { - const { - id, - result, - error - } = e.data; - const handler = this._pending.get(id); - if (!handler) return; - this._pending.delete(id); - if (error) { - handler.reject(new Error(error)); - } else { - handler.resolve(result); - } - }; - worker.onerror = e => { - for (const [, handler] of this._pending) { - handler.reject(new Error(e.message)); - } - }; - return worker; - } - _workerInitOptions() { - // Force includeScore so the main thread can do a global (score, idx) - // tie-break across shards. Score is stripped from the final result if the - // caller didn't ask for it. - return { - ...this._options, - includeScore: true - }; - } - async _init() { - const numWorkers = this._getNumWorkers(); - const chunkSize = Math.ceil(this._docs.length / numWorkers); - this._shards = []; - this._addCursor = 0; - const workerInitOptions = this._workerInitOptions(); - const initPromises = []; - for (let i = 0; i < numWorkers; i++) { - const start = i * chunkSize; - const end = Math.min(start + chunkSize, this._docs.length); - const chunk = this._docs.slice(start, end); - const globalIndices = []; - for (let j = start; j < end; j += 1) globalIndices.push(j); - const shard = { - worker: this._spawnWorker(), - globalIndices - }; - this._shards.push(shard); - initPromises.push(this._call(shard.worker, 'init', [chunk, workerInitOptions])); - } - await Promise.all(initPromises); - } - _call(worker, method, args) { - const id = this._nextId++; - return new Promise((resolve, reject) => { - this._pending.set(id, { - resolve, - reject - }); - worker.postMessage({ - id, - method, - args - }); - }); - } - async search(query, options) { - await this._ensureInit(); - const shards = this._shards; - const results = await Promise.all(shards.map(s => this._call(s.worker, 'search', [query, options]))); - - // Merge results from all shards, rewriting refIndex from shard-local to global - const merged = []; - for (let i = 0, len = results.length; i < len; i += 1) { - const { - globalIndices - } = shards[i]; - for (const r of results[i]) { - merged.push({ - ...r, - refIndex: globalIndices[r.refIndex] - }); - } - } - const shouldSort = this._options.shouldSort !== false; - if (shouldSort) { - // Mirror Fuse's default sortFn: (score, idx) tie-break, but on the - // GLOBAL refIndex so equal-score results across shards collapse into a - // deterministic order matching single-thread Fuse. - merged.sort((a, b) => { - const sa = a.score ?? 0; - const sb = b.score ?? 0; - if (sa === sb) { - return a.refIndex < b.refIndex ? -1 : 1; - } - return sa < sb ? -1 : 1; - }); - } else { - // Restore global collection order. Round-robin add() and - // shard-concatenation order otherwise leak through. - merged.sort((a, b) => a.refIndex - b.refIndex); - } - - // Workers always include score so the merge above can tie-break; strip it - // here if the caller didn't ask for it. Rebuild the object instead of - // `delete`-ing — `delete` deopts the shape and roughly doubles the - // post-sort cost on large result sets. - if (!this._options.includeScore) { - for (let i = 0, len = merged.length; i < len; i += 1) { - const r = merged[i]; - merged[i] = r.matches !== undefined ? { - item: r.item, - refIndex: r.refIndex, - matches: r.matches - } : { - item: r.item, - refIndex: r.refIndex - }; - } - } - const limit = options?.limit; - if (limit && limit > 0) { - return merged.slice(0, limit); - } - return merged; - } - async add(doc) { - await this._ensureInit(); - const shards = this._shards; - const shard = shards[this._addCursor % shards.length]; - this._addCursor += 1; - const globalIdx = this._docs.length; - this._docs.push(doc); - shard.globalIndices.push(globalIdx); - await this._call(shard.worker, 'add', [doc]); - } - async setCollection(docs) { - this._docs = docs.slice(); - if (!this._shards) { - this._initPromise = null; - return; - } - const shards = this._shards; - const chunkSize = Math.ceil(this._docs.length / shards.length); - this._addCursor = 0; - const tasks = []; - for (let i = 0, len = shards.length; i < len; i += 1) { - const start = i * chunkSize; - const end = Math.min(start + chunkSize, this._docs.length); - const chunk = this._docs.slice(start, end); - const globalIndices = []; - for (let j = start; j < end; j += 1) globalIndices.push(j); - shards[i].globalIndices = globalIndices; - tasks.push(this._call(shards[i].worker, 'setCollection', [chunk])); - } - await Promise.all(tasks); - } - terminate() { - if (this._shards) { - for (const { - worker - } of this._shards) { - worker.terminate(); - } - this._shards = null; - } - this._initPromise = null; - for (const [, handler] of this._pending) { - handler.reject(new Error('FuseWorker terminated')); - } - this._pending.clear(); - } +const browserWorkerBase = typeof document !== "undefined" ? document.currentScript?.src || new URL("fuse-worker.cjs", document.baseURI).href : void 0; +function resolveDefaultWorkerUrl() { + if (browserWorkerBase !== void 0) return new URL("./fuse.worker.mjs", browserWorkerBase); + try { + return new URL("./fuse.worker.mjs", require("url").pathToFileURL(__filename).href); + } catch { + if (browserWorkerBase !== void 0) return new URL("./fuse.worker.mjs", browserWorkerBase); + throw new Error("FuseWorker: unable to resolve the worker script URL automatically; pass workerOptions.workerUrl explicitly."); + } } +var FuseWorker = class FuseWorker { + constructor(docs, options, workerOptions) { + this._shards = null; + this._addCursor = 0; + this._initPromise = null; + this._pending = /* @__PURE__ */ new Map(); + this._nextId = 0; + this._docs = docs.slice(); + this._options = options || {}; + this._workerOptions = workerOptions || {}; + FuseWorker._assertNoFunctionOptions(this._options); + if (this._options.useTokenSearch) throw new Error(FUSE_WORKER_TOKEN_SEARCH_UNSUPPORTED); + this._workerUrl = this._workerOptions.workerUrl || resolveDefaultWorkerUrl(); + } + static _assertNoFunctionOptions(options) { + if (typeof options.sortFn === "function") throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION("sortFn")); + if (typeof options.getFn === "function") throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION("getFn")); + if (typeof options.tokenize === "function") throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION("tokenize")); + const keys = options.keys; + if (Array.isArray(keys)) for (let i = 0, len = keys.length; i < len; i += 1) { + const key = keys[i]; + if (key && typeof key === "object" && !Array.isArray(key)) { + if (typeof key.getFn === "function") { + const name = key.name; + const label = Array.isArray(name) ? name.join(".") : name ?? String(i); + throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION(`keys[${label}].getFn`)); + } + } + } + } + _getNumWorkers() { + return this._workerOptions.numWorkers || getDefaultWorkerCount(); + } + _ensureInit() { + if (this._initPromise) return this._initPromise; + this._initPromise = this._init(); + return this._initPromise; + } + _spawnWorker() { + const worker = new Worker(this._workerUrl, { type: "module" }); + worker.onmessage = (e) => { + const { id, result, error } = e.data; + const handler = this._pending.get(id); + if (!handler) return; + this._pending.delete(id); + if (error) handler.reject(new Error(error)); + else handler.resolve(result); + }; + worker.onerror = (e) => { + for (const [, handler] of this._pending) handler.reject(new Error(e.message)); + }; + return worker; + } + _workerInitOptions() { + return { + ...this._options, + includeScore: true + }; + } + async _init() { + const numWorkers = this._getNumWorkers(); + const chunkSize = Math.ceil(this._docs.length / numWorkers); + this._shards = []; + this._addCursor = 0; + const workerInitOptions = this._workerInitOptions(); + const initPromises = []; + for (let i = 0; i < numWorkers; i++) { + const start = i * chunkSize; + const end = Math.min(start + chunkSize, this._docs.length); + const chunk = this._docs.slice(start, end); + const globalIndices = []; + for (let j = start; j < end; j += 1) globalIndices.push(j); + const shard = { + worker: this._spawnWorker(), + globalIndices + }; + this._shards.push(shard); + initPromises.push(this._call(shard.worker, "init", [chunk, workerInitOptions])); + } + await Promise.all(initPromises); + } + _call(worker, method, args) { + const id = this._nextId++; + return new Promise((resolve, reject) => { + this._pending.set(id, { + resolve, + reject + }); + worker.postMessage({ + id, + method, + args + }); + }); + } + async search(query, options) { + await this._ensureInit(); + const shards = this._shards; + const results = await Promise.all(shards.map((s) => this._call(s.worker, "search", [query, options]))); + const merged = []; + for (let i = 0, len = results.length; i < len; i += 1) { + const { globalIndices } = shards[i]; + for (const r of results[i]) merged.push({ + ...r, + refIndex: globalIndices[r.refIndex] + }); + } + if (this._options.shouldSort !== false) merged.sort((a, b) => { + const sa = a.score ?? 0; + const sb = b.score ?? 0; + if (sa === sb) return a.refIndex < b.refIndex ? -1 : 1; + return sa < sb ? -1 : 1; + }); + else merged.sort((a, b) => a.refIndex - b.refIndex); + if (!this._options.includeScore) for (let i = 0, len = merged.length; i < len; i += 1) { + const r = merged[i]; + merged[i] = r.matches !== void 0 ? { + item: r.item, + refIndex: r.refIndex, + matches: r.matches + } : { + item: r.item, + refIndex: r.refIndex + }; + } + const limit = options?.limit; + if (limit && limit > 0) return merged.slice(0, limit); + return merged; + } + async add(doc) { + await this._ensureInit(); + const shards = this._shards; + const shard = shards[this._addCursor % shards.length]; + this._addCursor += 1; + const globalIdx = this._docs.length; + this._docs.push(doc); + shard.globalIndices.push(globalIdx); + await this._call(shard.worker, "add", [doc]); + } + async setCollection(docs) { + this._docs = docs.slice(); + if (!this._shards) { + this._initPromise = null; + return; + } + const shards = this._shards; + const chunkSize = Math.ceil(this._docs.length / shards.length); + this._addCursor = 0; + const tasks = []; + for (let i = 0, len = shards.length; i < len; i += 1) { + const start = i * chunkSize; + const end = Math.min(start + chunkSize, this._docs.length); + const chunk = this._docs.slice(start, end); + const globalIndices = []; + for (let j = start; j < end; j += 1) globalIndices.push(j); + shards[i].globalIndices = globalIndices; + tasks.push(this._call(shards[i].worker, "setCollection", [chunk])); + } + await Promise.all(tasks); + } + terminate() { + if (this._shards) { + for (const { worker } of this._shards) worker.terminate(); + this._shards = null; + } + this._initPromise = null; + for (const [, handler] of this._pending) handler.reject(/* @__PURE__ */ new Error("FuseWorker terminated")); + this._pending.clear(); + } +}; -exports.FuseWorker = FuseWorker; +//#endregion +exports.FuseWorker = FuseWorker; \ No newline at end of file diff --git a/dist/fuse-worker.d.ts b/dist/fuse-worker.d.ts index 2b53c6aba..26ac37534 100644 --- a/dist/fuse-worker.d.ts +++ b/dist/fuse-worker.d.ts @@ -1,18 +1,18 @@ -// Type definitions for Fuse.js v7.4.1 -// TypeScript v6.0.3 + +//#region src/types.d.ts type RangeTuple = [number, number]; interface FuseOptionKeyObject { - name: string | string[]; - weight?: number; - getFn?: (obj: T) => ReadonlyArray | string | null | undefined; + name: string | string[]; + weight?: number; + getFn?: (obj: T) => ReadonlyArray | string | null | undefined; } type FuseOptionKey = FuseOptionKeyObject | string | string[]; interface KeyObject { - path: string[]; - id: string; - weight: number; - src: string | string[]; - getFn?: ((obj: any) => ReadonlyArray | string | null | undefined) | null; + path: string[]; + id: string; + weight: number; + src: string | string[]; + getFn?: ((obj: any) => ReadonlyArray | string | null | undefined) | null; } type FuseGetFunction = (obj: T, path: string | string[]) => ReadonlyArray | string; /** @@ -23,140 +23,140 @@ type FuseGetFunction = (obj: T, path: string | string[]) => ReadonlyArray string[]; type FuseSortFunctionItem = { - [key: string]: { - $: string; - } | { - $: string; - idx: number; - }[]; + [key: string]: { + $: string; + } | { + $: string; + idx: number; + }[]; }; type FuseSortFunctionMatch = { - score: number; - key: KeyObject; - value: string; - indices: ReadonlyArray[]; + score: number; + key: KeyObject; + value: string; + indices: ReadonlyArray[]; }; type FuseSortFunctionMatchList = FuseSortFunctionMatch & { - idx: number; + idx: number; }; type FuseSortFunctionArg = { - idx: number; - item: FuseSortFunctionItem; - score: number; - matches?: (FuseSortFunctionMatch | FuseSortFunctionMatchList)[]; + idx: number; + item: FuseSortFunctionItem; + score: number; + matches?: (FuseSortFunctionMatch | FuseSortFunctionMatchList)[]; }; type FuseSortFunction = (a: FuseSortFunctionArg, b: FuseSortFunctionArg) => number; interface IFuseOptions { - /** Indicates whether comparisons should be case sensitive. */ - isCaseSensitive?: boolean; - /** Indicates whether comparisons should ignore diacritics (accents). */ - ignoreDiacritics?: boolean; - /** Determines how close the match must be to the fuzzy location. */ - distance?: number; - /** When true, the matching function will continue to the end of a search pattern even if a perfect match has already been located in the string. */ - findAllMatches?: boolean; - /** The function to use to retrieve an object's value at the provided path. */ - getFn?: FuseGetFunction; - /** When `true`, search will ignore `location` and `distance`. */ - ignoreLocation?: boolean; - /** When `true`, the calculation for the relevance score will ignore the field-length norm. */ - ignoreFieldNorm?: boolean; - /** Determines how much the field-length norm affects scoring. */ - fieldNormWeight?: number; - /** Whether the matches should be included in the result set. */ - includeMatches?: boolean; - /** Whether the score should be included in the result set. */ - includeScore?: boolean; - /** List of keys that will be searched. */ - keys?: Array>; - /** Determines approximately where in the text is the pattern expected to be found. */ - location?: number; - /** Only the matches whose length exceeds this value will be returned. */ - minMatchCharLength?: number; - /** Whether to sort the result list, by score. */ - shouldSort?: boolean; - /** The function to use to sort all the results. */ - sortFn?: FuseSortFunction; - /** At what point does the match algorithm give up. */ - threshold?: number; - /** When `true`, it enables the use of unix-like search commands. */ - useExtendedSearch?: boolean; - /** When `true`, enables token search with TF-IDF scoring. */ - useTokenSearch?: boolean; - /** - * Tokenizer used by `useTokenSearch`, applied identically at index-build - * and query time. Accepts either a global `RegExp` or a function returning - * `string[]`. Defaults to `/[\p{L}\p{M}\p{N}_]+/gu`, which handles CJK, - * Cyrillic, Greek, Arabic, Hebrew, Devanagari, etc. out of the box. Use a - * function form (e.g. wrapping `Intl.Segmenter`) for word-segmentation in - * scripts without whitespace boundaries. - */ - tokenize?: RegExp | FuseTokenizeFunction; - /** - * How the words of a multi-word query combine, for `useTokenSearch` only. - * `'any'` (default) returns a record if it matches any query word (OR); - * `'all'` returns it only when every query word matches somewhere in the - * record — any field or array element (AND). Use `'all'` for filtering, - * where adding a word should narrow the results. Has no effect unless - * `useTokenSearch` is `true`. - */ - tokenMatch?: 'all' | 'any'; + /** Indicates whether comparisons should be case sensitive. */ + isCaseSensitive?: boolean; + /** Indicates whether comparisons should ignore diacritics (accents). */ + ignoreDiacritics?: boolean; + /** Determines how close the match must be to the fuzzy location. */ + distance?: number; + /** When true, the matching function will continue to the end of a search pattern even if a perfect match has already been located in the string. */ + findAllMatches?: boolean; + /** The function to use to retrieve an object's value at the provided path. */ + getFn?: FuseGetFunction; + /** When `true`, search will ignore `location` and `distance`. */ + ignoreLocation?: boolean; + /** When `true`, the calculation for the relevance score will ignore the field-length norm. */ + ignoreFieldNorm?: boolean; + /** Determines how much the field-length norm affects scoring. */ + fieldNormWeight?: number; + /** Whether the matches should be included in the result set. */ + includeMatches?: boolean; + /** Whether the score should be included in the result set. */ + includeScore?: boolean; + /** List of keys that will be searched. */ + keys?: Array>; + /** Determines approximately where in the text is the pattern expected to be found. */ + location?: number; + /** Only the matches whose length exceeds this value will be returned. */ + minMatchCharLength?: number; + /** Whether to sort the result list, by score. */ + shouldSort?: boolean; + /** The function to use to sort all the results. */ + sortFn?: FuseSortFunction; + /** At what point does the match algorithm give up. */ + threshold?: number; + /** When `true`, it enables the use of unix-like search commands. */ + useExtendedSearch?: boolean; + /** When `true`, enables token search with TF-IDF scoring. */ + useTokenSearch?: boolean; + /** + * Tokenizer used by `useTokenSearch`, applied identically at index-build + * and query time. Accepts either a global `RegExp` or a function returning + * `string[]`. Defaults to `/[\p{L}\p{M}\p{N}_]+/gu`, which handles CJK, + * Cyrillic, Greek, Arabic, Hebrew, Devanagari, etc. out of the box. Use a + * function form (e.g. wrapping `Intl.Segmenter`) for word-segmentation in + * scripts without whitespace boundaries. + */ + tokenize?: RegExp | FuseTokenizeFunction; + /** + * How the words of a multi-word query combine, for `useTokenSearch` only. + * `'any'` (default) returns a record if it matches any query word (OR); + * `'all'` returns it only when every query word matches somewhere in the + * record — any field or array element (AND). Use `'all'` for filtering, + * where adding a word should narrow the results. Has no effect unless + * `useTokenSearch` is `true`. + */ + tokenMatch?: 'all' | 'any'; } interface FuseSearchOptions { - limit?: number; + limit?: number; } interface FuseResultMatch { - indices: ReadonlyArray; - key?: string; - refIndex?: number; - value?: string; + indices: ReadonlyArray; + key?: string; + refIndex?: number; + value?: string; } interface FuseResult { - item: T; - refIndex: number; - score?: number; - matches?: ReadonlyArray; + item: T; + refIndex: number; + score?: number; + matches?: ReadonlyArray; } type Expression = string | { - [key: string]: string; + [key: string]: string; } | { - $path: ReadonlyArray; - $val: string; + $path: ReadonlyArray; + $val: string; } | { - $and?: Expression[]; + $and?: Expression[]; } | { - $or?: Expression[]; + $or?: Expression[]; }; - +//#endregion +//#region src/workers/FuseWorker.d.ts interface FuseWorkerOptions { - /** Number of parallel workers. Defaults to navigator.hardwareConcurrency (max 8). */ - numWorkers?: number; - /** Custom URL to the worker script. If not provided, resolves automatically via import.meta.url. */ - workerUrl?: string | URL; + /** Number of parallel workers. Defaults to navigator.hardwareConcurrency (max 8). */ + numWorkers?: number; + /** Custom URL to the worker script. If not provided, resolves automatically via import.meta.url. */ + workerUrl?: string | URL; } declare class FuseWorker { - private _options; - private _workerOptions; - private _docs; - private _shards; - private _addCursor; - private _initPromise; - private _pending; - private _nextId; - private _workerUrl; - constructor(docs: ReadonlyArray, options?: IFuseOptions, workerOptions?: FuseWorkerOptions); - private static _assertNoFunctionOptions; - private _getNumWorkers; - private _ensureInit; - private _spawnWorker; - private _workerInitOptions; - private _init; - private _call; - search(query: string | Expression, options?: FuseSearchOptions): Promise[]>; - add(doc: T): Promise; - setCollection(docs: ReadonlyArray): Promise; - terminate(): void; + private _options; + private _workerOptions; + private _docs; + private _shards; + private _addCursor; + private _initPromise; + private _pending; + private _nextId; + private _workerUrl; + constructor(docs: ReadonlyArray, options?: IFuseOptions, workerOptions?: FuseWorkerOptions); + private static _assertNoFunctionOptions; + private _getNumWorkers; + private _ensureInit; + private _spawnWorker; + private _workerInitOptions; + private _init; + private _call; + search(query: string | Expression, options?: FuseSearchOptions): Promise[]>; + add(doc: T): Promise; + setCollection(docs: ReadonlyArray): Promise; + terminate(): void; } - -export { FuseWorker }; -export type { FuseWorkerOptions }; +//#endregion +export { FuseWorker, type FuseWorkerOptions }; \ No newline at end of file diff --git a/dist/fuse-worker.mjs b/dist/fuse-worker.mjs index 5be4cc041..5b541152a 100644 --- a/dist/fuse-worker.mjs +++ b/dist/fuse-worker.mjs @@ -6,249 +6,196 @@ * * http://www.apache.org/licenses/LICENSE-2.0 */ +//#region src/core/errorMessages.ts +const FUSE_WORKER_UNSUPPORTED_FN_OPTION = (option) => `FuseWorker does not support function-valued option '${option}': functions cannot be transferred to Web Workers via postMessage. Remove this option or fall back to Fuse.`; +const FUSE_WORKER_TOKEN_SEARCH_UNSUPPORTED = "FuseWorker does not support useTokenSearch: token search depends on corpus-level statistics (df, fieldCount) that are computed per shard, so per-shard scores would diverge from single-thread Fuse. Use Fuse on the main thread for token search."; -const FUSE_WORKER_UNSUPPORTED_FN_OPTION = option => `FuseWorker does not support function-valued option '${option}': ` + `functions cannot be transferred to Web Workers via postMessage. ` + `Remove this option or fall back to Fuse.`; -const FUSE_WORKER_TOKEN_SEARCH_UNSUPPORTED = `FuseWorker does not support useTokenSearch: token search depends on ` + `corpus-level statistics (df, fieldCount) that are computed per shard, ` + `so per-shard scores would diverge from single-thread Fuse. Use Fuse on ` + `the main thread for token search.`; - -/// - +//#endregion +//#region src/workers/FuseWorker.ts const DEFAULT_MAX_WORKERS = 8; function getDefaultWorkerCount() { - const hw = typeof navigator !== 'undefined' ? navigator.hardwareConcurrency || 4 : 4; - return Math.min(hw, DEFAULT_MAX_WORKERS); + const hw = typeof navigator !== "undefined" ? navigator.hardwareConcurrency || 4 : 4; + return Math.min(hw, DEFAULT_MAX_WORKERS); } -class FuseWorker { - _shards = null; - _addCursor = 0; - _initPromise = null; - _pending = new Map(); - _nextId = 0; - constructor(docs, options, workerOptions) { - this._docs = docs.slice(); - this._options = options || {}; - this._workerOptions = workerOptions || {}; - // Reject function-valued options eagerly. Without this check, postMessage - // throws DataCloneError on first search() rather than at construction. - FuseWorker._assertNoFunctionOptions(this._options); - // Token search needs global corpus statistics, but each shard would build - // its own — scores would diverge from single-thread Fuse. Refuse upfront - // rather than silently returning ordering that doesn't match. - if (this._options.useTokenSearch) { - throw new Error(FUSE_WORKER_TOKEN_SEARCH_UNSUPPORTED); - } - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore -- import.meta.url is resolved by Rollup at build time - this._workerUrl = this._workerOptions.workerUrl || new URL('./fuse.worker.mjs', import.meta.url); - } - static _assertNoFunctionOptions(options) { - if (typeof options.sortFn === 'function') { - throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION('sortFn')); - } - if (typeof options.getFn === 'function') { - throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION('getFn')); - } - if (typeof options.tokenize === 'function') { - throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION('tokenize')); - } - const keys = options.keys; - if (Array.isArray(keys)) { - for (let i = 0, len = keys.length; i < len; i += 1) { - const key = keys[i]; - if (key && typeof key === 'object' && !Array.isArray(key)) { - if (typeof key.getFn === 'function') { - const name = key.name; - const label = Array.isArray(name) ? name.join('.') : name ?? String(i); - throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION(`keys[${label}].getFn`)); - } - } - } - } - } - _getNumWorkers() { - return this._workerOptions.numWorkers || getDefaultWorkerCount(); - } - _ensureInit() { - if (this._initPromise) return this._initPromise; - this._initPromise = this._init(); - return this._initPromise; - } - _spawnWorker() { - const worker = new Worker(this._workerUrl, { - type: 'module' - }); - worker.onmessage = e => { - const { - id, - result, - error - } = e.data; - const handler = this._pending.get(id); - if (!handler) return; - this._pending.delete(id); - if (error) { - handler.reject(new Error(error)); - } else { - handler.resolve(result); - } - }; - worker.onerror = e => { - for (const [, handler] of this._pending) { - handler.reject(new Error(e.message)); - } - }; - return worker; - } - _workerInitOptions() { - // Force includeScore so the main thread can do a global (score, idx) - // tie-break across shards. Score is stripped from the final result if the - // caller didn't ask for it. - return { - ...this._options, - includeScore: true - }; - } - async _init() { - const numWorkers = this._getNumWorkers(); - const chunkSize = Math.ceil(this._docs.length / numWorkers); - this._shards = []; - this._addCursor = 0; - const workerInitOptions = this._workerInitOptions(); - const initPromises = []; - for (let i = 0; i < numWorkers; i++) { - const start = i * chunkSize; - const end = Math.min(start + chunkSize, this._docs.length); - const chunk = this._docs.slice(start, end); - const globalIndices = []; - for (let j = start; j < end; j += 1) globalIndices.push(j); - const shard = { - worker: this._spawnWorker(), - globalIndices - }; - this._shards.push(shard); - initPromises.push(this._call(shard.worker, 'init', [chunk, workerInitOptions])); - } - await Promise.all(initPromises); - } - _call(worker, method, args) { - const id = this._nextId++; - return new Promise((resolve, reject) => { - this._pending.set(id, { - resolve, - reject - }); - worker.postMessage({ - id, - method, - args - }); - }); - } - async search(query, options) { - await this._ensureInit(); - const shards = this._shards; - const results = await Promise.all(shards.map(s => this._call(s.worker, 'search', [query, options]))); - - // Merge results from all shards, rewriting refIndex from shard-local to global - const merged = []; - for (let i = 0, len = results.length; i < len; i += 1) { - const { - globalIndices - } = shards[i]; - for (const r of results[i]) { - merged.push({ - ...r, - refIndex: globalIndices[r.refIndex] - }); - } - } - const shouldSort = this._options.shouldSort !== false; - if (shouldSort) { - // Mirror Fuse's default sortFn: (score, idx) tie-break, but on the - // GLOBAL refIndex so equal-score results across shards collapse into a - // deterministic order matching single-thread Fuse. - merged.sort((a, b) => { - const sa = a.score ?? 0; - const sb = b.score ?? 0; - if (sa === sb) { - return a.refIndex < b.refIndex ? -1 : 1; - } - return sa < sb ? -1 : 1; - }); - } else { - // Restore global collection order. Round-robin add() and - // shard-concatenation order otherwise leak through. - merged.sort((a, b) => a.refIndex - b.refIndex); - } - - // Workers always include score so the merge above can tie-break; strip it - // here if the caller didn't ask for it. Rebuild the object instead of - // `delete`-ing — `delete` deopts the shape and roughly doubles the - // post-sort cost on large result sets. - if (!this._options.includeScore) { - for (let i = 0, len = merged.length; i < len; i += 1) { - const r = merged[i]; - merged[i] = r.matches !== undefined ? { - item: r.item, - refIndex: r.refIndex, - matches: r.matches - } : { - item: r.item, - refIndex: r.refIndex - }; - } - } - const limit = options?.limit; - if (limit && limit > 0) { - return merged.slice(0, limit); - } - return merged; - } - async add(doc) { - await this._ensureInit(); - const shards = this._shards; - const shard = shards[this._addCursor % shards.length]; - this._addCursor += 1; - const globalIdx = this._docs.length; - this._docs.push(doc); - shard.globalIndices.push(globalIdx); - await this._call(shard.worker, 'add', [doc]); - } - async setCollection(docs) { - this._docs = docs.slice(); - if (!this._shards) { - this._initPromise = null; - return; - } - const shards = this._shards; - const chunkSize = Math.ceil(this._docs.length / shards.length); - this._addCursor = 0; - const tasks = []; - for (let i = 0, len = shards.length; i < len; i += 1) { - const start = i * chunkSize; - const end = Math.min(start + chunkSize, this._docs.length); - const chunk = this._docs.slice(start, end); - const globalIndices = []; - for (let j = start; j < end; j += 1) globalIndices.push(j); - shards[i].globalIndices = globalIndices; - tasks.push(this._call(shards[i].worker, 'setCollection', [chunk])); - } - await Promise.all(tasks); - } - terminate() { - if (this._shards) { - for (const { - worker - } of this._shards) { - worker.terminate(); - } - this._shards = null; - } - this._initPromise = null; - for (const [, handler] of this._pending) { - handler.reject(new Error('FuseWorker terminated')); - } - this._pending.clear(); - } +const browserWorkerBase = typeof document !== "undefined" ? document.currentScript?.src || new URL("fuse-worker.cjs", document.baseURI).href : void 0; +function resolveDefaultWorkerUrl() { + try { + return new URL("./fuse.worker.mjs", import.meta.url); + } catch { + if (browserWorkerBase !== void 0) return new URL("./fuse.worker.mjs", browserWorkerBase); + throw new Error("FuseWorker: unable to resolve the worker script URL automatically; pass workerOptions.workerUrl explicitly."); + } } +var FuseWorker = class FuseWorker { + constructor(docs, options, workerOptions) { + this._shards = null; + this._addCursor = 0; + this._initPromise = null; + this._pending = /* @__PURE__ */ new Map(); + this._nextId = 0; + this._docs = docs.slice(); + this._options = options || {}; + this._workerOptions = workerOptions || {}; + FuseWorker._assertNoFunctionOptions(this._options); + if (this._options.useTokenSearch) throw new Error(FUSE_WORKER_TOKEN_SEARCH_UNSUPPORTED); + this._workerUrl = this._workerOptions.workerUrl || resolveDefaultWorkerUrl(); + } + static _assertNoFunctionOptions(options) { + if (typeof options.sortFn === "function") throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION("sortFn")); + if (typeof options.getFn === "function") throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION("getFn")); + if (typeof options.tokenize === "function") throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION("tokenize")); + const keys = options.keys; + if (Array.isArray(keys)) for (let i = 0, len = keys.length; i < len; i += 1) { + const key = keys[i]; + if (key && typeof key === "object" && !Array.isArray(key)) { + if (typeof key.getFn === "function") { + const name = key.name; + const label = Array.isArray(name) ? name.join(".") : name ?? String(i); + throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION(`keys[${label}].getFn`)); + } + } + } + } + _getNumWorkers() { + return this._workerOptions.numWorkers || getDefaultWorkerCount(); + } + _ensureInit() { + if (this._initPromise) return this._initPromise; + this._initPromise = this._init(); + return this._initPromise; + } + _spawnWorker() { + const worker = new Worker(this._workerUrl, { type: "module" }); + worker.onmessage = (e) => { + const { id, result, error } = e.data; + const handler = this._pending.get(id); + if (!handler) return; + this._pending.delete(id); + if (error) handler.reject(new Error(error)); + else handler.resolve(result); + }; + worker.onerror = (e) => { + for (const [, handler] of this._pending) handler.reject(new Error(e.message)); + }; + return worker; + } + _workerInitOptions() { + return { + ...this._options, + includeScore: true + }; + } + async _init() { + const numWorkers = this._getNumWorkers(); + const chunkSize = Math.ceil(this._docs.length / numWorkers); + this._shards = []; + this._addCursor = 0; + const workerInitOptions = this._workerInitOptions(); + const initPromises = []; + for (let i = 0; i < numWorkers; i++) { + const start = i * chunkSize; + const end = Math.min(start + chunkSize, this._docs.length); + const chunk = this._docs.slice(start, end); + const globalIndices = []; + for (let j = start; j < end; j += 1) globalIndices.push(j); + const shard = { + worker: this._spawnWorker(), + globalIndices + }; + this._shards.push(shard); + initPromises.push(this._call(shard.worker, "init", [chunk, workerInitOptions])); + } + await Promise.all(initPromises); + } + _call(worker, method, args) { + const id = this._nextId++; + return new Promise((resolve, reject) => { + this._pending.set(id, { + resolve, + reject + }); + worker.postMessage({ + id, + method, + args + }); + }); + } + async search(query, options) { + await this._ensureInit(); + const shards = this._shards; + const results = await Promise.all(shards.map((s) => this._call(s.worker, "search", [query, options]))); + const merged = []; + for (let i = 0, len = results.length; i < len; i += 1) { + const { globalIndices } = shards[i]; + for (const r of results[i]) merged.push({ + ...r, + refIndex: globalIndices[r.refIndex] + }); + } + if (this._options.shouldSort !== false) merged.sort((a, b) => { + const sa = a.score ?? 0; + const sb = b.score ?? 0; + if (sa === sb) return a.refIndex < b.refIndex ? -1 : 1; + return sa < sb ? -1 : 1; + }); + else merged.sort((a, b) => a.refIndex - b.refIndex); + if (!this._options.includeScore) for (let i = 0, len = merged.length; i < len; i += 1) { + const r = merged[i]; + merged[i] = r.matches !== void 0 ? { + item: r.item, + refIndex: r.refIndex, + matches: r.matches + } : { + item: r.item, + refIndex: r.refIndex + }; + } + const limit = options?.limit; + if (limit && limit > 0) return merged.slice(0, limit); + return merged; + } + async add(doc) { + await this._ensureInit(); + const shards = this._shards; + const shard = shards[this._addCursor % shards.length]; + this._addCursor += 1; + const globalIdx = this._docs.length; + this._docs.push(doc); + shard.globalIndices.push(globalIdx); + await this._call(shard.worker, "add", [doc]); + } + async setCollection(docs) { + this._docs = docs.slice(); + if (!this._shards) { + this._initPromise = null; + return; + } + const shards = this._shards; + const chunkSize = Math.ceil(this._docs.length / shards.length); + this._addCursor = 0; + const tasks = []; + for (let i = 0, len = shards.length; i < len; i += 1) { + const start = i * chunkSize; + const end = Math.min(start + chunkSize, this._docs.length); + const chunk = this._docs.slice(start, end); + const globalIndices = []; + for (let j = start; j < end; j += 1) globalIndices.push(j); + shards[i].globalIndices = globalIndices; + tasks.push(this._call(shards[i].worker, "setCollection", [chunk])); + } + await Promise.all(tasks); + } + terminate() { + if (this._shards) { + for (const { worker } of this._shards) worker.terminate(); + this._shards = null; + } + this._initPromise = null; + for (const [, handler] of this._pending) handler.reject(/* @__PURE__ */ new Error("FuseWorker terminated")); + this._pending.clear(); + } +}; -export { FuseWorker }; +//#endregion +export { FuseWorker }; \ No newline at end of file diff --git a/dist/fuse.basic.cjs b/dist/fuse.basic.cjs index 895f0d995..5bcdfc96c 100644 --- a/dist/fuse.basic.cjs +++ b/dist/fuse.basic.cjs @@ -7,1677 +7,1178 @@ * http://www.apache.org/licenses/LICENSE-2.0 */ -'use strict'; - +//#region src/helpers/typeGuards.ts function isArray(value) { - return !Array.isArray ? getTag(value) === '[object Array]' : Array.isArray(value); + return !Array.isArray ? getTag(value) === "[object Array]" : Array.isArray(value); } function baseToString(value) { - // Exit early for strings to avoid a performance hit in some environments. - if (typeof value == 'string') { - return value; - } - if (typeof value === 'bigint') { - return value.toString(); - } - const result = value + ''; - return result == '0' && 1 / value == -Infinity ? '-0' : result; + if (typeof value == "string") return value; + if (typeof value === "bigint") return value.toString(); + const result = value + ""; + return result == "0" && 1 / value == -Infinity ? "-0" : result; } function toString(value) { - return value == null ? '' : baseToString(value); + return value == null ? "" : baseToString(value); } function isString(value) { - return typeof value === 'string'; + return typeof value === "string"; } function isNumber(value) { - return typeof value === 'number'; + return typeof value === "number"; } - -// Adapted from: https://github.com/lodash/lodash/blob/master/isBoolean.js function isBoolean(value) { - return value === true || value === false || isObjectLike(value) && getTag(value) == '[object Boolean]'; + return value === true || value === false || isObjectLike(value) && getTag(value) == "[object Boolean]"; } function isObject(value) { - return typeof value === 'object'; + return typeof value === "object"; } - -// Checks if `value` is object-like. function isObjectLike(value) { - return isObject(value) && value !== null; + return isObject(value) && value !== null; } function isDefined(value) { - return value !== undefined && value !== null; + return value !== void 0 && value !== null; } function isBlank(value) { - return !value.trim().length; + return !value.trim().length; } - -// Gets the `toStringTag` of `value`. -// Adapted from: https://github.com/lodash/lodash/blob/master/.internal/getTag.js function getTag(value) { - return value == null ? value === undefined ? '[object Undefined]' : '[object Null]' : Object.prototype.toString.call(value); + return value == null ? value === void 0 ? "[object Undefined]" : "[object Null]" : Object.prototype.toString.call(value); } -const EXTENDED_SEARCH_UNAVAILABLE = 'Extended search is not available'; -const LOGICAL_SEARCH_UNAVAILABLE = 'Logical search is not available'; -const TOKEN_SEARCH_UNAVAILABLE = 'Token search is not available'; +//#endregion +//#region src/core/errorMessages.ts +const EXTENDED_SEARCH_UNAVAILABLE = "Extended search is not available"; +const LOGICAL_SEARCH_UNAVAILABLE = "Logical search is not available"; +const TOKEN_SEARCH_UNAVAILABLE = "Token search is not available"; const INCORRECT_INDEX_TYPE = "Incorrect 'index' type"; -const INVALID_DOC_INDEX = 'Invalid doc index: must be a non-negative integer within the bounds of the docs array'; -const LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY = key => `Invalid value for key ${key}`; -const PATTERN_LENGTH_TOO_LARGE = max => `Pattern length exceeds max of ${max}.`; -const MISSING_KEY_PROPERTY = name => `Missing ${name} property in key`; -const INVALID_KEY_WEIGHT_VALUE = key => `Property 'weight' in key '${key}' must be a positive integer`; -const FUSE_MATCH_TOKEN_SEARCH_UNSUPPORTED = `Fuse.match does not support useTokenSearch: token search requires ` + `corpus-level statistics (df, fieldCount) that a one-off string ` + `comparison does not have. Use new Fuse(...).search(...) instead.`; - +const INVALID_DOC_INDEX = "Invalid doc index: must be a non-negative integer within the bounds of the docs array"; +const LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY = (key) => `Invalid value for key ${key}`; +const PATTERN_LENGTH_TOO_LARGE = (max) => `Pattern length exceeds max of ${max}.`; +const MISSING_KEY_PROPERTY = (name) => `Missing ${name} property in key`; +const INVALID_KEY_WEIGHT_VALUE = (key) => `Property 'weight' in key '${key}' must be a positive integer`; +const FUSE_MATCH_TOKEN_SEARCH_UNSUPPORTED = "Fuse.match does not support useTokenSearch: token search requires corpus-level statistics (df, fieldCount) that a one-off string comparison does not have. Use new Fuse(...).search(...) instead."; + +//#endregion +//#region src/tools/KeyStore.ts const hasOwn = Object.prototype.hasOwnProperty; -class KeyStore { - constructor(keys) { - this._keys = []; - this._keyMap = {}; - let totalWeight = 0; - keys.forEach(key => { - const obj = createKey(key); - this._keys.push(obj); - this._keyMap[obj.id] = obj; - totalWeight += obj.weight; - }); - - // Normalize weights so that their sum is equal to 1 - this._keys.forEach(key => { - key.weight /= totalWeight; - }); - } - get(keyId) { - return this._keyMap[keyId]; - } - keys() { - return this._keys; - } - toJSON() { - return JSON.stringify(this._keys); - } -} +var KeyStore = class { + constructor(keys) { + this._keys = []; + this._keyMap = {}; + let totalWeight = 0; + keys.forEach((key) => { + const obj = createKey(key); + this._keys.push(obj); + this._keyMap[obj.id] = obj; + totalWeight += obj.weight; + }); + this._keys.forEach((key) => { + key.weight /= totalWeight; + }); + } + get(keyId) { + return this._keyMap[keyId]; + } + keys() { + return this._keys; + } + toJSON() { + return JSON.stringify(this._keys); + } +}; function createKey(key) { - let path = null; - let id = null; - let src = null; - let weight = 1; - let getFn = null; - if (isString(key) || isArray(key)) { - src = key; - path = createKeyPath(key); - id = createKeyId(key); - } else { - if (!hasOwn.call(key, 'name')) { - throw new Error(MISSING_KEY_PROPERTY('name')); - } - const name = key.name; - src = name; - if (hasOwn.call(key, 'weight') && key.weight !== undefined) { - weight = key.weight; - if (weight <= 0) { - throw new Error(INVALID_KEY_WEIGHT_VALUE(createKeyId(name))); - } - } - path = createKeyPath(name); - id = createKeyId(name); - getFn = key.getFn ?? null; - } - return { - path: path, - id: id, - weight, - src: src, - getFn - }; + let path = null; + let id = null; + let src = null; + let weight = 1; + let getFn = null; + if (isString(key) || isArray(key)) { + src = key; + path = createKeyPath(key); + id = createKeyId(key); + } else { + if (!hasOwn.call(key, "name")) throw new Error(MISSING_KEY_PROPERTY("name")); + const name = key.name; + src = name; + if (hasOwn.call(key, "weight") && key.weight !== void 0) { + weight = key.weight; + if (weight <= 0) throw new Error(INVALID_KEY_WEIGHT_VALUE(createKeyId(name))); + } + path = createKeyPath(name); + id = createKeyId(name); + getFn = key.getFn ?? null; + } + return { + path, + id, + weight, + src, + getFn + }; } function createKeyPath(key) { - return isArray(key) ? key : key.split('.'); + return isArray(key) ? key : key.split("."); } function createKeyId(key) { - return isArray(key) ? key.join('.') : key; + return isArray(key) ? key.join(".") : key; } +//#endregion +//#region src/helpers/get.ts function get(obj, path) { - const list = []; - let arr = false; - const deepGet = (obj, path, index, arrayIndex) => { - if (!isDefined(obj)) { - return; - } - if (!path[index]) { - // If there's no path left, we've arrived at the object we care about. - list.push(arrayIndex !== undefined ? { - v: obj, - i: arrayIndex - } : obj); - } else { - const key = path[index]; - const value = obj[key]; - if (!isDefined(value)) { - return; - } - - // If we're at the last value in the path, and if it's a string/number/bool, - // add it to the list - if (index === path.length - 1 && (isString(value) || isNumber(value) || isBoolean(value) || typeof value === 'bigint')) { - list.push(arrayIndex !== undefined ? { - v: toString(value), - i: arrayIndex - } : toString(value)); - } else if (isArray(value)) { - arr = true; - // Search each item in the array. - for (let i = 0, len = value.length; i < len; i += 1) { - deepGet(value[i], path, index + 1, i); - } - } else if (path.length) { - // An object. Recurse further. - deepGet(value, path, index + 1, arrayIndex); - } - } - }; - - // Backwards compatibility (since path used to be a string) - deepGet(obj, isString(path) ? path.split('.') : path, 0); - return arr ? list : list[0]; + const list = []; + let arr = false; + const deepGet = (obj, path, index, arrayIndex) => { + if (!isDefined(obj)) return; + if (!path[index]) list.push(arrayIndex !== void 0 ? { + v: obj, + i: arrayIndex + } : obj); + else { + const value = obj[path[index]]; + if (!isDefined(value)) return; + if (index === path.length - 1 && (isString(value) || isNumber(value) || isBoolean(value) || typeof value === "bigint")) list.push(arrayIndex !== void 0 ? { + v: toString(value), + i: arrayIndex + } : toString(value)); + else if (isArray(value)) { + arr = true; + for (let i = 0, len = value.length; i < len; i += 1) deepGet(value[i], path, index + 1, i); + } else if (path.length) deepGet(value, path, index + 1, arrayIndex); + } + }; + deepGet(obj, isString(path) ? path.split(".") : path, 0); + return arr ? list : list[0]; } +//#endregion +//#region src/core/config.ts const MatchOptions = { - includeMatches: false, - findAllMatches: false, - minMatchCharLength: 1 + includeMatches: false, + findAllMatches: false, + minMatchCharLength: 1 }; const BasicOptions = { - isCaseSensitive: false, - ignoreDiacritics: false, - includeScore: false, - keys: [], - shouldSort: true, - sortFn: (a, b) => a.score === b.score ? a.idx < b.idx ? -1 : 1 : a.score < b.score ? -1 : 1 + isCaseSensitive: false, + ignoreDiacritics: false, + includeScore: false, + keys: [], + shouldSort: true, + sortFn: (a, b) => a.score === b.score ? a.idx < b.idx ? -1 : 1 : a.score < b.score ? -1 : 1 }; const FuzzyOptions = { - location: 0, - threshold: 0.6, - distance: 100 + location: 0, + threshold: .6, + distance: 100 }; const AdvancedOptions = { - useExtendedSearch: false, - useTokenSearch: false, - tokenize: undefined, - tokenMatch: 'any', - getFn: get, - ignoreLocation: false, - ignoreFieldNorm: false, - fieldNormWeight: 1 + useExtendedSearch: false, + useTokenSearch: false, + tokenize: void 0, + tokenMatch: "any", + getFn: get, + ignoreLocation: false, + ignoreFieldNorm: false, + fieldNormWeight: 1 }; const Config = Object.freeze({ - ...BasicOptions, - ...MatchOptions, - ...FuzzyOptions, - ...AdvancedOptions + ...BasicOptions, + ...MatchOptions, + ...FuzzyOptions, + ...AdvancedOptions }); -// Field-length norm: the shorter the field, the higher the weight. -// Set to 3 decimals to reduce index size. +//#endregion +//#region src/tools/fieldNorm.ts function norm(weight = 1, mantissa = 3) { - const cache = new Map(); - const m = Math.pow(10, mantissa); - return { - get(value) { - // Count words by counting space transitions — avoids allocating a regex match array - let numTokens = 1; - let inSpace = false; - for (let i = 0; i < value.length; i++) { - if (value.charCodeAt(i) === 32) { - if (!inSpace) { - numTokens++; - inSpace = true; - } - } else { - inSpace = false; - } - } - if (cache.has(numTokens)) { - return cache.get(numTokens); - } - - // Default function is 1/sqrt(x), weight makes that variable - const n = Math.round(m / Math.pow(numTokens, 0.5 * weight)) / m; - cache.set(numTokens, n); - return n; - }, - clear() { - cache.clear(); - } - }; + const cache = /* @__PURE__ */ new Map(); + const m = Math.pow(10, mantissa); + return { + get(value) { + let numTokens = 1; + let inSpace = false; + for (let i = 0; i < value.length; i++) if (value.charCodeAt(i) === 32) { + if (!inSpace) { + numTokens++; + inSpace = true; + } + } else inSpace = false; + if (cache.has(numTokens)) return cache.get(numTokens); + const n = Math.round(m / Math.pow(numTokens, .5 * weight)) / m; + cache.set(numTokens, n); + return n; + }, + clear() { + cache.clear(); + } + }; } -class FuseIndex { - constructor({ - getFn = Config.getFn, - fieldNormWeight = Config.fieldNormWeight - } = {}) { - this.norm = norm(fieldNormWeight, 3); - this.getFn = getFn; - this.isCreated = false; - this.docs = []; - this.keys = []; - this._keysMap = {}; - this.setIndexRecords(); - } - setSources(docs = []) { - this.docs = docs; - } - setIndexRecords(records = []) { - this.records = records; - } - setKeys(keys = []) { - this.keys = keys; - this._keysMap = {}; - keys.forEach((key, idx) => { - this._keysMap[key.id] = idx; - }); - } - create() { - if (this.isCreated || !this.docs.length) { - return; - } - this.isCreated = true; - const len = this.docs.length; - this.records = new Array(len); - let recordCount = 0; - - // List is Array - if (isString(this.docs[0])) { - for (let i = 0; i < len; i++) { - const record = this._createStringRecord(this.docs[i], i); - if (record) { - this.records[recordCount++] = record; - } - } - } else { - // List is Array - for (let i = 0; i < len; i++) { - this.records[recordCount++] = this._createObjectRecord(this.docs[i], i); - } - } - this.records.length = recordCount; - this.norm.clear(); - } - // Appends a record for `doc` at `docIndex` (the doc's position in the source - // array). Returns the appended record, or null when `doc` is a blank string - // (those are skipped at record creation; see `_createStringRecord`). Callers - // use the return value to gate downstream bookkeeping like the inverted - // index, which must not be touched when no record was produced. - add(doc, docIndex) { - if (!Number.isInteger(docIndex) || docIndex < 0) { - throw new Error(INVALID_DOC_INDEX); - } - if (isString(doc)) { - const record = this._createStringRecord(doc, docIndex); - if (record) { - this.records.push(record); - } - return record; - } - const record = this._createObjectRecord(doc, docIndex); - this.records.push(record); - return record; - } - // Removes the record for the doc at the specified source-array (docs) index. - // Blank string docs have no record; callers may pass such an index and the - // splice is a no-op, but subsequent records still need their .i decremented - // to track the docs array that the caller is splicing in parallel. - removeAt(idx) { - if (!Number.isInteger(idx) || idx < 0) { - throw new Error(INVALID_DOC_INDEX); - } - - // Find and remove the record at this doc-index, if one exists. Records are - // typically sorted by .i but the algorithm doesn't depend on it — parsed - // indexes via setIndexRecords may arrive in arbitrary order. - for (let i = 0, len = this.records.length; i < len; i += 1) { - if (this.records[i].i === idx) { - this.records.splice(i, 1); - break; - } - } - - // Decrement every record whose source-array index is now stale. - for (let i = 0, len = this.records.length; i < len; i += 1) { - if (this.records[i].i > idx) { - this.records[i].i -= 1; - } - } - } - // Removes records for the docs at the specified source-array indices, then - // shifts every surviving record's .i down by the count of removed indices - // strictly less than it (mirrors removeAndShiftInvertedIndex's shift math). - // Invalid entries (non-integer, negative) in `indices` are dropped silently - // — removeAll's natural use case is "caller passed a list of matched doc - // indices"; asymmetric throw-vs-no-op would be more surprising than a clean - // filter. - removeAll(indices) { - const toRemove = new Set(); - for (const v of indices) { - if (Number.isInteger(v) && v >= 0) { - toRemove.add(v); - } - } - if (toRemove.size === 0) { - return; - } - this.records = this.records.filter(r => !toRemove.has(r.i)); - const sorted = Array.from(toRemove).sort((a, b) => a - b); - for (const record of this.records) { - // shift = count of removed indices strictly less than record.i - let lo = 0; - let hi = sorted.length; - while (lo < hi) { - const mid = lo + hi >>> 1; - if (sorted[mid] < record.i) lo = mid + 1;else hi = mid; - } - record.i -= lo; - } - } - getValueForItemAtKeyId(item, keyId) { - return item[this._keysMap[keyId]]; - } - size() { - return this.records.length; - } - _createStringRecord(doc, docIndex) { - if (!isDefined(doc) || isBlank(doc)) { - return null; - } - return { - v: doc, - i: docIndex, - n: this.norm.get(doc) - }; - } - _createObjectRecord(doc, docIndex) { - const record = { - i: docIndex, - $: {} - }; - - // Iterate over every key (i.e, path), and fetch the value at that key - for (let keyIndex = 0, keyLen = this.keys.length; keyIndex < keyLen; keyIndex++) { - const key = this.keys[keyIndex]; - const value = key.getFn ? key.getFn(doc) : this.getFn(doc, key.path); - if (!isDefined(value)) { - continue; - } - if (isArray(value)) { - const subRecords = []; - for (let i = 0, len = value.length; i < len; i += 1) { - const item = value[i]; - if (!isDefined(item)) { - continue; - } - if (isString(item)) { - // Custom getFn returning plain string array (backward compat) - if (!isBlank(item)) { - const subRecord = { - v: item, - i: i, - n: this.norm.get(item) - }; - subRecords.push(subRecord); - } - } else if (isDefined(item.v)) { - // Default get() returns {v, i} objects with original array indices - const text = isString(item.v) ? item.v : toString(item.v); - if (!isBlank(text)) { - const subRecord = { - v: text, - i: item.i, - n: this.norm.get(text) - }; - subRecords.push(subRecord); - } - } - } - record.$[keyIndex] = subRecords; - } else if (isString(value) && !isBlank(value)) { - const subRecord = { - v: value, - n: this.norm.get(value) - }; - record.$[keyIndex] = subRecord; - } - } - return record; - } - toJSON() { - return { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - keys: this.keys.map(({ - getFn, - ...key - }) => key), - records: this.records - }; - } -} -function createIndex(keys, docs, { - getFn = Config.getFn, - fieldNormWeight = Config.fieldNormWeight -} = {}) { - const myIndex = new FuseIndex({ - getFn, - fieldNormWeight - }); - myIndex.setKeys(keys.map(createKey)); - myIndex.setSources(docs); - myIndex.create(); - return myIndex; +//#endregion +//#region src/tools/FuseIndex.ts +var FuseIndex = class { + constructor({ getFn = Config.getFn, fieldNormWeight = Config.fieldNormWeight } = {}) { + this.norm = norm(fieldNormWeight, 3); + this.getFn = getFn; + this.isCreated = false; + this.docs = []; + this.keys = []; + this._keysMap = {}; + this.setIndexRecords(); + } + setSources(docs = []) { + this.docs = docs; + } + setIndexRecords(records = []) { + this.records = records; + } + setKeys(keys = []) { + this.keys = keys; + this._keysMap = {}; + keys.forEach((key, idx) => { + this._keysMap[key.id] = idx; + }); + } + create() { + if (this.isCreated || !this.docs.length) return; + this.isCreated = true; + const len = this.docs.length; + this.records = new Array(len); + let recordCount = 0; + if (isString(this.docs[0])) for (let i = 0; i < len; i++) { + const record = this._createStringRecord(this.docs[i], i); + if (record) this.records[recordCount++] = record; + } + else for (let i = 0; i < len; i++) this.records[recordCount++] = this._createObjectRecord(this.docs[i], i); + this.records.length = recordCount; + this.norm.clear(); + } + add(doc, docIndex) { + if (!Number.isInteger(docIndex) || docIndex < 0) throw new Error(INVALID_DOC_INDEX); + if (isString(doc)) { + const record = this._createStringRecord(doc, docIndex); + if (record) this.records.push(record); + return record; + } + const record = this._createObjectRecord(doc, docIndex); + this.records.push(record); + return record; + } + removeAt(idx) { + if (!Number.isInteger(idx) || idx < 0) throw new Error(INVALID_DOC_INDEX); + for (let i = 0, len = this.records.length; i < len; i += 1) if (this.records[i].i === idx) { + this.records.splice(i, 1); + break; + } + for (let i = 0, len = this.records.length; i < len; i += 1) if (this.records[i].i > idx) this.records[i].i -= 1; + } + removeAll(indices) { + const toRemove = /* @__PURE__ */ new Set(); + for (const v of indices) if (Number.isInteger(v) && v >= 0) toRemove.add(v); + if (toRemove.size === 0) return; + this.records = this.records.filter((r) => !toRemove.has(r.i)); + const sorted = Array.from(toRemove).sort((a, b) => a - b); + for (const record of this.records) { + let lo = 0; + let hi = sorted.length; + while (lo < hi) { + const mid = lo + hi >>> 1; + if (sorted[mid] < record.i) lo = mid + 1; + else hi = mid; + } + record.i -= lo; + } + } + getValueForItemAtKeyId(item, keyId) { + return item[this._keysMap[keyId]]; + } + size() { + return this.records.length; + } + _createStringRecord(doc, docIndex) { + if (!isDefined(doc) || isBlank(doc)) return null; + return { + v: doc, + i: docIndex, + n: this.norm.get(doc) + }; + } + _createObjectRecord(doc, docIndex) { + const record = { + i: docIndex, + $: {} + }; + for (let keyIndex = 0, keyLen = this.keys.length; keyIndex < keyLen; keyIndex++) { + const key = this.keys[keyIndex]; + const value = key.getFn ? key.getFn(doc) : this.getFn(doc, key.path); + if (!isDefined(value)) continue; + if (isArray(value)) { + const subRecords = []; + for (let i = 0, len = value.length; i < len; i += 1) { + const item = value[i]; + if (!isDefined(item)) continue; + if (isString(item)) { + if (!isBlank(item)) { + const subRecord = { + v: item, + i, + n: this.norm.get(item) + }; + subRecords.push(subRecord); + } + } else if (isDefined(item.v)) { + const text = isString(item.v) ? item.v : toString(item.v); + if (!isBlank(text)) { + const subRecord = { + v: text, + i: item.i, + n: this.norm.get(text) + }; + subRecords.push(subRecord); + } + } + } + record.$[keyIndex] = subRecords; + } else if (isString(value) && !isBlank(value)) { + const subRecord = { + v: value, + n: this.norm.get(value) + }; + record.$[keyIndex] = subRecord; + } + } + return record; + } + toJSON() { + return { + keys: this.keys.map(({ getFn, ...key }) => key), + records: this.records + }; + } +}; +function createIndex(keys, docs, { getFn = Config.getFn, fieldNormWeight = Config.fieldNormWeight } = {}) { + const myIndex = new FuseIndex({ + getFn, + fieldNormWeight + }); + myIndex.setKeys(keys.map(createKey)); + myIndex.setSources(docs); + myIndex.create(); + return myIndex; } -function parseIndex(data, { - getFn = Config.getFn, - fieldNormWeight = Config.fieldNormWeight -} = {}) { - const { - keys, - records - } = data; - const myIndex = new FuseIndex({ - getFn, - fieldNormWeight - }); - myIndex.setKeys(keys); - myIndex.setIndexRecords(records); - return myIndex; +function parseIndex(data, { getFn = Config.getFn, fieldNormWeight = Config.fieldNormWeight } = {}) { + const { keys, records } = data; + const myIndex = new FuseIndex({ + getFn, + fieldNormWeight + }); + myIndex.setKeys(keys); + myIndex.setIndexRecords(records); + return myIndex; } +//#endregion +//#region src/search/bitap/convertMaskToIndices.ts function convertMaskToIndices(matchmask = [], minMatchCharLength = Config.minMatchCharLength) { - const indices = []; - let start = -1; - let end = -1; - let i = 0; - for (let len = matchmask.length; i < len; i += 1) { - const match = matchmask[i]; - if (match && start === -1) { - start = i; - } else if (!match && start !== -1) { - end = i - 1; - if (end - start + 1 >= minMatchCharLength) { - indices.push([start, end]); - } - start = -1; - } - } - - // (i-1 - start) + 1 => i - start - if (matchmask[i - 1] && i - start >= minMatchCharLength) { - indices.push([start, i - 1]); - } - return indices; + const indices = []; + let start = -1; + let end = -1; + let i = 0; + for (let len = matchmask.length; i < len; i += 1) { + const match = matchmask[i]; + if (match && start === -1) start = i; + else if (!match && start !== -1) { + end = i - 1; + if (end - start + 1 >= minMatchCharLength) indices.push([start, end]); + start = -1; + } + } + if (matchmask[i - 1] && i - start >= minMatchCharLength) indices.push([start, i - 1]); + return indices; } -// Machine word size -const MAX_BITS = 32; - -function search(text, pattern, patternAlphabet, { - location = Config.location, - distance = Config.distance, - threshold = Config.threshold, - findAllMatches = Config.findAllMatches, - minMatchCharLength = Config.minMatchCharLength, - includeMatches = Config.includeMatches, - ignoreLocation = Config.ignoreLocation -} = {}) { - if (pattern.length > MAX_BITS) { - throw new Error(PATTERN_LENGTH_TOO_LARGE(MAX_BITS)); - } - const patternLen = pattern.length; - // Set starting location at beginning text and initialize the alphabet. - const textLen = text.length; - // Handle the case when location > text.length - const expectedLocation = Math.max(0, Math.min(location, textLen)); - // Highest score beyond which we give up. - let currentThreshold = threshold; - // Is there a nearby exact match? (speedup) - let bestLocation = expectedLocation; - - // Inlined score computation — avoids object allocation per call in hot loops. - // See ./computeScore.ts for the documented version of this formula. - const calcScore = (errors, currentLocation) => { - const accuracy = errors / patternLen; - if (ignoreLocation) return accuracy; - const proximity = Math.abs(expectedLocation - currentLocation); - if (!distance) return proximity ? 1.0 : accuracy; - return accuracy + proximity / distance; - }; - - // Performance: only computer matches when the minMatchCharLength > 1 - // OR if `includeMatches` is true. - const computeMatches = minMatchCharLength > 1 || includeMatches; - // A mask of the matches, used for building the indices - const matchMask = computeMatches ? Array(textLen) : []; - let index; - - // Get all exact matches, here for speed up - while ((index = text.indexOf(pattern, bestLocation)) > -1) { - const score = calcScore(0, index); - currentThreshold = Math.min(score, currentThreshold); - bestLocation = index + patternLen; - if (computeMatches) { - let i = 0; - while (i < patternLen) { - matchMask[index + i] = 1; - i += 1; - } - } - } - - // Reset the best location - bestLocation = -1; - let lastBitArr = []; - let finalScore = 1; - let bestErrors = 0; - let binMax = patternLen + textLen; - const mask = 1 << patternLen - 1; - for (let i = 0; i < patternLen; i += 1) { - // Scan for the best match; each iteration allows for one more error. - // Run a binary search to determine how far from the match location we can stray - // at this error level. - let binMin = 0; - let binMid = binMax; - while (binMin < binMid) { - const score = calcScore(i, expectedLocation + binMid); - if (score <= currentThreshold) { - binMin = binMid; - } else { - binMax = binMid; - } - binMid = Math.floor((binMax - binMin) / 2 + binMin); - } - - // Use the result from this iteration as the maximum for the next. - binMax = binMid; - let start = Math.max(1, expectedLocation - binMid + 1); - const finish = findAllMatches ? textLen : Math.min(expectedLocation + binMid, textLen) + patternLen; - - // Initialize the bit array - const bitArr = Array(finish + 2); - bitArr[finish + 1] = (1 << i) - 1; - for (let j = finish; j >= start; j -= 1) { - const currentLocation = j - 1; - const charMatch = patternAlphabet[text[currentLocation]]; - - // First pass: exact match - bitArr[j] = (bitArr[j + 1] << 1 | 1) & charMatch; - - // Subsequent passes: fuzzy match - if (i) { - bitArr[j] |= (lastBitArr[j + 1] | lastBitArr[j]) << 1 | 1 | lastBitArr[j + 1]; - } - if (bitArr[j] & mask) { - finalScore = calcScore(i, currentLocation); - - // This match will almost certainly be better than any existing match. - // But check anyway. - if (finalScore <= currentThreshold) { - // Indeed it is - currentThreshold = finalScore; - bestLocation = currentLocation; - bestErrors = i; - - // Already passed `loc`, downhill from here on in. - if (bestLocation <= expectedLocation) { - break; - } - - // When passing `bestLocation`, don't exceed our current distance from `expectedLocation`. - start = Math.max(1, 2 * expectedLocation - bestLocation); - } - } - } - - // No hope for a (better) match at greater error levels. - const score = calcScore(i + 1, expectedLocation); - if (score > currentThreshold) { - break; - } - lastBitArr = bitArr; - } - - // Fill matchMask across the matched window only. Bitap anchors a match at - // bestLocation (the start), spanning patternLen characters plus up to - // bestErrors extra characters when errors are text-side insertions. Marking - // alphabet positions in that window keeps the highlight indices honest about - // what actually matched, instead of every pattern-alphabet character the - // scan happened to visit. - if (computeMatches && bestLocation >= 0) { - const matchEnd = Math.min(textLen - 1, bestLocation + patternLen - 1 + bestErrors); - for (let k = bestLocation; k <= matchEnd; k += 1) { - if (patternAlphabet[text[k]]) { - matchMask[k] = 1; - } - } - } - const result = { - isMatch: bestLocation >= 0, - // Count exact matches (those with a score of 0) to be "almost" exact - score: Math.max(0.001, finalScore) - }; - if (computeMatches) { - const indices = convertMaskToIndices(matchMask, minMatchCharLength); - if (!indices.length) { - result.isMatch = false; - } else if (includeMatches) { - result.indices = indices; - } - } - return result; +//#endregion +//#region src/search/bitap/search.ts +function search(text, pattern, patternAlphabet, { location = Config.location, distance = Config.distance, threshold = Config.threshold, findAllMatches = Config.findAllMatches, minMatchCharLength = Config.minMatchCharLength, includeMatches = Config.includeMatches, ignoreLocation = Config.ignoreLocation } = {}) { + if (pattern.length > 32) throw new Error(PATTERN_LENGTH_TOO_LARGE(32)); + const patternLen = pattern.length; + const textLen = text.length; + const expectedLocation = Math.max(0, Math.min(location, textLen)); + let currentThreshold = threshold; + let bestLocation = expectedLocation; + const calcScore = (errors, currentLocation) => { + const accuracy = errors / patternLen; + if (ignoreLocation) return accuracy; + const proximity = Math.abs(expectedLocation - currentLocation); + if (!distance) return proximity ? 1 : accuracy; + return accuracy + proximity / distance; + }; + const computeMatches = minMatchCharLength > 1 || includeMatches; + const matchMask = computeMatches ? Array(textLen) : []; + let index; + while ((index = text.indexOf(pattern, bestLocation)) > -1) { + const score = calcScore(0, index); + currentThreshold = Math.min(score, currentThreshold); + bestLocation = index + patternLen; + if (computeMatches) { + let i = 0; + while (i < patternLen) { + matchMask[index + i] = 1; + i += 1; + } + } + } + bestLocation = -1; + let lastBitArr = []; + let finalScore = 1; + let bestErrors = 0; + let binMax = patternLen + textLen; + const mask = 1 << patternLen - 1; + for (let i = 0; i < patternLen; i += 1) { + let binMin = 0; + let binMid = binMax; + while (binMin < binMid) { + if (calcScore(i, expectedLocation + binMid) <= currentThreshold) binMin = binMid; + else binMax = binMid; + binMid = Math.floor((binMax - binMin) / 2 + binMin); + } + binMax = binMid; + let start = Math.max(1, expectedLocation - binMid + 1); + const finish = findAllMatches ? textLen : Math.min(expectedLocation + binMid, textLen) + patternLen; + const bitArr = Array(finish + 2); + bitArr[finish + 1] = (1 << i) - 1; + for (let j = finish; j >= start; j -= 1) { + const currentLocation = j - 1; + const charMatch = patternAlphabet[text[currentLocation]]; + bitArr[j] = (bitArr[j + 1] << 1 | 1) & charMatch; + if (i) bitArr[j] |= (lastBitArr[j + 1] | lastBitArr[j]) << 1 | 1 | lastBitArr[j + 1]; + if (bitArr[j] & mask) { + finalScore = calcScore(i, currentLocation); + if (finalScore <= currentThreshold) { + currentThreshold = finalScore; + bestLocation = currentLocation; + bestErrors = i; + if (bestLocation <= expectedLocation) break; + start = Math.max(1, 2 * expectedLocation - bestLocation); + } + } + } + if (calcScore(i + 1, expectedLocation) > currentThreshold) break; + lastBitArr = bitArr; + } + if (computeMatches && bestLocation >= 0) { + const matchEnd = Math.min(textLen - 1, bestLocation + patternLen - 1 + bestErrors); + for (let k = bestLocation; k <= matchEnd; k += 1) if (patternAlphabet[text[k]]) matchMask[k] = 1; + } + const result = { + isMatch: bestLocation >= 0, + score: Math.max(.001, finalScore) + }; + if (computeMatches) { + const indices = convertMaskToIndices(matchMask, minMatchCharLength); + if (!indices.length) result.isMatch = false; + else if (includeMatches) result.indices = indices; + } + return result; } +//#endregion +//#region src/search/bitap/createPatternAlphabet.ts function createPatternAlphabet(pattern) { - const mask = {}; - for (let i = 0, len = pattern.length; i < len; i += 1) { - const char = pattern.charAt(i); - mask[char] = (mask[char] || 0) | 1 << len - i - 1; - } - return mask; + const mask = {}; + for (let i = 0, len = pattern.length; i < len; i += 1) { + const char = pattern.charAt(i); + mask[char] = (mask[char] || 0) | 1 << len - i - 1; + } + return mask; } +//#endregion +//#region src/helpers/mergeIndices.ts function mergeIndices(indices) { - if (indices.length <= 1) return indices; - indices.sort((a, b) => a[0] - b[0] || a[1] - b[1]); - const merged = [indices[0]]; - for (let i = 1, len = indices.length; i < len; i += 1) { - const last = merged[merged.length - 1]; - const curr = indices[i]; - if (curr[0] <= last[1] + 1) { - last[1] = Math.max(last[1], curr[1]); - } else { - merged.push(curr); - } - } - return merged; + if (indices.length <= 1) return indices; + indices.sort((a, b) => a[0] - b[0] || a[1] - b[1]); + const merged = [indices[0]]; + for (let i = 1, len = indices.length; i < len; i += 1) { + const last = merged[merged.length - 1]; + const curr = indices[i]; + if (curr[0] <= last[1] + 1) last[1] = Math.max(last[1], curr[1]); + else merged.push(curr); + } + return merged; } -// Characters that survive NFD normalization unchanged and need explicit mapping +//#endregion +//#region src/helpers/diacritics.ts const NON_DECOMPOSABLE_MAP = { - '\u0142': 'l', - // ł - '\u0141': 'L', - // Ł - '\u0111': 'd', - // đ - '\u0110': 'D', - // Đ - '\u00F8': 'o', - // ø - '\u00D8': 'O', - // Ø - '\u0127': 'h', - // ħ - '\u0126': 'H', - // Ħ - '\u0167': 't', - // ŧ - '\u0166': 'T', - // Ŧ - '\u0131': 'i', - // ı - '\u00DF': 'ss' // ß + "ł": "l", + "Ł": "L", + "đ": "d", + "Đ": "D", + "ø": "o", + "Ø": "O", + "ħ": "h", + "Ħ": "H", + "ŧ": "t", + "Ŧ": "T", + "ı": "i", + "ß": "ss" +}; +const NON_DECOMPOSABLE_RE = new RegExp("[" + Object.keys(NON_DECOMPOSABLE_MAP).join("") + "]", "g"); +const stripDiacritics = typeof String.prototype.normalize === "function" ? (str) => str.normalize("NFD").replace(/[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]/g, "").replace(NON_DECOMPOSABLE_RE, (ch) => NON_DECOMPOSABLE_MAP[ch]) : (str) => str; + +//#endregion +//#region src/search/bitap/index.ts +var BitapSearch = class { + constructor(pattern, { location = Config.location, threshold = Config.threshold, distance = Config.distance, includeMatches = Config.includeMatches, findAllMatches = Config.findAllMatches, minMatchCharLength = Config.minMatchCharLength, isCaseSensitive = Config.isCaseSensitive, ignoreDiacritics = Config.ignoreDiacritics, ignoreLocation = Config.ignoreLocation } = {}) { + this.options = { + location, + threshold, + distance, + includeMatches, + findAllMatches, + minMatchCharLength, + isCaseSensitive, + ignoreDiacritics, + ignoreLocation + }; + pattern = isCaseSensitive ? pattern : pattern.toLowerCase(); + pattern = ignoreDiacritics ? stripDiacritics(pattern) : pattern; + this.pattern = pattern; + this.chunks = []; + if (!this.pattern.length) return; + const addChunk = (pattern, startIndex) => { + this.chunks.push({ + pattern, + alphabet: createPatternAlphabet(pattern), + startIndex + }); + }; + const len = this.pattern.length; + if (len > 32) { + let i = 0; + const remainder = len % 32; + const end = len - remainder; + while (i < end) { + addChunk(this.pattern.substr(i, 32), i); + i += 32; + } + if (remainder) { + const startIndex = len - 32; + addChunk(this.pattern.substr(startIndex), startIndex); + } + } else addChunk(this.pattern, 0); + } + searchIn(text) { + const { isCaseSensitive, ignoreDiacritics, includeMatches } = this.options; + text = isCaseSensitive ? text : text.toLowerCase(); + text = ignoreDiacritics ? stripDiacritics(text) : text; + if (this.pattern === text) { + const result = { + isMatch: true, + score: 0 + }; + if (includeMatches) result.indices = [[0, text.length - 1]]; + return result; + } + const { location, distance, threshold, findAllMatches, minMatchCharLength, ignoreLocation } = this.options; + const allIndices = []; + let totalScore = 0; + let hasMatches = false; + this.chunks.forEach(({ pattern, alphabet, startIndex }) => { + const { isMatch, score, indices } = search(text, pattern, alphabet, { + location: location + startIndex, + distance, + threshold, + findAllMatches, + minMatchCharLength, + includeMatches, + ignoreLocation + }); + if (isMatch) hasMatches = true; + totalScore += score; + if (isMatch && indices) allIndices.push(...indices); + }); + const result = { + isMatch: hasMatches, + score: hasMatches ? totalScore / this.chunks.length : 1 + }; + if (hasMatches && includeMatches) result.indices = mergeIndices(allIndices); + return result; + } }; -const NON_DECOMPOSABLE_RE = new RegExp('[' + Object.keys(NON_DECOMPOSABLE_MAP).join('') + ']', 'g'); -const stripDiacritics = typeof String.prototype.normalize === 'function' ? str => str.normalize('NFD').replace(/[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]/g, '').replace(NON_DECOMPOSABLE_RE, ch => NON_DECOMPOSABLE_MAP[ch]) : str => str; - -class BitapSearch { - constructor(pattern, { - location = Config.location, - threshold = Config.threshold, - distance = Config.distance, - includeMatches = Config.includeMatches, - findAllMatches = Config.findAllMatches, - minMatchCharLength = Config.minMatchCharLength, - isCaseSensitive = Config.isCaseSensitive, - ignoreDiacritics = Config.ignoreDiacritics, - ignoreLocation = Config.ignoreLocation - } = {}) { - this.options = { - location, - threshold, - distance, - includeMatches, - findAllMatches, - minMatchCharLength, - isCaseSensitive, - ignoreDiacritics, - ignoreLocation - }; - pattern = isCaseSensitive ? pattern : pattern.toLowerCase(); - pattern = ignoreDiacritics ? stripDiacritics(pattern) : pattern; - this.pattern = pattern; - this.chunks = []; - if (!this.pattern.length) { - return; - } - const addChunk = (pattern, startIndex) => { - this.chunks.push({ - pattern, - alphabet: createPatternAlphabet(pattern), - startIndex - }); - }; - const len = this.pattern.length; - if (len > MAX_BITS) { - let i = 0; - const remainder = len % MAX_BITS; - const end = len - remainder; - while (i < end) { - addChunk(this.pattern.substr(i, MAX_BITS), i); - i += MAX_BITS; - } - if (remainder) { - const startIndex = len - MAX_BITS; - addChunk(this.pattern.substr(startIndex), startIndex); - } - } else { - addChunk(this.pattern, 0); - } - } - searchIn(text) { - const { - isCaseSensitive, - ignoreDiacritics, - includeMatches - } = this.options; - text = isCaseSensitive ? text : text.toLowerCase(); - text = ignoreDiacritics ? stripDiacritics(text) : text; - - // Exact match - if (this.pattern === text) { - const result = { - isMatch: true, - score: 0 - }; - if (includeMatches) { - result.indices = [[0, text.length - 1]]; - } - return result; - } - - // Otherwise, use Bitap algorithm - const { - location, - distance, - threshold, - findAllMatches, - minMatchCharLength, - ignoreLocation - } = this.options; - const allIndices = []; - let totalScore = 0; - let hasMatches = false; - this.chunks.forEach(({ - pattern, - alphabet, - startIndex - }) => { - const { - isMatch, - score, - indices - } = search(text, pattern, alphabet, { - location: location + startIndex, - distance, - threshold, - findAllMatches, - minMatchCharLength, - includeMatches, - ignoreLocation - }); - if (isMatch) { - hasMatches = true; - } - totalScore += score; - if (isMatch && indices) { - allIndices.push(...indices); - } - }); - const result = { - isMatch: hasMatches, - score: hasMatches ? totalScore / this.chunks.length : 1 - }; - if (hasMatches && includeMatches) { - result.indices = mergeIndices(allIndices); - } - return result; - } -} +//#endregion +//#region src/core/register.ts const registeredSearchers = []; function register(...args) { - registeredSearchers.push(...args); + registeredSearchers.push(...args); } function createSearcher(pattern, options) { - for (let i = 0, len = registeredSearchers.length; i < len; i += 1) { - const searcherClass = registeredSearchers[i]; - if (searcherClass.condition(pattern, options)) { - return new searcherClass(pattern, options); - } - } - return new BitapSearch(pattern, options); + for (let i = 0, len = registeredSearchers.length; i < len; i += 1) { + const searcherClass = registeredSearchers[i]; + if (searcherClass.condition(pattern, options)) return new searcherClass(pattern, options); + } + return new BitapSearch(pattern, options); } +//#endregion +//#region src/core/queryParser.ts const LogicalOperator = { - AND: '$and', - OR: '$or' + AND: "$and", + OR: "$or" }; const KeyType = { - PATH: '$path', - PATTERN: '$val' + PATH: "$path", + PATTERN: "$val" }; -const isExpression = query => !!(query[LogicalOperator.AND] || query[LogicalOperator.OR]); -const isPath = query => !!query[KeyType.PATH]; -const isLeaf = query => !isArray(query) && isObject(query) && !isExpression(query); -const convertToExplicit = query => ({ - [LogicalOperator.AND]: Object.keys(query).map(key => ({ - [key]: query[key] - })) -}); - -// When `auto` is `true`, the parse function will infer and initialize and add -// the appropriate `Searcher` instance -function parse(query, options, { - auto = true -} = {}) { - const next = query => { - // Keyless string entry: search across all keys - if (isString(query)) { - const obj = { - keyId: null, - pattern: query - }; - if (auto) { - obj.searcher = createSearcher(query, options); - } - return obj; - } - const keys = Object.keys(query); - const isQueryPath = isPath(query); - if (!isQueryPath && keys.length > 1 && !isExpression(query)) { - return next(convertToExplicit(query)); - } - if (isLeaf(query)) { - const key = isQueryPath ? query[KeyType.PATH] : keys[0]; - const pattern = isQueryPath ? query[KeyType.PATTERN] : query[key]; - if (!isString(pattern)) { - throw new Error(LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY(key)); - } - const obj = { - keyId: createKeyId(key), - pattern - }; - if (auto) { - obj.searcher = createSearcher(pattern, options); - } - return obj; - } - const node = { - children: [], - operator: keys[0] - }; - keys.forEach(key => { - const value = query[key]; - if (isArray(value)) { - value.forEach(item => { - node.children.push(next(item)); - }); - } - }); - return node; - }; - if (!isExpression(query)) { - query = convertToExplicit(query); - } - return next(query); +const isExpression = (query) => !!(query[LogicalOperator.AND] || query[LogicalOperator.OR]); +const isPath = (query) => !!query[KeyType.PATH]; +const isLeaf = (query) => !isArray(query) && isObject(query) && !isExpression(query); +const convertToExplicit = (query) => ({ [LogicalOperator.AND]: Object.keys(query).map((key) => ({ [key]: query[key] })) }); +function parse(query, options, { auto = true } = {}) { + const next = (query) => { + if (isString(query)) { + const obj = { + keyId: null, + pattern: query + }; + if (auto) obj.searcher = createSearcher(query, options); + return obj; + } + const keys = Object.keys(query); + const isQueryPath = isPath(query); + if (!isQueryPath && keys.length > 1 && !isExpression(query)) return next(convertToExplicit(query)); + if (isLeaf(query)) { + const key = isQueryPath ? query[KeyType.PATH] : keys[0]; + const pattern = isQueryPath ? query[KeyType.PATTERN] : query[key]; + if (!isString(pattern)) throw new Error(LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY(key)); + const obj = { + keyId: createKeyId(key), + pattern + }; + if (auto) obj.searcher = createSearcher(pattern, options); + return obj; + } + const node = { + children: [], + operator: keys[0] + }; + keys.forEach((key) => { + const value = query[key]; + if (isArray(value)) value.forEach((item) => { + node.children.push(next(item)); + }); + }); + return node; + }; + if (!isExpression(query)) query = convertToExplicit(query); + return next(query); } -function computeScoreSingle(matches, { - ignoreFieldNorm = Config.ignoreFieldNorm -}) { - let totalScore = 1; - matches.forEach(({ - key, - norm, - score - }) => { - const weight = key ? key.weight : null; - totalScore *= Math.pow(score === 0 && weight ? Number.EPSILON : score, (weight || 1) * (ignoreFieldNorm ? 1 : norm)); - }); - return totalScore; +//#endregion +//#region src/core/computeScore.ts +function computeScoreSingle(matches, { ignoreFieldNorm = Config.ignoreFieldNorm }) { + let totalScore = 1; + matches.forEach(({ key, norm, score }) => { + const weight = key ? key.weight : null; + totalScore *= Math.pow(score === 0 && weight ? Number.EPSILON : score, (weight || 1) * (ignoreFieldNorm ? 1 : norm)); + }); + return totalScore; } -function computeScore(results, { - ignoreFieldNorm = Config.ignoreFieldNorm -}) { - results.forEach(result => { - result.score = computeScoreSingle(result.matches, { - ignoreFieldNorm - }); - }); +function computeScore(results, { ignoreFieldNorm = Config.ignoreFieldNorm }) { + results.forEach((result) => { + result.score = computeScoreSingle(result.matches, { ignoreFieldNorm }); + }); } -// Max-heap by score: keeps the worst (highest) score at the top -// so we can efficiently evict it when a better result arrives. -class MaxHeap { - constructor(limit) { - this.limit = limit; - this.heap = []; - } - get size() { - return this.heap.length; - } - shouldInsert(score) { - return this.size < this.limit || score < this.heap[0].score; - } - insert(item) { - if (this.size < this.limit) { - this.heap.push(item); - this._bubbleUp(this.size - 1); - } else if (item.score < this.heap[0].score) { - this.heap[0] = item; - this._sinkDown(0); - } - } - extractSorted(sortFn) { - return this.heap.sort(sortFn); - } - _bubbleUp(i) { - const heap = this.heap; - while (i > 0) { - const parent = i - 1 >> 1; - if (heap[i].score <= heap[parent].score) break; - const tmp = heap[i]; - heap[i] = heap[parent]; - heap[parent] = tmp; - i = parent; - } - } - _sinkDown(i) { - const heap = this.heap; - const len = heap.length; - let largest = i; - do { - i = largest; - const left = 2 * i + 1; - const right = 2 * i + 2; - if (left < len && heap[left].score > heap[largest].score) { - largest = left; - } - if (right < len && heap[right].score > heap[largest].score) { - largest = right; - } - if (largest !== i) { - const tmp = heap[i]; - heap[i] = heap[largest]; - heap[largest] = tmp; - } - } while (largest !== i); - } -} +//#endregion +//#region src/tools/MaxHeap.ts +var MaxHeap = class { + constructor(limit) { + this.limit = limit; + this.heap = []; + } + get size() { + return this.heap.length; + } + shouldInsert(score) { + return this.size < this.limit || score < this.heap[0].score; + } + insert(item) { + if (this.size < this.limit) { + this.heap.push(item); + this._bubbleUp(this.size - 1); + } else if (item.score < this.heap[0].score) { + this.heap[0] = item; + this._sinkDown(0); + } + } + extractSorted(sortFn) { + return this.heap.sort(sortFn); + } + _bubbleUp(i) { + const heap = this.heap; + while (i > 0) { + const parent = i - 1 >> 1; + if (heap[i].score <= heap[parent].score) break; + const tmp = heap[i]; + heap[i] = heap[parent]; + heap[parent] = tmp; + i = parent; + } + } + _sinkDown(i) { + const heap = this.heap; + const len = heap.length; + let largest = i; + do { + i = largest; + const left = 2 * i + 1; + const right = 2 * i + 2; + if (left < len && heap[left].score > heap[largest].score) largest = left; + if (right < len && heap[right].score > heap[largest].score) largest = right; + if (largest !== i) { + const tmp = heap[i]; + heap[i] = heap[largest]; + heap[largest] = tmp; + } + } while (largest !== i); + } +}; +//#endregion +//#region src/core/formatMatches.ts function formatMatches(result) { - const matches = []; - result.matches.forEach(match => { - if (!isDefined(match.indices) || !match.indices.length) { - return; - } - const obj = { - indices: match.indices, - value: match.value - }; - if (match.key) { - // `key.id` is the canonical dotted-string identity (array paths joined - // with '.'); `key.src` is the raw user input and can be a string[]. - obj.key = match.key.id; - } - if (match.idx > -1) { - obj.refIndex = match.idx; - } - matches.push(obj); - }); - return matches; + const matches = []; + result.matches.forEach((match) => { + if (!isDefined(match.indices) || !match.indices.length) return; + const obj = { + indices: match.indices, + value: match.value + }; + if (match.key) obj.key = match.key.id; + if (match.idx > -1) obj.refIndex = match.idx; + matches.push(obj); + }); + return matches; } -function format(results, docs, { - includeMatches = Config.includeMatches, - includeScore = Config.includeScore -} = {}) { - return results.map(result => { - const { - idx - } = result; - const data = { - item: docs[idx], - refIndex: idx - }; - if (includeMatches) data.matches = formatMatches(result); - if (includeScore) data.score = result.score; - return data; - }); +//#endregion +//#region src/core/format.ts +function format(results, docs, { includeMatches = Config.includeMatches, includeScore = Config.includeScore } = {}) { + return results.map((result) => { + const { idx } = result; + const data = { + item: docs[idx], + refIndex: idx + }; + if (includeMatches) data.matches = formatMatches(result); + if (includeScore) data.score = result.score; + return data; + }); } -// Includes \p{M} (Mark) so combining marks stay attached to their base -// letter — without it, scripts like Devanagari and NFD-normalized Latin -// shatter (e.g. 'हिन्दी' → ['ह','न','द'], 'café'.normalize('NFD') → ['cafe']). +//#endregion +//#region src/search/token/analyzer.ts const DEFAULT_TOKEN = /[\p{L}\p{M}\p{N}_]+/gu; -const warned = new WeakSet(); +const warned = /* @__PURE__ */ new WeakSet(); function warnNonGlobal(regex) { - if (!warned.has(regex)) { - warned.add(regex); - console.warn(`[Fuse] tokenize regex ${regex} lacks the global flag; only the ` + `first match per text will be returned. Add the 'g' flag.`); - } + if (!warned.has(regex)) { + warned.add(regex); + console.warn(`[Fuse] tokenize regex ${regex} lacks the global flag; only the first match per text will be returned. Add the 'g' flag.`); + } } function resolveTokenize(tokenize) { - if (typeof tokenize === 'function') { - let validated = false; - return text => { - const result = tokenize(text); - if (!validated) { - validated = true; - if (!Array.isArray(result) || result.some(t => typeof t !== 'string')) { - throw new Error(`[Fuse] tokenize function must return string[]; received ${Array.isArray(result) ? 'array containing non-strings' : typeof result}.`); - } - } - return result; - }; - } - if (tokenize instanceof RegExp) { - if (!tokenize.global) warnNonGlobal(tokenize); - return text => text.match(tokenize) || []; - } - return text => text.match(DEFAULT_TOKEN) || []; + if (typeof tokenize === "function") { + let validated = false; + return (text) => { + const result = tokenize(text); + if (!validated) { + validated = true; + if (!Array.isArray(result) || result.some((t) => typeof t !== "string")) throw new Error(`[Fuse] tokenize function must return string[]; received ${Array.isArray(result) ? "array containing non-strings" : typeof result}.`); + } + return result; + }; + } + if (tokenize instanceof RegExp) { + if (!tokenize.global) warnNonGlobal(tokenize); + return (text) => text.match(tokenize) || []; + } + return (text) => text.match(DEFAULT_TOKEN) || []; } -function createAnalyzer({ - isCaseSensitive = false, - ignoreDiacritics = false, - tokenize -} = {}) { - const tokenizeFn = resolveTokenize(tokenize); - return { - tokenize(text) { - if (!isCaseSensitive) { - text = text.toLowerCase(); - } - if (ignoreDiacritics) { - text = stripDiacritics(text); - } - return tokenizeFn(text); - } - }; +function createAnalyzer({ isCaseSensitive = false, ignoreDiacritics = false, tokenize } = {}) { + const tokenizeFn = resolveTokenize(tokenize); + return { tokenize(text) { + if (!isCaseSensitive) text = text.toLowerCase(); + if (ignoreDiacritics) text = stripDiacritics(text); + return tokenizeFn(text); + } }; } -// `tokenMatch: 'all'` packs per-term coverage into a bitmask. JS bitwise ops -// are 32-bit *signed*, so bit 31 is the sign bit — only bits 0..30 are safe. -// Queries with more than this many terms fall back to a Set (no bit limit). -const MAX_MASK_TERMS = 31; - -// Stats-only inverted index for token search (per Plan 008 Direction B). -// -// The query path consumes only `df` and `fieldCount` (IDF weighting). The -// per-doc maps exist solely to keep `df` and `fieldCount` correct under -// `add` / `remove` / `removeAt`: -// -// docFieldCount[doc] = # distinct fields the doc contributed; subtracted -// from `fieldCount` on remove. -// docTermFieldHits[doc] = Map; each entry decrements `df[term]` by -// that count on remove. -// -// `df` is incremented once per (doc, term, field) at index time. Removing a -// doc decrements `df` by the same count, mirroring the increment exactly. - +//#endregion +//#region src/search/token/InvertedIndex.ts function addField(index, text, docIdx, analyzer) { - const tokens = analyzer.tokenize(text); - if (!tokens.length) return; - index.fieldCount++; - index.docFieldCount.set(docIdx, (index.docFieldCount.get(docIdx) || 0) + 1); - - // We count each (doc, term, field) once — repeated occurrences within the - // same field don't multiply df. - const distinctTerms = new Set(tokens); - let perDocTerms = index.docTermFieldHits.get(docIdx); - if (!perDocTerms) { - perDocTerms = new Map(); - index.docTermFieldHits.set(docIdx, perDocTerms); - } - for (const term of distinctTerms) { - perDocTerms.set(term, (perDocTerms.get(term) || 0) + 1); - index.df.set(term, (index.df.get(term) || 0) + 1); - } + const tokens = analyzer.tokenize(text); + if (!tokens.length) return; + index.fieldCount++; + index.docFieldCount.set(docIdx, (index.docFieldCount.get(docIdx) || 0) + 1); + const distinctTerms = new Set(tokens); + let perDocTerms = index.docTermFieldHits.get(docIdx); + if (!perDocTerms) { + perDocTerms = /* @__PURE__ */ new Map(); + index.docTermFieldHits.set(docIdx, perDocTerms); + } + for (const term of distinctTerms) { + perDocTerms.set(term, (perDocTerms.get(term) || 0) + 1); + index.df.set(term, (index.df.get(term) || 0) + 1); + } } function ingestRecord(index, record, keyCount, analyzer) { - const { - i: docIdx, - v, - $: fields - } = record; - if (v !== undefined) { - addField(index, v, docIdx, analyzer); - return; - } - if (!fields) return; - for (let keyIdx = 0; keyIdx < keyCount; keyIdx++) { - const value = fields[keyIdx]; - if (!value) continue; - if (Array.isArray(value)) { - for (const sub of value) addField(index, sub.v, docIdx, analyzer); - } else { - addField(index, value.v, docIdx, analyzer); - } - } + const { i: docIdx, v, $: fields } = record; + if (v !== void 0) { + addField(index, v, docIdx, analyzer); + return; + } + if (!fields) return; + for (let keyIdx = 0; keyIdx < keyCount; keyIdx++) { + const value = fields[keyIdx]; + if (!value) continue; + if (Array.isArray(value)) for (const sub of value) addField(index, sub.v, docIdx, analyzer); + else addField(index, value.v, docIdx, analyzer); + } } function buildInvertedIndex(records, keyCount, analyzer) { - const index = { - fieldCount: 0, - df: new Map(), - docFieldCount: new Map(), - docTermFieldHits: new Map() - }; - for (const record of records) { - ingestRecord(index, record, keyCount, analyzer); - } - return index; + const index = { + fieldCount: 0, + df: /* @__PURE__ */ new Map(), + docFieldCount: /* @__PURE__ */ new Map(), + docTermFieldHits: /* @__PURE__ */ new Map() + }; + for (const record of records) ingestRecord(index, record, keyCount, analyzer); + return index; } function addToInvertedIndex(index, record, keyCount, analyzer) { - ingestRecord(index, record, keyCount, analyzer); + ingestRecord(index, record, keyCount, analyzer); } function removeFromInvertedIndex(index, docIdx) { - const fieldCount = index.docFieldCount.get(docIdx); - if (fieldCount === undefined) return; - index.fieldCount -= fieldCount; - index.docFieldCount.delete(docIdx); - const perDocTerms = index.docTermFieldHits.get(docIdx); - if (!perDocTerms) return; - for (const [term, hits] of perDocTerms) { - const next = (index.df.get(term) || 0) - hits; - if (next <= 0) { - index.df.delete(term); - } else { - index.df.set(term, next); - } - } - index.docTermFieldHits.delete(docIdx); + const fieldCount = index.docFieldCount.get(docIdx); + if (fieldCount === void 0) return; + index.fieldCount -= fieldCount; + index.docFieldCount.delete(docIdx); + const perDocTerms = index.docTermFieldHits.get(docIdx); + if (!perDocTerms) return; + for (const [term, hits] of perDocTerms) { + const next = (index.df.get(term) || 0) - hits; + if (next <= 0) index.df.delete(term); + else index.df.set(term, next); + } + index.docTermFieldHits.delete(docIdx); } - -// Removes the given docIdx entries and renumbers the remaining per-doc maps -// so they stay in sync with FuseIndex's contiguous renumbering on remove. function removeAndShiftInvertedIndex(index, removedIndices) { - if (removedIndices.length === 0) return; - - // De-dup and sort so the shift computation is O(log k) per lookup. - const sorted = Array.from(new Set(removedIndices)).sort((a, b) => a - b); - for (const idx of sorted) { - removeFromInvertedIndex(index, idx); - } - - // For any surviving oldIdx, its new idx is oldIdx minus the number of - // removed indices strictly less than oldIdx. - const shift = oldIdx => { - let lo = 0; - let hi = sorted.length; - while (lo < hi) { - const mid = lo + hi >>> 1; - if (sorted[mid] < oldIdx) lo = mid + 1;else hi = mid; - } - return oldIdx - lo; - }; - const firstRemoved = sorted[0]; - const shiftedDocFieldCount = new Map(); - for (const [oldKey, count] of index.docFieldCount) { - shiftedDocFieldCount.set(oldKey > firstRemoved ? shift(oldKey) : oldKey, count); - } - index.docFieldCount = shiftedDocFieldCount; - const shiftedDocTermFieldHits = new Map(); - for (const [oldKey, terms] of index.docTermFieldHits) { - shiftedDocTermFieldHits.set(oldKey > firstRemoved ? shift(oldKey) : oldKey, terms); - } - index.docTermFieldHits = shiftedDocTermFieldHits; + if (removedIndices.length === 0) return; + const sorted = Array.from(new Set(removedIndices)).sort((a, b) => a - b); + for (const idx of sorted) removeFromInvertedIndex(index, idx); + const shift = (oldIdx) => { + let lo = 0; + let hi = sorted.length; + while (lo < hi) { + const mid = lo + hi >>> 1; + if (sorted[mid] < oldIdx) lo = mid + 1; + else hi = mid; + } + return oldIdx - lo; + }; + const firstRemoved = sorted[0]; + const shiftedDocFieldCount = /* @__PURE__ */ new Map(); + for (const [oldKey, count] of index.docFieldCount) shiftedDocFieldCount.set(oldKey > firstRemoved ? shift(oldKey) : oldKey, count); + index.docFieldCount = shiftedDocFieldCount; + const shiftedDocTermFieldHits = /* @__PURE__ */ new Map(); + for (const [oldKey, terms] of index.docTermFieldHits) shiftedDocTermFieldHits.set(oldKey > firstRemoved ? shift(oldKey) : oldKey, terms); + index.docTermFieldHits = shiftedDocTermFieldHits; } -class Fuse { - // Statics are assigned in entry.ts - - constructor(docs, options, index) { - this.options = { - ...Config, - ...options - }; - if (this.options.useExtendedSearch && true) { - throw new Error(EXTENDED_SEARCH_UNAVAILABLE); - } - if (this.options.useTokenSearch && true) { - throw new Error(TOKEN_SEARCH_UNAVAILABLE); - } - this._keyStore = new KeyStore(this.options.keys); - this._docs = docs; - this._myIndex = null; - this._invertedIndex = null; - this.setCollection(docs, index); - this._lastQuery = null; - this._lastSearcher = null; - } - _getSearcher(query) { - if (this._lastQuery === query) { - return this._lastSearcher; - } - const opts = this._invertedIndex ? { - ...this.options, - _invertedIndex: this._invertedIndex - } : this.options; - const searcher = createSearcher(query, opts); - this._lastQuery = query; - this._lastSearcher = searcher; - return searcher; - } - setCollection(docs, index) { - this._docs = docs; - if (index && !(index instanceof FuseIndex)) { - throw new Error(INCORRECT_INDEX_TYPE); - } - this._myIndex = index || createIndex(this.options.keys, this._docs, { - getFn: this.options.getFn, - fieldNormWeight: this.options.fieldNormWeight - }); - if (this.options.useTokenSearch) { - const analyzer = createAnalyzer({ - isCaseSensitive: this.options.isCaseSensitive, - ignoreDiacritics: this.options.ignoreDiacritics, - tokenize: this.options.tokenize - }); - this._invertedIndex = buildInvertedIndex(this._myIndex.records, this._myIndex.keys.length, analyzer); - } - this._invalidateSearcherCache(); - } - add(doc) { - if (!isDefined(doc)) { - return; - } - this._docs.push(doc); - const record = this._myIndex.add(doc, this._docs.length - 1); - - // Skip inverted-index bookkeeping when no record was appended (blank - // strings produce null). The previous code read `records[records.length-1]` - // unconditionally, which would re-ingest the previous doc on `add("")`. - if (this._invertedIndex && record) { - const analyzer = createAnalyzer({ - isCaseSensitive: this.options.isCaseSensitive, - ignoreDiacritics: this.options.ignoreDiacritics, - tokenize: this.options.tokenize - }); - addToInvertedIndex(this._invertedIndex, record, this._myIndex.keys.length, analyzer); - } - this._invalidateSearcherCache(); - } - remove(predicate = () => false) { - const results = []; - const indicesToRemove = []; - for (let i = 0, len = this._docs.length; i < len; i += 1) { - if (predicate(this._docs[i], i)) { - results.push(this._docs[i]); - indicesToRemove.push(i); - } - } - if (indicesToRemove.length) { - if (this._invertedIndex) { - removeAndShiftInvertedIndex(this._invertedIndex, indicesToRemove); - } - - // Filter docs in a single pass instead of reverse-splicing - const toRemove = new Set(indicesToRemove); - this._docs = this._docs.filter((_, i) => !toRemove.has(i)); - this._myIndex.removeAll(indicesToRemove); - this._invalidateSearcherCache(); - } - return results; - } - removeAt(idx) { - // Validate before any mutation. The previous code spliced `_docs` first - // and let FuseIndex.removeAt throw afterward — partial-state on invalid - // input. Atomic now. - if (!Number.isInteger(idx) || idx < 0 || idx >= this._docs.length) { - throw new Error(INVALID_DOC_INDEX); - } - if (this._invertedIndex) { - removeAndShiftInvertedIndex(this._invertedIndex, [idx]); - } - const doc = this._docs.splice(idx, 1)[0]; - this._myIndex.removeAt(idx); - this._invalidateSearcherCache(); - return doc; - } - _invalidateSearcherCache() { - this._lastQuery = null; - this._lastSearcher = null; - } - getIndex() { - return this._myIndex; - } - search(query, options) { - const { - limit = -1 - } = options || {}; - const { - includeMatches, - includeScore, - shouldSort, - sortFn, - ignoreFieldNorm - } = this.options; - - // Empty string query returns all docs (useful for search UIs) - if (isString(query) && !query.trim()) { - let docs = this._docs.map((item, idx) => ({ - item, - refIndex: idx - })); - if (isNumber(limit) && limit > -1) { - docs = docs.slice(0, limit); - } - return docs; - } - const useHeap = isNumber(limit) && limit > 0 && isString(query); - let results; - if (useHeap) { - const heap = new MaxHeap(limit); - if (isString(this._docs[0])) { - this._searchStringList(query, { - heap, - ignoreFieldNorm - }); - } else { - this._searchObjectList(query, { - heap, - ignoreFieldNorm - }); - } - results = heap.extractSorted(sortFn); - } else { - results = isString(query) ? isString(this._docs[0]) ? this._searchStringList(query) : this._searchObjectList(query) : this._searchLogical(query); - computeScore(results, { - ignoreFieldNorm - }); - if (shouldSort) { - results.sort(sortFn); - } - if (isNumber(limit) && limit > -1) { - results = results.slice(0, limit); - } - } - return format(results, this._docs, { - includeMatches, - includeScore - }); - } - _searchStringList(query, { - heap, - ignoreFieldNorm - } = {}) { - const searcher = this._getSearcher(query); - const requireAllTokens = this.options.useTokenSearch && this.options.tokenMatch === 'all'; - const { - records - } = this._myIndex; - const results = heap ? null : []; - - // Iterate over every string in the index - records.forEach(({ - v: text, - i: idx, - n: norm - }) => { - if (!isDefined(text)) { - return; - } - const searchResult = searcher.searchIn(text); - if (searchResult.isMatch) { - const match = { - score: searchResult.score, - value: text, - norm: norm, - indices: searchResult.indices - }; - if (requireAllTokens) { - match.matchedMask = searchResult.matchedMask; - match.matchedTerms = searchResult.matchedTerms; - match.termCount = searchResult.termCount; - } - const matches = [match]; - - // Record-level AND gate (token search `tokenMatch: 'all'`), applied - // before heap insertion so `limit` returns the same top-N as unlimited. - if (!requireAllTokens || this._coversAllTokens(matches)) { - const result = { - item: text, - idx, - matches - }; - if (heap) { - result.score = computeScoreSingle(result.matches, { - ignoreFieldNorm - }); - if (heap.shouldInsert(result.score)) { - heap.insert(result); - } - } else { - results.push(result); - } - } - } - }); - return results; - } - _searchLogical(query) { - { - throw new Error(LOGICAL_SEARCH_UNAVAILABLE); - } - } - - // When a search involves inverse patterns (e.g. !Syrup), the aggregation - // across keys switches from "ANY key matches" to "ALL keys must match." - // This is signaled by hasInverse on the SearchResult from ExtendedSearch. - // - // For mixed patterns like "^hello !Syrup", a key failure is ambiguous — - // it could be the positive or inverse term that failed. In that case we - // conservatively exclude the item, which is strictly better than the old - // behavior of including it. See: https://github.com/krisk/Fuse/issues/712 - _searchObjectList(query, { - heap, - ignoreFieldNorm - } = {}) { - const searcher = this._getSearcher(query); - const requireAllTokens = this.options.useTokenSearch && this.options.tokenMatch === 'all'; - const { - keys, - records - } = this._myIndex; - const results = heap ? null : []; - - // List is Array - records.forEach(({ - $: item, - i: idx - }) => { - if (!isDefined(item)) { - return; - } - const matches = []; - let anyKeyFailed = false; - let hasInverse = false; - - // Iterate over every key (i.e, path), and fetch the value at that key - keys.forEach((key, keyIndex) => { - const keyMatches = this._findMatches({ - key, - value: item[keyIndex], - searcher - }); - if (keyMatches.length) { - matches.push(...keyMatches); - if (keyMatches[0].hasInverse) { - hasInverse = true; - } - } else { - anyKeyFailed = true; - } - }); - - // If the search involves inverse patterns, ALL keys must match - if (hasInverse && anyKeyFailed) { - return; - } - - // Record-level AND gate (token search `tokenMatch: 'all'`): every query - // term must be covered across the record's field/array-element matches. - // Applied before heap insertion so `limit` returns the same top-N. - if (matches.length && (!requireAllTokens || this._coversAllTokens(matches))) { - const result = { - idx, - item, - matches - }; - if (heap) { - result.score = computeScoreSingle(result.matches, { - ignoreFieldNorm - }); - if (heap.shouldInsert(result.score)) { - heap.insert(result); - } - } else { - results.push(result); - } - } - }); - return results; - } - _findMatches({ - key, - value, - searcher - }) { - if (!isDefined(value)) { - return []; - } - const matches = []; - if (isArray(value)) { - value.forEach(({ - v: text, - i: idx, - n: norm - }) => { - if (!isDefined(text)) { - return; - } - const searchResult = searcher.searchIn(text); - if (searchResult.isMatch) { - const match = { - score: searchResult.score, - key, - value: text, - idx, - norm, - indices: searchResult.indices, - hasInverse: searchResult.hasInverse - }; - // Carry token-search AND coverage only when present, so the default - // (non-token / 'any') MatchScore keeps its original object shape. - if (searchResult.termCount !== undefined) { - match.matchedMask = searchResult.matchedMask; - match.matchedTerms = searchResult.matchedTerms; - match.termCount = searchResult.termCount; - } - matches.push(match); - } - }); - } else { - const { - v: text, - n: norm - } = value; - const searchResult = searcher.searchIn(text); - if (searchResult.isMatch) { - const match = { - score: searchResult.score, - key, - value: text, - norm, - indices: searchResult.indices, - hasInverse: searchResult.hasInverse - }; - if (searchResult.termCount !== undefined) { - match.matchedMask = searchResult.matchedMask; - match.matchedTerms = searchResult.matchedTerms; - match.termCount = searchResult.termCount; - } - matches.push(match); - } - } - return matches; - } - - // Record-level AND gate for token search (`tokenMatch: 'all'`). Returns true - // unless the matched terms across ALL of a record's field/array-element - // matches fail to cover every query term. `termCount` is only set by - // TokenSearch in 'all' mode, so non-token / 'any' searches always pass. - _coversAllTokens(matches) { - const termCount = matches.length ? matches[0].termCount : undefined; - if (termCount === undefined) { - return true; - } - if (termCount <= MAX_MASK_TERMS) { - let coverage = 0; - for (let i = 0; i < matches.length; i++) { - coverage |= matches[i].matchedMask || 0; - } - return coverage === 2 ** termCount - 1; - } - const coverage = new Set(); - for (let i = 0; i < matches.length; i++) { - const terms = matches[i].matchedTerms; - if (terms) { - for (const t of terms) { - coverage.add(t); - } - } - } - return coverage.size === termCount; - } -} +//#endregion +//#region src/core/index.ts +var Fuse = class { + constructor(docs, options, index) { + this.options = { + ...Config, + ...options + }; + if (this.options.useExtendedSearch && true) throw new Error(EXTENDED_SEARCH_UNAVAILABLE); + if (this.options.useTokenSearch && true) throw new Error(TOKEN_SEARCH_UNAVAILABLE); + this._keyStore = new KeyStore(this.options.keys); + this._docs = docs; + this._myIndex = null; + this._invertedIndex = null; + this.setCollection(docs, index); + this._lastQuery = null; + this._lastSearcher = null; + } + _getSearcher(query) { + if (this._lastQuery === query) return this._lastSearcher; + const searcher = createSearcher(query, this._invertedIndex ? { + ...this.options, + _invertedIndex: this._invertedIndex + } : this.options); + this._lastQuery = query; + this._lastSearcher = searcher; + return searcher; + } + setCollection(docs, index) { + this._docs = docs; + if (index && !(index instanceof FuseIndex)) throw new Error(INCORRECT_INDEX_TYPE); + this._myIndex = index || createIndex(this.options.keys, this._docs, { + getFn: this.options.getFn, + fieldNormWeight: this.options.fieldNormWeight + }); + if (this.options.useTokenSearch) { + const analyzer = createAnalyzer({ + isCaseSensitive: this.options.isCaseSensitive, + ignoreDiacritics: this.options.ignoreDiacritics, + tokenize: this.options.tokenize + }); + this._invertedIndex = buildInvertedIndex(this._myIndex.records, this._myIndex.keys.length, analyzer); + } + this._invalidateSearcherCache(); + } + add(doc) { + if (!isDefined(doc)) return; + this._docs.push(doc); + const record = this._myIndex.add(doc, this._docs.length - 1); + if (this._invertedIndex && record) { + const analyzer = createAnalyzer({ + isCaseSensitive: this.options.isCaseSensitive, + ignoreDiacritics: this.options.ignoreDiacritics, + tokenize: this.options.tokenize + }); + addToInvertedIndex(this._invertedIndex, record, this._myIndex.keys.length, analyzer); + } + this._invalidateSearcherCache(); + } + remove(predicate = () => false) { + const results = []; + const indicesToRemove = []; + for (let i = 0, len = this._docs.length; i < len; i += 1) if (predicate(this._docs[i], i)) { + results.push(this._docs[i]); + indicesToRemove.push(i); + } + if (indicesToRemove.length) { + if (this._invertedIndex) removeAndShiftInvertedIndex(this._invertedIndex, indicesToRemove); + const toRemove = new Set(indicesToRemove); + this._docs = this._docs.filter((_, i) => !toRemove.has(i)); + this._myIndex.removeAll(indicesToRemove); + this._invalidateSearcherCache(); + } + return results; + } + removeAt(idx) { + if (!Number.isInteger(idx) || idx < 0 || idx >= this._docs.length) throw new Error(INVALID_DOC_INDEX); + if (this._invertedIndex) removeAndShiftInvertedIndex(this._invertedIndex, [idx]); + const doc = this._docs.splice(idx, 1)[0]; + this._myIndex.removeAt(idx); + this._invalidateSearcherCache(); + return doc; + } + _invalidateSearcherCache() { + this._lastQuery = null; + this._lastSearcher = null; + } + getIndex() { + return this._myIndex; + } + search(query, options) { + const { limit = -1 } = options || {}; + const { includeMatches, includeScore, shouldSort, sortFn, ignoreFieldNorm } = this.options; + if (isString(query) && !query.trim()) { + let docs = this._docs.map((item, idx) => ({ + item, + refIndex: idx + })); + if (isNumber(limit) && limit > -1) docs = docs.slice(0, limit); + return docs; + } + const useHeap = isNumber(limit) && limit > 0 && isString(query); + let results; + if (useHeap) { + const heap = new MaxHeap(limit); + if (isString(this._docs[0])) this._searchStringList(query, { + heap, + ignoreFieldNorm + }); + else this._searchObjectList(query, { + heap, + ignoreFieldNorm + }); + results = heap.extractSorted(sortFn); + } else { + results = isString(query) ? isString(this._docs[0]) ? this._searchStringList(query) : this._searchObjectList(query) : this._searchLogical(query); + computeScore(results, { ignoreFieldNorm }); + if (shouldSort) results.sort(sortFn); + if (isNumber(limit) && limit > -1) results = results.slice(0, limit); + } + return format(results, this._docs, { + includeMatches, + includeScore + }); + } + _searchStringList(query, { heap, ignoreFieldNorm } = {}) { + const searcher = this._getSearcher(query); + const requireAllTokens = this.options.useTokenSearch && this.options.tokenMatch === "all"; + const { records } = this._myIndex; + const results = heap ? null : []; + records.forEach(({ v: text, i: idx, n: norm }) => { + if (!isDefined(text)) return; + const searchResult = searcher.searchIn(text); + if (searchResult.isMatch) { + const match = { + score: searchResult.score, + value: text, + norm, + indices: searchResult.indices + }; + if (requireAllTokens) { + match.matchedMask = searchResult.matchedMask; + match.matchedTerms = searchResult.matchedTerms; + match.termCount = searchResult.termCount; + } + const matches = [match]; + if (!requireAllTokens || this._coversAllTokens(matches)) { + const result = { + item: text, + idx, + matches + }; + if (heap) { + result.score = computeScoreSingle(result.matches, { ignoreFieldNorm }); + if (heap.shouldInsert(result.score)) heap.insert(result); + } else results.push(result); + } + } + }); + return results; + } + _searchLogical(query) { + throw new Error(LOGICAL_SEARCH_UNAVAILABLE); + } + _searchObjectList(query, { heap, ignoreFieldNorm } = {}) { + const searcher = this._getSearcher(query); + const requireAllTokens = this.options.useTokenSearch && this.options.tokenMatch === "all"; + const { keys, records } = this._myIndex; + const results = heap ? null : []; + records.forEach(({ $: item, i: idx }) => { + if (!isDefined(item)) return; + const matches = []; + let anyKeyFailed = false; + let hasInverse = false; + keys.forEach((key, keyIndex) => { + const keyMatches = this._findMatches({ + key, + value: item[keyIndex], + searcher + }); + if (keyMatches.length) { + matches.push(...keyMatches); + if (keyMatches[0].hasInverse) hasInverse = true; + } else anyKeyFailed = true; + }); + if (hasInverse && anyKeyFailed) return; + if (matches.length && (!requireAllTokens || this._coversAllTokens(matches))) { + const result = { + idx, + item, + matches + }; + if (heap) { + result.score = computeScoreSingle(result.matches, { ignoreFieldNorm }); + if (heap.shouldInsert(result.score)) heap.insert(result); + } else results.push(result); + } + }); + return results; + } + _findMatches({ key, value, searcher }) { + if (!isDefined(value)) return []; + const matches = []; + if (isArray(value)) value.forEach(({ v: text, i: idx, n: norm }) => { + if (!isDefined(text)) return; + const searchResult = searcher.searchIn(text); + if (searchResult.isMatch) { + const match = { + score: searchResult.score, + key, + value: text, + idx, + norm, + indices: searchResult.indices, + hasInverse: searchResult.hasInverse + }; + if (searchResult.termCount !== void 0) { + match.matchedMask = searchResult.matchedMask; + match.matchedTerms = searchResult.matchedTerms; + match.termCount = searchResult.termCount; + } + matches.push(match); + } + }); + else { + const { v: text, n: norm } = value; + const searchResult = searcher.searchIn(text); + if (searchResult.isMatch) { + const match = { + score: searchResult.score, + key, + value: text, + norm, + indices: searchResult.indices, + hasInverse: searchResult.hasInverse + }; + if (searchResult.termCount !== void 0) { + match.matchedMask = searchResult.matchedMask; + match.matchedTerms = searchResult.matchedTerms; + match.termCount = searchResult.termCount; + } + matches.push(match); + } + } + return matches; + } + _coversAllTokens(matches) { + const termCount = matches.length ? matches[0].termCount : void 0; + if (termCount === void 0) return true; + if (termCount <= 31) { + let coverage = 0; + for (let i = 0; i < matches.length; i++) coverage |= matches[i].matchedMask || 0; + return coverage === 2 ** termCount - 1; + } + const coverage = /* @__PURE__ */ new Set(); + for (let i = 0; i < matches.length; i++) { + const terms = matches[i].matchedTerms; + if (terms) for (const t of terms) coverage.add(t); + } + return coverage.size === termCount; + } +}; -Fuse.version = '7.4.1'; +//#endregion +//#region src/entry.ts +Fuse.version = "7.4.1"; Fuse.createIndex = createIndex; Fuse.parseIndex = parseIndex; Fuse.config = Config; -Fuse.match = function (pattern, text, options) { - // Token search needs corpus statistics (df, fieldCount) that a one-off - // string comparison can't provide. Reject it here so the contract is the - // same in the full and basic builds — without this guard, the full build - // crashes with an opaque TypeError and the basic build silently falls back - // to fuzzy matching. - if (options && options.useTokenSearch) { - throw new Error(FUSE_MATCH_TOKEN_SEARCH_UNSUPPORTED); - } - const searcher = createSearcher(pattern, { - ...Config, - ...options - }); - return searcher.searchIn(text); +Fuse.match = function(pattern, text, options) { + if (options && options.useTokenSearch) throw new Error(FUSE_MATCH_TOKEN_SEARCH_UNSUPPORTED); + return createSearcher(pattern, { + ...Config, + ...options + }).searchIn(text); }; -{ - Fuse.parseQuery = parse; -} -Fuse.use = function (...plugins) { - plugins.forEach(plugin => register(plugin)); +Fuse.parseQuery = parse; +Fuse.use = function(...plugins) { + plugins.forEach((plugin) => register(plugin)); }; +var entry_default = Fuse; -// Re-export public types - -module.exports = Fuse; +//#endregion +module.exports = entry_default; \ No newline at end of file diff --git a/dist/fuse.basic.min.cjs b/dist/fuse.basic.min.cjs index eb2ad33ff..2545d7e05 100644 --- a/dist/fuse.basic.min.cjs +++ b/dist/fuse.basic.min.cjs @@ -6,4 +6,4 @@ * * http://www.apache.org/licenses/LICENSE-2.0 */ -"use strict";function e(e){return Array.isArray?Array.isArray(e):"[object Array]"===u(e)}function t(e){return null==e?"":function(e){if("string"==typeof e)return e;if("bigint"==typeof e)return e.toString();const t=e+"";return"0"==t&&1/e==-1/0?"-0":t}(e)}function s(e){return"string"==typeof e}function n(e){return"number"==typeof e}function i(e){return!0===e||!1===e||function(e){return function(e){return"object"==typeof e}(e)&&null!==e}(e)&&"[object Boolean]"==u(e)}function r(e){return null!=e}function o(e){return!e.trim().length}function u(e){return null==e?void 0===e?"[object Undefined]":"[object Null]":Object.prototype.toString.call(e)}const c="Invalid doc index: must be a non-negative integer within the bounds of the docs array",h=Object.prototype.hasOwnProperty;class a{constructor(e){this._keys=[],this._keyMap={};let t=0;e.forEach(e=>{const s=l(e);this._keys.push(s),this._keyMap[s.id]=s,t+=s.weight}),this._keys.forEach(e=>{e.weight/=t})}get(e){return this._keyMap[e]}keys(){return this._keys}toJSON(){return JSON.stringify(this._keys)}}function l(t){let n=null,i=null,r=null,o=1,u=null;if(s(t)||e(t))r=t,n=d(t),i=f(t);else{if(!h.call(t,"name"))throw new Error((e=>`Missing ${e} property in key`)("name"));const e=t.name;if(r=e,h.call(t,"weight")&&void 0!==t.weight&&(o=t.weight,o<=0))throw new Error((e=>`Property 'weight' in key '${e}' must be a positive integer`)(f(e)));n=d(e),i=f(e),u=t.getFn??null}return{path:n,id:i,weight:o,src:r,getFn:u}}function d(t){return e(t)?t:t.split(".")}function f(t){return e(t)?t.join("."):t}const g={useExtendedSearch:!1,useTokenSearch:!1,tokenize:void 0,tokenMatch:"any",getFn:function(o,u){const c=[];let h=!1;const a=(o,u,l,d)=>{if(r(o))if(u[l]){const f=o[u[l]];if(!r(f))return;if(l===u.length-1&&(s(f)||n(f)||i(f)||"bigint"==typeof f))c.push(void 0!==d?{v:t(f),i:d}:t(f));else if(e(f)){h=!0;for(let e=0,t=f.length;ee.score===t.score?e.idx{this._keysMap[e.id]=t})}create(){if(this.isCreated||!this.docs.length)return;this.isCreated=!0;const e=this.docs.length;this.records=new Array(e);let t=0;if(s(this.docs[0]))for(let s=0;se&&(this.records[t].i-=1)}removeAll(e){const t=new Set;for(const s of e)Number.isInteger(s)&&s>=0&&t.add(s);if(0===t.size)return;this.records=this.records.filter(e=>!t.has(e.i));const s=Array.from(t).sort((e,t)=>e-t);for(const e of this.records){let t=0,n=s.length;for(;t>>1;s[i]t),records:this.records}}}function C(e,t,{getFn:s=A.getFn,fieldNormWeight:n=A.fieldNormWeight}={}){const i=new m({getFn:s,fieldNormWeight:n});return i.setKeys(e.map(l)),i.setSources(t),i.create(),i}const p=32;function F(e,t,s,{location:n=A.location,distance:i=A.distance,threshold:r=A.threshold,findAllMatches:o=A.findAllMatches,minMatchCharLength:u=A.minMatchCharLength,includeMatches:c=A.includeMatches,ignoreLocation:h=A.ignoreLocation}={}){if(t.length>p)throw new Error(`Pattern length exceeds max of ${p}.`);const a=t.length,l=e.length,d=Math.max(0,Math.min(n,l));let f=r,g=d;const m=(e,t)=>{const s=e/a;if(h)return s;const n=Math.abs(d-t);return i?s+n/i:n?1:s},C=u>1||c,F=C?Array(l):[];let E;for(;(E=e.indexOf(t,g))>-1;){const e=m(0,E);if(f=Math.min(e,f),g=E+a,C){let e=0;for(;e=r;n-=1){const i=n-1,o=s[e[i]];if(c[n]=(c[n+1]<<1|1)&o,t&&(c[n]|=(_[n+1]|_[n])<<1|1|_[n+1]),c[n]&v&&(D=m(t,i),D<=f)){if(f=D,g=i,y=t,g<=d)break;r=Math.max(1,2*d-g)}}if(m(t+1,d)>f)break;_=c}if(C&&g>=0){const t=Math.min(l-1,g+a-1+y);for(let n=g;n<=t;n+=1)s[e[n]]&&(F[n]=1)}const M={isMatch:g>=0,score:Math.max(.001,D)};if(C){const e=function(e=[],t=A.minMatchCharLength){const s=[];let n=-1,i=-1,r=0;for(let o=e.length;r=t&&s.push([n,i]),n=-1)}return e[r-1]&&r-n>=t&&s.push([n,r-1]),s}(F,u);e.length?c&&(M.indices=e):M.isMatch=!1}return M}function E(e){const t={};for(let s=0,n=e.length;se.normalize("NFD").replace(/[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]/g,"").replace(D,e=>_[e]):e=>e;class B{constructor(e,{location:t=A.location,threshold:s=A.threshold,distance:n=A.distance,includeMatches:i=A.includeMatches,findAllMatches:r=A.findAllMatches,minMatchCharLength:o=A.minMatchCharLength,isCaseSensitive:u=A.isCaseSensitive,ignoreDiacritics:c=A.ignoreDiacritics,ignoreLocation:h=A.ignoreLocation}={}){if(this.options={location:t,threshold:s,distance:n,includeMatches:i,findAllMatches:r,minMatchCharLength:o,isCaseSensitive:u,ignoreDiacritics:c,ignoreLocation:h},e=u?e:e.toLowerCase(),e=c?y(e):e,this.pattern=e,this.chunks=[],!this.pattern.length)return;const a=(e,t)=>{this.chunks.push({pattern:e,alphabet:E(e),startIndex:t})},l=this.pattern.length;if(l>p){let e=0;const t=l%p,s=l-t;for(;e{const{isMatch:g,score:A,indices:m}=F(e,t,s,{location:i+f,distance:r,threshold:o,findAllMatches:u,minMatchCharLength:c,includeMatches:n,ignoreLocation:h});g&&(d=!0),l+=A,g&&m&&a.push(...m)});const f={isMatch:d,score:d?l/this.chunks.length:1};return d&&n&&(f.indices=function(e){if(e.length<=1)return e;e.sort((e,t)=>e[0]-t[0]||e[1]-t[1]);const t=[e[0]];for(let s=1,n=e.length;s{const r=e?e.weight:null;s*=Math.pow(0===i&&r?Number.EPSILON:i,(r||1)*(t?1:n))}),s}class w{constructor(e){this.limit=e,this.heap=[]}get size(){return this.heap.length}shouldInsert(e){return this.size0;){const s=e-1>>1;if(t[e].score<=t[s].score)break;const n=t[e];t[e]=t[s],t[s]=n,e=s}}_sinkDown(e){const t=this.heap,s=t.length;let n=e;do{const i=2*(e=n)+1,r=2*e+2;if(it[n].score&&(n=i),rt[n].score&&(n=r),n!==e){const s=t[e];t[e]=t[n],t[n]=s}}while(n!==e)}}function S(e,t,{includeMatches:s=A.includeMatches,includeScore:n=A.includeScore}={}){return e.map(e=>{const{idx:i}=e,o={item:t[i],refIndex:i};return s&&(o.matches=function(e){const t=[];return e.matches.forEach(e=>{if(!r(e.indices)||!e.indices.length)return;const s={indices:e.indices,value:e.value};e.key&&(s.key=e.key.id),e.idx>-1&&(s.refIndex=e.idx),t.push(s)}),t}(e)),n&&(o.score=e.score),o})}const x=/[\p{L}\p{M}\p{N}_]+/gu;function I({isCaseSensitive:e=!1,ignoreDiacritics:t=!1,tokenize:s}={}){const n=function(e){return"function"==typeof e?t=>e(t):e instanceof RegExp?(e.global,t=>t.match(e)||[]):e=>e.match(x)||[]}(s);return{tokenize:s=>(e||(s=s.toLowerCase()),t&&(s=y(s)),n(s))}}function b(e,t,s,n){const i=n.tokenize(t);if(!i.length)return;e.fieldCount++,e.docFieldCount.set(s,(e.docFieldCount.get(s)||0)+1);const r=new Set(i);let o=e.docTermFieldHits.get(s);o||(o=new Map,e.docTermFieldHits.set(s,o));for(const t of r)o.set(t,(o.get(t)||0)+1),e.df.set(t,(e.df.get(t)||0)+1)}function N(e,t,s,n){const{i:i,v:r,$:o}=t;if(void 0===r){if(o)for(let t=0;te-t);for(const t of s)L(e,t);const n=e=>{let t=0,n=s.length;for(;t>>1;s[i]i?n(t):t,s);e.docFieldCount=r;const o=new Map;for(const[t,s]of e.docTermFieldHits)o.set(t>i?n(t):t,s);e.docTermFieldHits=o}class z{constructor(e,t,s){if(this.options={...A,...t},this.options.useExtendedSearch)throw new Error("Extended search is not available");if(this.options.useTokenSearch)throw new Error("Token search is not available");this._keyStore=new a(this.options.keys),this._docs=e,this._myIndex=null,this._invertedIndex=null,this.setCollection(e,s),this._lastQuery=null,this._lastSearcher=null}_getSearcher(e){if(this._lastQuery===e)return this._lastSearcher;const t=M(e,this._invertedIndex?{...this.options,_invertedIndex:this._invertedIndex}:this.options);return this._lastQuery=e,this._lastSearcher=t,t}setCollection(e,t){if(this._docs=e,t&&!(t instanceof m))throw new Error("Incorrect 'index' type");if(this._myIndex=t||C(this.options.keys,this._docs,{getFn:this.options.getFn,fieldNormWeight:this.options.fieldNormWeight}),this.options.useTokenSearch){const e=I({isCaseSensitive:this.options.isCaseSensitive,ignoreDiacritics:this.options.ignoreDiacritics,tokenize:this.options.tokenize});this._invertedIndex=function(e,t,s){const n={fieldCount:0,df:new Map,docFieldCount:new Map,docTermFieldHits:new Map};for(const i of e)N(n,i,t,s);return n}(this._myIndex.records,this._myIndex.keys.length,e)}this._invalidateSearcherCache()}add(e){if(!r(e))return;this._docs.push(e);const t=this._myIndex.add(e,this._docs.length-1);if(this._invertedIndex&&t){const e=I({isCaseSensitive:this.options.isCaseSensitive,ignoreDiacritics:this.options.ignoreDiacritics,tokenize:this.options.tokenize});!function(e,t,s,n){N(e,t,s,n)}(this._invertedIndex,t,this._myIndex.keys.length,e)}this._invalidateSearcherCache()}remove(e=()=>!1){const t=[],s=[];for(let n=0,i=this._docs.length;n!e.has(s)),this._myIndex.removeAll(s),this._invalidateSearcherCache()}return t}removeAt(e){if(!Number.isInteger(e)||e<0||e>=this._docs.length)throw new Error(c);this._invertedIndex&&T(this._invertedIndex,[e]);const t=this._docs.splice(e,1)[0];return this._myIndex.removeAt(e),this._invalidateSearcherCache(),t}_invalidateSearcherCache(){this._lastQuery=null,this._lastSearcher=null}getIndex(){return this._myIndex}search(e,t){const{limit:i=-1}=t||{},{includeMatches:r,includeScore:o,shouldSort:u,sortFn:c,ignoreFieldNorm:h}=this.options;if(s(e)&&!e.trim()){let e=this._docs.map((e,t)=>({item:e,refIndex:t}));return n(i)&&i>-1&&(e=e.slice(0,i)),e}let a;if(n(i)&&i>0&&s(e)){const t=new w(i);s(this._docs[0])?this._searchStringList(e,{heap:t,ignoreFieldNorm:h}):this._searchObjectList(e,{heap:t,ignoreFieldNorm:h}),a=t.extractSorted(c)}else a=s(e)?s(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e),function(e,{ignoreFieldNorm:t=A.ignoreFieldNorm}){e.forEach(e=>{e.score=k(e.matches,{ignoreFieldNorm:t})})}(a,{ignoreFieldNorm:h}),u&&a.sort(c),n(i)&&i>-1&&(a=a.slice(0,i));return S(a,this._docs,{includeMatches:r,includeScore:o})}_searchStringList(e,{heap:t,ignoreFieldNorm:s}={}){const n=this._getSearcher(e),i=this.options.useTokenSearch&&"all"===this.options.tokenMatch,{records:o}=this._myIndex,u=t?null:[];return o.forEach(({v:e,i:o,n:c})=>{if(!r(e))return;const h=n.searchIn(e);if(h.isMatch){const n={score:h.score,value:e,norm:c,indices:h.indices};i&&(n.matchedMask=h.matchedMask,n.matchedTerms=h.matchedTerms,n.termCount=h.termCount);const r=[n];if(!i||this._coversAllTokens(r)){const n={item:e,idx:o,matches:r};t?(n.score=k(n.matches,{ignoreFieldNorm:s}),t.shouldInsert(n.score)&&t.insert(n)):u.push(n)}}}),u}_searchLogical(e){throw new Error("Logical search is not available")}_searchObjectList(e,{heap:t,ignoreFieldNorm:s}={}){const n=this._getSearcher(e),i=this.options.useTokenSearch&&"all"===this.options.tokenMatch,{keys:o,records:u}=this._myIndex,c=t?null:[];return u.forEach(({$:e,i:u})=>{if(!r(e))return;const h=[];let a=!1,l=!1;if(o.forEach((t,s)=>{const i=this._findMatches({key:t,value:e[s],searcher:n});i.length?(h.push(...i),i[0].hasInverse&&(l=!0)):a=!0}),(!l||!a)&&h.length&&(!i||this._coversAllTokens(h))){const n={idx:u,item:e,matches:h};t?(n.score=k(n.matches,{ignoreFieldNorm:s}),t.shouldInsert(n.score)&&t.insert(n)):c.push(n)}}),c}_findMatches({key:t,value:s,searcher:n}){if(!r(s))return[];const i=[];if(e(s))s.forEach(({v:e,i:s,n:o})=>{if(!r(e))return;const u=n.searchIn(e);if(u.isMatch){const n={score:u.score,key:t,value:e,idx:s,norm:o,indices:u.indices,hasInverse:u.hasInverse};void 0!==u.termCount&&(n.matchedMask=u.matchedMask,n.matchedTerms=u.matchedTerms,n.termCount=u.termCount),i.push(n)}});else{const{v:e,n:r}=s,o=n.searchIn(e);if(o.isMatch){const s={score:o.score,key:t,value:e,norm:r,indices:o.indices,hasInverse:o.hasInverse};void 0!==o.termCount&&(s.matchedMask=o.matchedMask,s.matchedTerms=o.matchedTerms,s.termCount=o.termCount),i.push(s)}}return i}_coversAllTokens(e){const t=e.length?e[0].termCount:void 0;if(void 0===t)return!0;if(t<=31){let s=0;for(let t=0;tfunction(...e){v.push(...e)}(e))},module.exports=z; \ No newline at end of file +function e(e){return Array.isArray?Array.isArray(e):u(e)===`[object Array]`}function t(e){if(typeof e==`string`)return e;if(typeof e==`bigint`)return e.toString();let t=e+``;return t==`0`&&1/e==-1/0?`-0`:t}function n(e){return e==null?``:t(e)}function r(e){return typeof e==`string`}function i(e){return typeof e==`number`}function a(e){return e===!0||e===!1||s(e)&&u(e)==`[object Boolean]`}function o(e){return typeof e==`object`}function s(e){return o(e)&&e!==null}function c(e){return e!=null}function l(e){return!e.trim().length}function u(e){return e==null?e===void 0?`[object Undefined]`:`[object Null]`:Object.prototype.toString.call(e)}const d=`Invalid doc index: must be a non-negative integer within the bounds of the docs array`,f=e=>`Pattern length exceeds max of ${e}.`,p=e=>`Missing ${e} property in key`,m=e=>`Property 'weight' in key '${e}' must be a positive integer`,h=Object.prototype.hasOwnProperty;var g=class{constructor(e){this._keys=[],this._keyMap={};let t=0;e.forEach(e=>{let n=_(e);this._keys.push(n),this._keyMap[n.id]=n,t+=n.weight}),this._keys.forEach(e=>{e.weight/=t})}get(e){return this._keyMap[e]}keys(){return this._keys}toJSON(){return JSON.stringify(this._keys)}};function _(t){let n=null,i=null,a=null,o=1,s=null;if(r(t)||e(t))a=t,n=v(t),i=y(t);else{if(!h.call(t,`name`))throw Error(p(`name`));let e=t.name;if(a=e,h.call(t,`weight`)&&t.weight!==void 0&&(o=t.weight,o<=0))throw Error(m(y(e)));n=v(e),i=y(e),s=t.getFn??null}return{path:n,id:i,weight:o,src:a,getFn:s}}function v(t){return e(t)?t:t.split(`.`)}function y(t){return e(t)?t.join(`.`):t}function b(t,o){let s=[],l=!1,u=(t,o,d,f)=>{if(c(t))if(!o[d])s.push(f===void 0?t:{v:t,i:f});else{let p=t[o[d]];if(!c(p))return;if(d===o.length-1&&(r(p)||i(p)||a(p)||typeof p==`bigint`))s.push(f===void 0?n(p):{v:n(p),i:f});else if(e(p)){l=!0;for(let e=0,t=p.length;ee.score===t.score?e.idx{this._keysMap[e.id]=t})}create(){if(this.isCreated||!this.docs.length)return;this.isCreated=!0;let e=this.docs.length;this.records=Array(e);let t=0;if(r(this.docs[0]))for(let n=0;ne&&--this.records[t].i}removeAll(e){let t=new Set;for(let n of e)Number.isInteger(n)&&n>=0&&t.add(n);if(t.size===0)return;this.records=this.records.filter(e=>!t.has(e.i));let n=Array.from(t).sort((e,t)=>e-t);for(let e of this.records){let t=0,r=n.length;for(;t>>1;n[i]t),records:this.records}}};function O(e,t,{getFn:n=T.getFn,fieldNormWeight:r=T.fieldNormWeight}={}){let i=new D({getFn:n,fieldNormWeight:r});return i.setKeys(e.map(_)),i.setSources(t),i.create(),i}function k(e,{getFn:t=T.getFn,fieldNormWeight:n=T.fieldNormWeight}={}){let{keys:r,records:i}=e,a=new D({getFn:t,fieldNormWeight:n});return a.setKeys(r),a.setIndexRecords(i),a}function A(e=[],t=T.minMatchCharLength){let n=[],r=-1,i=-1,a=0;for(let o=e.length;a=t&&n.push([r,i]),r=-1)}return e[a-1]&&a-r>=t&&n.push([r,a-1]),n}function j(e,t,n,{location:r=T.location,distance:i=T.distance,threshold:a=T.threshold,findAllMatches:o=T.findAllMatches,minMatchCharLength:s=T.minMatchCharLength,includeMatches:c=T.includeMatches,ignoreLocation:l=T.ignoreLocation}={}){if(t.length>32)throw Error(f(32));let u=t.length,d=e.length,p=Math.max(0,Math.min(r,d)),m=a,h=p,g=(e,t)=>{let n=e/u;if(l)return n;let r=Math.abs(p-t);return i?n+r/i:r?1:n},_=s>1||c,v=_?Array(d):[],y;for(;(y=e.indexOf(t,h))>-1;){let e=g(0,y);if(m=Math.min(e,m),h=y+u,_){let e=0;for(;e=a;--r){let i=r-1,o=n[e[i]];if(c[r]=(c[r+1]<<1|1)&o,t&&(c[r]|=(b[r+1]|b[r])<<1|1|b[r+1]),c[r]&w&&(x=g(t,i),x<=m)){if(m=x,h=i,S=t,h<=p)break;a=Math.max(1,2*p-h)}}if(g(t+1,p)>m)break;b=c}if(_&&h>=0){let t=Math.min(d-1,h+u-1+S);for(let r=h;r<=t;r+=1)n[e[r]]&&(v[r]=1)}let E={isMatch:h>=0,score:Math.max(.001,x)};if(_){let e=A(v,s);e.length?c&&(E.indices=e):E.isMatch=!1}return E}function M(e){let t={};for(let n=0,r=e.length;ne[0]-t[0]||e[1]-t[1]);let t=[e[0]];for(let n=1,r=e.length;ne.normalize(`NFD`).replace(/[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]/g,``).replace(F,e=>P[e]):e=>e;var L=class{constructor(e,{location:t=T.location,threshold:n=T.threshold,distance:r=T.distance,includeMatches:i=T.includeMatches,findAllMatches:a=T.findAllMatches,minMatchCharLength:o=T.minMatchCharLength,isCaseSensitive:s=T.isCaseSensitive,ignoreDiacritics:c=T.ignoreDiacritics,ignoreLocation:l=T.ignoreLocation}={}){if(this.options={location:t,threshold:n,distance:r,includeMatches:i,findAllMatches:a,minMatchCharLength:o,isCaseSensitive:s,ignoreDiacritics:c,ignoreLocation:l},e=s?e:e.toLowerCase(),e=c?I(e):e,this.pattern=e,this.chunks=[],!this.pattern.length)return;let u=(e,t)=>{this.chunks.push({pattern:e,alphabet:M(e),startIndex:t})},d=this.pattern.length;if(d>32){let e=0,t=d%32,n=d-t;for(;e{let{isMatch:m,score:h,indices:g}=j(e,t,n,{location:i+p,distance:a,threshold:o,findAllMatches:s,minMatchCharLength:c,includeMatches:r,ignoreLocation:l});m&&(f=!0),d+=h,m&&g&&u.push(...g)});let p={isMatch:f,score:f?d/this.chunks.length:1};return f&&r&&(p.indices=N(u)),p}};const R=[];function z(...e){R.push(...e)}function B(e,t){for(let n=0,r=R.length;n{let a=e?e.weight:null;n*=(i===0&&a?2**-52:i)**+((a||1)*(t?1:r))}),n}function H(e,{ignoreFieldNorm:t=T.ignoreFieldNorm}){e.forEach(e=>{e.score=V(e.matches,{ignoreFieldNorm:t})})}var U=class{constructor(e){this.limit=e,this.heap=[]}get size(){return this.heap.length}shouldInsert(e){return this.size0;){let n=e-1>>1;if(t[e].score<=t[n].score)break;let r=t[e];t[e]=t[n],t[n]=r,e=n}}_sinkDown(e){let t=this.heap,n=t.length,r=e;do{e=r;let i=2*e+1,a=2*e+2;if(it[r].score&&(r=i),at[r].score&&(r=a),r!==e){let n=t[e];t[e]=t[r],t[r]=n}}while(r!==e)}};function W(e){let t=[];return e.matches.forEach(e=>{if(!c(e.indices)||!e.indices.length)return;let n={indices:e.indices,value:e.value};e.key&&(n.key=e.key.id),e.idx>-1&&(n.refIndex=e.idx),t.push(n)}),t}function G(e,t,{includeMatches:n=T.includeMatches,includeScore:r=T.includeScore}={}){return e.map(e=>{let{idx:i}=e,a={item:t[i],refIndex:i};return n&&(a.matches=W(e)),r&&(a.score=e.score),a})}const K=/[\p{L}\p{M}\p{N}_]+/gu;function q(e){return typeof e==`function`?t=>e(t):e instanceof RegExp?(e.global,t=>t.match(e)||[]):e=>e.match(K)||[]}function J({isCaseSensitive:e=!1,ignoreDiacritics:t=!1,tokenize:n}={}){let r=q(n);return{tokenize(n){return e||(n=n.toLowerCase()),t&&(n=I(n)),r(n)}}}function Y(e,t,n,r){let i=r.tokenize(t);if(!i.length)return;e.fieldCount++,e.docFieldCount.set(n,(e.docFieldCount.get(n)||0)+1);let a=new Set(i),o=e.docTermFieldHits.get(n);o||(o=new Map,e.docTermFieldHits.set(n,o));for(let t of a)o.set(t,(o.get(t)||0)+1),e.df.set(t,(e.df.get(t)||0)+1)}function X(e,t,n,r){let{i,v:a,$:o}=t;if(a!==void 0){Y(e,a,i,r);return}if(o)for(let t=0;te-t);for(let t of n)te(e,t);let r=e=>{let t=0,r=n.length;for(;t>>1;n[i]i?r(t):t,n);e.docFieldCount=a;let o=new Map;for(let[t,n]of e.docTermFieldHits)o.set(t>i?r(t):t,n);e.docTermFieldHits=o}var $=class{constructor(e,t,n){if(this.options={...T,...t},this.options.useExtendedSearch)throw Error(`Extended search is not available`);if(this.options.useTokenSearch)throw Error(`Token search is not available`);this._keyStore=new g(this.options.keys),this._docs=e,this._myIndex=null,this._invertedIndex=null,this.setCollection(e,n),this._lastQuery=null,this._lastSearcher=null}_getSearcher(e){if(this._lastQuery===e)return this._lastSearcher;let t=B(e,this._invertedIndex?{...this.options,_invertedIndex:this._invertedIndex}:this.options);return this._lastQuery=e,this._lastSearcher=t,t}setCollection(e,t){if(this._docs=e,t&&!(t instanceof D))throw Error(`Incorrect 'index' type`);if(this._myIndex=t||O(this.options.keys,this._docs,{getFn:this.options.getFn,fieldNormWeight:this.options.fieldNormWeight}),this.options.useTokenSearch){let e=J({isCaseSensitive:this.options.isCaseSensitive,ignoreDiacritics:this.options.ignoreDiacritics,tokenize:this.options.tokenize});this._invertedIndex=Z(this._myIndex.records,this._myIndex.keys.length,e)}this._invalidateSearcherCache()}add(e){if(!c(e))return;this._docs.push(e);let t=this._myIndex.add(e,this._docs.length-1);if(this._invertedIndex&&t){let e=J({isCaseSensitive:this.options.isCaseSensitive,ignoreDiacritics:this.options.ignoreDiacritics,tokenize:this.options.tokenize});ee(this._invertedIndex,t,this._myIndex.keys.length,e)}this._invalidateSearcherCache()}remove(e=()=>!1){let t=[],n=[];for(let r=0,i=this._docs.length;r!e.has(n)),this._myIndex.removeAll(n),this._invalidateSearcherCache()}return t}removeAt(e){if(!Number.isInteger(e)||e<0||e>=this._docs.length)throw Error(d);this._invertedIndex&&Q(this._invertedIndex,[e]);let t=this._docs.splice(e,1)[0];return this._myIndex.removeAt(e),this._invalidateSearcherCache(),t}_invalidateSearcherCache(){this._lastQuery=null,this._lastSearcher=null}getIndex(){return this._myIndex}search(e,t){let{limit:n=-1}=t||{},{includeMatches:a,includeScore:o,shouldSort:s,sortFn:c,ignoreFieldNorm:l}=this.options;if(r(e)&&!e.trim()){let e=this._docs.map((e,t)=>({item:e,refIndex:t}));return i(n)&&n>-1&&(e=e.slice(0,n)),e}let u=i(n)&&n>0&&r(e),d;if(u){let t=new U(n);r(this._docs[0])?this._searchStringList(e,{heap:t,ignoreFieldNorm:l}):this._searchObjectList(e,{heap:t,ignoreFieldNorm:l}),d=t.extractSorted(c)}else d=r(e)?r(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e),H(d,{ignoreFieldNorm:l}),s&&d.sort(c),i(n)&&n>-1&&(d=d.slice(0,n));return G(d,this._docs,{includeMatches:a,includeScore:o})}_searchStringList(e,{heap:t,ignoreFieldNorm:n}={}){let r=this._getSearcher(e),i=this.options.useTokenSearch&&this.options.tokenMatch===`all`,{records:a}=this._myIndex,o=t?null:[];return a.forEach(({v:e,i:a,n:s})=>{if(!c(e))return;let l=r.searchIn(e);if(l.isMatch){let r={score:l.score,value:e,norm:s,indices:l.indices};i&&(r.matchedMask=l.matchedMask,r.matchedTerms=l.matchedTerms,r.termCount=l.termCount);let c=[r];if(!i||this._coversAllTokens(c)){let r={item:e,idx:a,matches:c};t?(r.score=V(r.matches,{ignoreFieldNorm:n}),t.shouldInsert(r.score)&&t.insert(r)):o.push(r)}}}),o}_searchLogical(e){throw Error(`Logical search is not available`)}_searchObjectList(e,{heap:t,ignoreFieldNorm:n}={}){let r=this._getSearcher(e),i=this.options.useTokenSearch&&this.options.tokenMatch===`all`,{keys:a,records:o}=this._myIndex,s=t?null:[];return o.forEach(({$:e,i:o})=>{if(!c(e))return;let l=[],u=!1,d=!1;if(a.forEach((t,n)=>{let i=this._findMatches({key:t,value:e[n],searcher:r});i.length?(l.push(...i),i[0].hasInverse&&(d=!0)):u=!0}),!(d&&u)&&l.length&&(!i||this._coversAllTokens(l))){let r={idx:o,item:e,matches:l};t?(r.score=V(r.matches,{ignoreFieldNorm:n}),t.shouldInsert(r.score)&&t.insert(r)):s.push(r)}}),s}_findMatches({key:t,value:n,searcher:r}){if(!c(n))return[];let i=[];if(e(n))n.forEach(({v:e,i:n,n:a})=>{if(!c(e))return;let o=r.searchIn(e);if(o.isMatch){let r={score:o.score,key:t,value:e,idx:n,norm:a,indices:o.indices,hasInverse:o.hasInverse};o.termCount!==void 0&&(r.matchedMask=o.matchedMask,r.matchedTerms=o.matchedTerms,r.termCount=o.termCount),i.push(r)}});else{let{v:e,n:a}=n,o=r.searchIn(e);if(o.isMatch){let n={score:o.score,key:t,value:e,norm:a,indices:o.indices,hasInverse:o.hasInverse};o.termCount!==void 0&&(n.matchedMask=o.matchedMask,n.matchedTerms=o.matchedTerms,n.termCount=o.termCount),i.push(n)}}return i}_coversAllTokens(e){let t=e.length?e[0].termCount:void 0;if(t===void 0)return!0;if(t<=31){let n=0;for(let t=0;tz(e))};var ne=$;module.exports=ne; \ No newline at end of file diff --git a/dist/fuse.basic.min.mjs b/dist/fuse.basic.min.mjs index 423aa3475..54c8fae81 100644 --- a/dist/fuse.basic.min.mjs +++ b/dist/fuse.basic.min.mjs @@ -6,4 +6,4 @@ * * http://www.apache.org/licenses/LICENSE-2.0 */ -function e(e){return Array.isArray?Array.isArray(e):"[object Array]"===u(e)}function t(e){return null==e?"":function(e){if("string"==typeof e)return e;if("bigint"==typeof e)return e.toString();const t=e+"";return"0"==t&&1/e==-1/0?"-0":t}(e)}function s(e){return"string"==typeof e}function n(e){return"number"==typeof e}function i(e){return!0===e||!1===e||function(e){return function(e){return"object"==typeof e}(e)&&null!==e}(e)&&"[object Boolean]"==u(e)}function r(e){return null!=e}function o(e){return!e.trim().length}function u(e){return null==e?void 0===e?"[object Undefined]":"[object Null]":Object.prototype.toString.call(e)}const c="Invalid doc index: must be a non-negative integer within the bounds of the docs array",h=Object.prototype.hasOwnProperty;class a{constructor(e){this._keys=[],this._keyMap={};let t=0;e.forEach(e=>{const s=l(e);this._keys.push(s),this._keyMap[s.id]=s,t+=s.weight}),this._keys.forEach(e=>{e.weight/=t})}get(e){return this._keyMap[e]}keys(){return this._keys}toJSON(){return JSON.stringify(this._keys)}}function l(t){let n=null,i=null,r=null,o=1,u=null;if(s(t)||e(t))r=t,n=d(t),i=f(t);else{if(!h.call(t,"name"))throw new Error((e=>`Missing ${e} property in key`)("name"));const e=t.name;if(r=e,h.call(t,"weight")&&void 0!==t.weight&&(o=t.weight,o<=0))throw new Error((e=>`Property 'weight' in key '${e}' must be a positive integer`)(f(e)));n=d(e),i=f(e),u=t.getFn??null}return{path:n,id:i,weight:o,src:r,getFn:u}}function d(t){return e(t)?t:t.split(".")}function f(t){return e(t)?t.join("."):t}const g={useExtendedSearch:!1,useTokenSearch:!1,tokenize:void 0,tokenMatch:"any",getFn:function(o,u){const c=[];let h=!1;const a=(o,u,l,d)=>{if(r(o))if(u[l]){const f=o[u[l]];if(!r(f))return;if(l===u.length-1&&(s(f)||n(f)||i(f)||"bigint"==typeof f))c.push(void 0!==d?{v:t(f),i:d}:t(f));else if(e(f)){h=!0;for(let e=0,t=f.length;ee.score===t.score?e.idx{this._keysMap[e.id]=t})}create(){if(this.isCreated||!this.docs.length)return;this.isCreated=!0;const e=this.docs.length;this.records=new Array(e);let t=0;if(s(this.docs[0]))for(let s=0;se&&(this.records[t].i-=1)}removeAll(e){const t=new Set;for(const s of e)Number.isInteger(s)&&s>=0&&t.add(s);if(0===t.size)return;this.records=this.records.filter(e=>!t.has(e.i));const s=Array.from(t).sort((e,t)=>e-t);for(const e of this.records){let t=0,n=s.length;for(;t>>1;s[i]t),records:this.records}}}function C(e,t,{getFn:s=A.getFn,fieldNormWeight:n=A.fieldNormWeight}={}){const i=new m({getFn:s,fieldNormWeight:n});return i.setKeys(e.map(l)),i.setSources(t),i.create(),i}const p=32;function F(e,t,s,{location:n=A.location,distance:i=A.distance,threshold:r=A.threshold,findAllMatches:o=A.findAllMatches,minMatchCharLength:u=A.minMatchCharLength,includeMatches:c=A.includeMatches,ignoreLocation:h=A.ignoreLocation}={}){if(t.length>p)throw new Error(`Pattern length exceeds max of ${p}.`);const a=t.length,l=e.length,d=Math.max(0,Math.min(n,l));let f=r,g=d;const m=(e,t)=>{const s=e/a;if(h)return s;const n=Math.abs(d-t);return i?s+n/i:n?1:s},C=u>1||c,F=C?Array(l):[];let E;for(;(E=e.indexOf(t,g))>-1;){const e=m(0,E);if(f=Math.min(e,f),g=E+a,C){let e=0;for(;e=r;n-=1){const i=n-1,o=s[e[i]];if(c[n]=(c[n+1]<<1|1)&o,t&&(c[n]|=(_[n+1]|_[n])<<1|1|_[n+1]),c[n]&v&&(D=m(t,i),D<=f)){if(f=D,g=i,y=t,g<=d)break;r=Math.max(1,2*d-g)}}if(m(t+1,d)>f)break;_=c}if(C&&g>=0){const t=Math.min(l-1,g+a-1+y);for(let n=g;n<=t;n+=1)s[e[n]]&&(F[n]=1)}const M={isMatch:g>=0,score:Math.max(.001,D)};if(C){const e=function(e=[],t=A.minMatchCharLength){const s=[];let n=-1,i=-1,r=0;for(let o=e.length;r=t&&s.push([n,i]),n=-1)}return e[r-1]&&r-n>=t&&s.push([n,r-1]),s}(F,u);e.length?c&&(M.indices=e):M.isMatch=!1}return M}function E(e){const t={};for(let s=0,n=e.length;se.normalize("NFD").replace(/[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]/g,"").replace(D,e=>_[e]):e=>e;class B{constructor(e,{location:t=A.location,threshold:s=A.threshold,distance:n=A.distance,includeMatches:i=A.includeMatches,findAllMatches:r=A.findAllMatches,minMatchCharLength:o=A.minMatchCharLength,isCaseSensitive:u=A.isCaseSensitive,ignoreDiacritics:c=A.ignoreDiacritics,ignoreLocation:h=A.ignoreLocation}={}){if(this.options={location:t,threshold:s,distance:n,includeMatches:i,findAllMatches:r,minMatchCharLength:o,isCaseSensitive:u,ignoreDiacritics:c,ignoreLocation:h},e=u?e:e.toLowerCase(),e=c?y(e):e,this.pattern=e,this.chunks=[],!this.pattern.length)return;const a=(e,t)=>{this.chunks.push({pattern:e,alphabet:E(e),startIndex:t})},l=this.pattern.length;if(l>p){let e=0;const t=l%p,s=l-t;for(;e{const{isMatch:g,score:A,indices:m}=F(e,t,s,{location:i+f,distance:r,threshold:o,findAllMatches:u,minMatchCharLength:c,includeMatches:n,ignoreLocation:h});g&&(d=!0),l+=A,g&&m&&a.push(...m)});const f={isMatch:d,score:d?l/this.chunks.length:1};return d&&n&&(f.indices=function(e){if(e.length<=1)return e;e.sort((e,t)=>e[0]-t[0]||e[1]-t[1]);const t=[e[0]];for(let s=1,n=e.length;s{const r=e?e.weight:null;s*=Math.pow(0===i&&r?Number.EPSILON:i,(r||1)*(t?1:n))}),s}class w{constructor(e){this.limit=e,this.heap=[]}get size(){return this.heap.length}shouldInsert(e){return this.size0;){const s=e-1>>1;if(t[e].score<=t[s].score)break;const n=t[e];t[e]=t[s],t[s]=n,e=s}}_sinkDown(e){const t=this.heap,s=t.length;let n=e;do{const i=2*(e=n)+1,r=2*e+2;if(it[n].score&&(n=i),rt[n].score&&(n=r),n!==e){const s=t[e];t[e]=t[n],t[n]=s}}while(n!==e)}}function S(e,t,{includeMatches:s=A.includeMatches,includeScore:n=A.includeScore}={}){return e.map(e=>{const{idx:i}=e,o={item:t[i],refIndex:i};return s&&(o.matches=function(e){const t=[];return e.matches.forEach(e=>{if(!r(e.indices)||!e.indices.length)return;const s={indices:e.indices,value:e.value};e.key&&(s.key=e.key.id),e.idx>-1&&(s.refIndex=e.idx),t.push(s)}),t}(e)),n&&(o.score=e.score),o})}const x=/[\p{L}\p{M}\p{N}_]+/gu;function I({isCaseSensitive:e=!1,ignoreDiacritics:t=!1,tokenize:s}={}){const n=function(e){return"function"==typeof e?t=>e(t):e instanceof RegExp?(e.global,t=>t.match(e)||[]):e=>e.match(x)||[]}(s);return{tokenize:s=>(e||(s=s.toLowerCase()),t&&(s=y(s)),n(s))}}function b(e,t,s,n){const i=n.tokenize(t);if(!i.length)return;e.fieldCount++,e.docFieldCount.set(s,(e.docFieldCount.get(s)||0)+1);const r=new Set(i);let o=e.docTermFieldHits.get(s);o||(o=new Map,e.docTermFieldHits.set(s,o));for(const t of r)o.set(t,(o.get(t)||0)+1),e.df.set(t,(e.df.get(t)||0)+1)}function N(e,t,s,n){const{i:i,v:r,$:o}=t;if(void 0===r){if(o)for(let t=0;te-t);for(const t of s)L(e,t);const n=e=>{let t=0,n=s.length;for(;t>>1;s[i]i?n(t):t,s);e.docFieldCount=r;const o=new Map;for(const[t,s]of e.docTermFieldHits)o.set(t>i?n(t):t,s);e.docTermFieldHits=o}class z{constructor(e,t,s){if(this.options={...A,...t},this.options.useExtendedSearch)throw new Error("Extended search is not available");if(this.options.useTokenSearch)throw new Error("Token search is not available");this._keyStore=new a(this.options.keys),this._docs=e,this._myIndex=null,this._invertedIndex=null,this.setCollection(e,s),this._lastQuery=null,this._lastSearcher=null}_getSearcher(e){if(this._lastQuery===e)return this._lastSearcher;const t=M(e,this._invertedIndex?{...this.options,_invertedIndex:this._invertedIndex}:this.options);return this._lastQuery=e,this._lastSearcher=t,t}setCollection(e,t){if(this._docs=e,t&&!(t instanceof m))throw new Error("Incorrect 'index' type");if(this._myIndex=t||C(this.options.keys,this._docs,{getFn:this.options.getFn,fieldNormWeight:this.options.fieldNormWeight}),this.options.useTokenSearch){const e=I({isCaseSensitive:this.options.isCaseSensitive,ignoreDiacritics:this.options.ignoreDiacritics,tokenize:this.options.tokenize});this._invertedIndex=function(e,t,s){const n={fieldCount:0,df:new Map,docFieldCount:new Map,docTermFieldHits:new Map};for(const i of e)N(n,i,t,s);return n}(this._myIndex.records,this._myIndex.keys.length,e)}this._invalidateSearcherCache()}add(e){if(!r(e))return;this._docs.push(e);const t=this._myIndex.add(e,this._docs.length-1);if(this._invertedIndex&&t){const e=I({isCaseSensitive:this.options.isCaseSensitive,ignoreDiacritics:this.options.ignoreDiacritics,tokenize:this.options.tokenize});!function(e,t,s,n){N(e,t,s,n)}(this._invertedIndex,t,this._myIndex.keys.length,e)}this._invalidateSearcherCache()}remove(e=()=>!1){const t=[],s=[];for(let n=0,i=this._docs.length;n!e.has(s)),this._myIndex.removeAll(s),this._invalidateSearcherCache()}return t}removeAt(e){if(!Number.isInteger(e)||e<0||e>=this._docs.length)throw new Error(c);this._invertedIndex&&T(this._invertedIndex,[e]);const t=this._docs.splice(e,1)[0];return this._myIndex.removeAt(e),this._invalidateSearcherCache(),t}_invalidateSearcherCache(){this._lastQuery=null,this._lastSearcher=null}getIndex(){return this._myIndex}search(e,t){const{limit:i=-1}=t||{},{includeMatches:r,includeScore:o,shouldSort:u,sortFn:c,ignoreFieldNorm:h}=this.options;if(s(e)&&!e.trim()){let e=this._docs.map((e,t)=>({item:e,refIndex:t}));return n(i)&&i>-1&&(e=e.slice(0,i)),e}let a;if(n(i)&&i>0&&s(e)){const t=new w(i);s(this._docs[0])?this._searchStringList(e,{heap:t,ignoreFieldNorm:h}):this._searchObjectList(e,{heap:t,ignoreFieldNorm:h}),a=t.extractSorted(c)}else a=s(e)?s(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e),function(e,{ignoreFieldNorm:t=A.ignoreFieldNorm}){e.forEach(e=>{e.score=k(e.matches,{ignoreFieldNorm:t})})}(a,{ignoreFieldNorm:h}),u&&a.sort(c),n(i)&&i>-1&&(a=a.slice(0,i));return S(a,this._docs,{includeMatches:r,includeScore:o})}_searchStringList(e,{heap:t,ignoreFieldNorm:s}={}){const n=this._getSearcher(e),i=this.options.useTokenSearch&&"all"===this.options.tokenMatch,{records:o}=this._myIndex,u=t?null:[];return o.forEach(({v:e,i:o,n:c})=>{if(!r(e))return;const h=n.searchIn(e);if(h.isMatch){const n={score:h.score,value:e,norm:c,indices:h.indices};i&&(n.matchedMask=h.matchedMask,n.matchedTerms=h.matchedTerms,n.termCount=h.termCount);const r=[n];if(!i||this._coversAllTokens(r)){const n={item:e,idx:o,matches:r};t?(n.score=k(n.matches,{ignoreFieldNorm:s}),t.shouldInsert(n.score)&&t.insert(n)):u.push(n)}}}),u}_searchLogical(e){throw new Error("Logical search is not available")}_searchObjectList(e,{heap:t,ignoreFieldNorm:s}={}){const n=this._getSearcher(e),i=this.options.useTokenSearch&&"all"===this.options.tokenMatch,{keys:o,records:u}=this._myIndex,c=t?null:[];return u.forEach(({$:e,i:u})=>{if(!r(e))return;const h=[];let a=!1,l=!1;if(o.forEach((t,s)=>{const i=this._findMatches({key:t,value:e[s],searcher:n});i.length?(h.push(...i),i[0].hasInverse&&(l=!0)):a=!0}),(!l||!a)&&h.length&&(!i||this._coversAllTokens(h))){const n={idx:u,item:e,matches:h};t?(n.score=k(n.matches,{ignoreFieldNorm:s}),t.shouldInsert(n.score)&&t.insert(n)):c.push(n)}}),c}_findMatches({key:t,value:s,searcher:n}){if(!r(s))return[];const i=[];if(e(s))s.forEach(({v:e,i:s,n:o})=>{if(!r(e))return;const u=n.searchIn(e);if(u.isMatch){const n={score:u.score,key:t,value:e,idx:s,norm:o,indices:u.indices,hasInverse:u.hasInverse};void 0!==u.termCount&&(n.matchedMask=u.matchedMask,n.matchedTerms=u.matchedTerms,n.termCount=u.termCount),i.push(n)}});else{const{v:e,n:r}=s,o=n.searchIn(e);if(o.isMatch){const s={score:o.score,key:t,value:e,norm:r,indices:o.indices,hasInverse:o.hasInverse};void 0!==o.termCount&&(s.matchedMask=o.matchedMask,s.matchedTerms=o.matchedTerms,s.termCount=o.termCount),i.push(s)}}return i}_coversAllTokens(e){const t=e.length?e[0].termCount:void 0;if(void 0===t)return!0;if(t<=31){let s=0;for(let t=0;tfunction(...e){v.push(...e)}(e))};export{z as default}; \ No newline at end of file +function e(e){return Array.isArray?Array.isArray(e):u(e)===`[object Array]`}function t(e){if(typeof e==`string`)return e;if(typeof e==`bigint`)return e.toString();let t=e+``;return t==`0`&&1/e==-1/0?`-0`:t}function n(e){return e==null?``:t(e)}function r(e){return typeof e==`string`}function i(e){return typeof e==`number`}function a(e){return e===!0||e===!1||s(e)&&u(e)==`[object Boolean]`}function o(e){return typeof e==`object`}function s(e){return o(e)&&e!==null}function c(e){return e!=null}function l(e){return!e.trim().length}function u(e){return e==null?e===void 0?`[object Undefined]`:`[object Null]`:Object.prototype.toString.call(e)}const d=`Invalid doc index: must be a non-negative integer within the bounds of the docs array`,f=e=>`Pattern length exceeds max of ${e}.`,p=e=>`Missing ${e} property in key`,m=e=>`Property 'weight' in key '${e}' must be a positive integer`,h=Object.prototype.hasOwnProperty;var g=class{constructor(e){this._keys=[],this._keyMap={};let t=0;e.forEach(e=>{let n=_(e);this._keys.push(n),this._keyMap[n.id]=n,t+=n.weight}),this._keys.forEach(e=>{e.weight/=t})}get(e){return this._keyMap[e]}keys(){return this._keys}toJSON(){return JSON.stringify(this._keys)}};function _(t){let n=null,i=null,a=null,o=1,s=null;if(r(t)||e(t))a=t,n=v(t),i=y(t);else{if(!h.call(t,`name`))throw Error(p(`name`));let e=t.name;if(a=e,h.call(t,`weight`)&&t.weight!==void 0&&(o=t.weight,o<=0))throw Error(m(y(e)));n=v(e),i=y(e),s=t.getFn??null}return{path:n,id:i,weight:o,src:a,getFn:s}}function v(t){return e(t)?t:t.split(`.`)}function y(t){return e(t)?t.join(`.`):t}function b(t,o){let s=[],l=!1,u=(t,o,d,f)=>{if(c(t))if(!o[d])s.push(f===void 0?t:{v:t,i:f});else{let p=t[o[d]];if(!c(p))return;if(d===o.length-1&&(r(p)||i(p)||a(p)||typeof p==`bigint`))s.push(f===void 0?n(p):{v:n(p),i:f});else if(e(p)){l=!0;for(let e=0,t=p.length;ee.score===t.score?e.idx{this._keysMap[e.id]=t})}create(){if(this.isCreated||!this.docs.length)return;this.isCreated=!0;let e=this.docs.length;this.records=Array(e);let t=0;if(r(this.docs[0]))for(let n=0;ne&&--this.records[t].i}removeAll(e){let t=new Set;for(let n of e)Number.isInteger(n)&&n>=0&&t.add(n);if(t.size===0)return;this.records=this.records.filter(e=>!t.has(e.i));let n=Array.from(t).sort((e,t)=>e-t);for(let e of this.records){let t=0,r=n.length;for(;t>>1;n[i]t),records:this.records}}};function O(e,t,{getFn:n=T.getFn,fieldNormWeight:r=T.fieldNormWeight}={}){let i=new D({getFn:n,fieldNormWeight:r});return i.setKeys(e.map(_)),i.setSources(t),i.create(),i}function k(e,{getFn:t=T.getFn,fieldNormWeight:n=T.fieldNormWeight}={}){let{keys:r,records:i}=e,a=new D({getFn:t,fieldNormWeight:n});return a.setKeys(r),a.setIndexRecords(i),a}function A(e=[],t=T.minMatchCharLength){let n=[],r=-1,i=-1,a=0;for(let o=e.length;a=t&&n.push([r,i]),r=-1)}return e[a-1]&&a-r>=t&&n.push([r,a-1]),n}function j(e,t,n,{location:r=T.location,distance:i=T.distance,threshold:a=T.threshold,findAllMatches:o=T.findAllMatches,minMatchCharLength:s=T.minMatchCharLength,includeMatches:c=T.includeMatches,ignoreLocation:l=T.ignoreLocation}={}){if(t.length>32)throw Error(f(32));let u=t.length,d=e.length,p=Math.max(0,Math.min(r,d)),m=a,h=p,g=(e,t)=>{let n=e/u;if(l)return n;let r=Math.abs(p-t);return i?n+r/i:r?1:n},_=s>1||c,v=_?Array(d):[],y;for(;(y=e.indexOf(t,h))>-1;){let e=g(0,y);if(m=Math.min(e,m),h=y+u,_){let e=0;for(;e=a;--r){let i=r-1,o=n[e[i]];if(c[r]=(c[r+1]<<1|1)&o,t&&(c[r]|=(b[r+1]|b[r])<<1|1|b[r+1]),c[r]&w&&(x=g(t,i),x<=m)){if(m=x,h=i,S=t,h<=p)break;a=Math.max(1,2*p-h)}}if(g(t+1,p)>m)break;b=c}if(_&&h>=0){let t=Math.min(d-1,h+u-1+S);for(let r=h;r<=t;r+=1)n[e[r]]&&(v[r]=1)}let E={isMatch:h>=0,score:Math.max(.001,x)};if(_){let e=A(v,s);e.length?c&&(E.indices=e):E.isMatch=!1}return E}function M(e){let t={};for(let n=0,r=e.length;ne[0]-t[0]||e[1]-t[1]);let t=[e[0]];for(let n=1,r=e.length;ne.normalize(`NFD`).replace(/[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]/g,``).replace(F,e=>P[e]):e=>e;var L=class{constructor(e,{location:t=T.location,threshold:n=T.threshold,distance:r=T.distance,includeMatches:i=T.includeMatches,findAllMatches:a=T.findAllMatches,minMatchCharLength:o=T.minMatchCharLength,isCaseSensitive:s=T.isCaseSensitive,ignoreDiacritics:c=T.ignoreDiacritics,ignoreLocation:l=T.ignoreLocation}={}){if(this.options={location:t,threshold:n,distance:r,includeMatches:i,findAllMatches:a,minMatchCharLength:o,isCaseSensitive:s,ignoreDiacritics:c,ignoreLocation:l},e=s?e:e.toLowerCase(),e=c?I(e):e,this.pattern=e,this.chunks=[],!this.pattern.length)return;let u=(e,t)=>{this.chunks.push({pattern:e,alphabet:M(e),startIndex:t})},d=this.pattern.length;if(d>32){let e=0,t=d%32,n=d-t;for(;e{let{isMatch:m,score:h,indices:g}=j(e,t,n,{location:i+p,distance:a,threshold:o,findAllMatches:s,minMatchCharLength:c,includeMatches:r,ignoreLocation:l});m&&(f=!0),d+=h,m&&g&&u.push(...g)});let p={isMatch:f,score:f?d/this.chunks.length:1};return f&&r&&(p.indices=N(u)),p}};const R=[];function z(...e){R.push(...e)}function B(e,t){for(let n=0,r=R.length;n{let a=e?e.weight:null;n*=(i===0&&a?2**-52:i)**+((a||1)*(t?1:r))}),n}function H(e,{ignoreFieldNorm:t=T.ignoreFieldNorm}){e.forEach(e=>{e.score=V(e.matches,{ignoreFieldNorm:t})})}var U=class{constructor(e){this.limit=e,this.heap=[]}get size(){return this.heap.length}shouldInsert(e){return this.size0;){let n=e-1>>1;if(t[e].score<=t[n].score)break;let r=t[e];t[e]=t[n],t[n]=r,e=n}}_sinkDown(e){let t=this.heap,n=t.length,r=e;do{e=r;let i=2*e+1,a=2*e+2;if(it[r].score&&(r=i),at[r].score&&(r=a),r!==e){let n=t[e];t[e]=t[r],t[r]=n}}while(r!==e)}};function W(e){let t=[];return e.matches.forEach(e=>{if(!c(e.indices)||!e.indices.length)return;let n={indices:e.indices,value:e.value};e.key&&(n.key=e.key.id),e.idx>-1&&(n.refIndex=e.idx),t.push(n)}),t}function G(e,t,{includeMatches:n=T.includeMatches,includeScore:r=T.includeScore}={}){return e.map(e=>{let{idx:i}=e,a={item:t[i],refIndex:i};return n&&(a.matches=W(e)),r&&(a.score=e.score),a})}const K=/[\p{L}\p{M}\p{N}_]+/gu;function q(e){return typeof e==`function`?t=>e(t):e instanceof RegExp?(e.global,t=>t.match(e)||[]):e=>e.match(K)||[]}function J({isCaseSensitive:e=!1,ignoreDiacritics:t=!1,tokenize:n}={}){let r=q(n);return{tokenize(n){return e||(n=n.toLowerCase()),t&&(n=I(n)),r(n)}}}function Y(e,t,n,r){let i=r.tokenize(t);if(!i.length)return;e.fieldCount++,e.docFieldCount.set(n,(e.docFieldCount.get(n)||0)+1);let a=new Set(i),o=e.docTermFieldHits.get(n);o||(o=new Map,e.docTermFieldHits.set(n,o));for(let t of a)o.set(t,(o.get(t)||0)+1),e.df.set(t,(e.df.get(t)||0)+1)}function X(e,t,n,r){let{i,v:a,$:o}=t;if(a!==void 0){Y(e,a,i,r);return}if(o)for(let t=0;te-t);for(let t of n)te(e,t);let r=e=>{let t=0,r=n.length;for(;t>>1;n[i]i?r(t):t,n);e.docFieldCount=a;let o=new Map;for(let[t,n]of e.docTermFieldHits)o.set(t>i?r(t):t,n);e.docTermFieldHits=o}var $=class{constructor(e,t,n){if(this.options={...T,...t},this.options.useExtendedSearch)throw Error(`Extended search is not available`);if(this.options.useTokenSearch)throw Error(`Token search is not available`);this._keyStore=new g(this.options.keys),this._docs=e,this._myIndex=null,this._invertedIndex=null,this.setCollection(e,n),this._lastQuery=null,this._lastSearcher=null}_getSearcher(e){if(this._lastQuery===e)return this._lastSearcher;let t=B(e,this._invertedIndex?{...this.options,_invertedIndex:this._invertedIndex}:this.options);return this._lastQuery=e,this._lastSearcher=t,t}setCollection(e,t){if(this._docs=e,t&&!(t instanceof D))throw Error(`Incorrect 'index' type`);if(this._myIndex=t||O(this.options.keys,this._docs,{getFn:this.options.getFn,fieldNormWeight:this.options.fieldNormWeight}),this.options.useTokenSearch){let e=J({isCaseSensitive:this.options.isCaseSensitive,ignoreDiacritics:this.options.ignoreDiacritics,tokenize:this.options.tokenize});this._invertedIndex=Z(this._myIndex.records,this._myIndex.keys.length,e)}this._invalidateSearcherCache()}add(e){if(!c(e))return;this._docs.push(e);let t=this._myIndex.add(e,this._docs.length-1);if(this._invertedIndex&&t){let e=J({isCaseSensitive:this.options.isCaseSensitive,ignoreDiacritics:this.options.ignoreDiacritics,tokenize:this.options.tokenize});ee(this._invertedIndex,t,this._myIndex.keys.length,e)}this._invalidateSearcherCache()}remove(e=()=>!1){let t=[],n=[];for(let r=0,i=this._docs.length;r!e.has(n)),this._myIndex.removeAll(n),this._invalidateSearcherCache()}return t}removeAt(e){if(!Number.isInteger(e)||e<0||e>=this._docs.length)throw Error(d);this._invertedIndex&&Q(this._invertedIndex,[e]);let t=this._docs.splice(e,1)[0];return this._myIndex.removeAt(e),this._invalidateSearcherCache(),t}_invalidateSearcherCache(){this._lastQuery=null,this._lastSearcher=null}getIndex(){return this._myIndex}search(e,t){let{limit:n=-1}=t||{},{includeMatches:a,includeScore:o,shouldSort:s,sortFn:c,ignoreFieldNorm:l}=this.options;if(r(e)&&!e.trim()){let e=this._docs.map((e,t)=>({item:e,refIndex:t}));return i(n)&&n>-1&&(e=e.slice(0,n)),e}let u=i(n)&&n>0&&r(e),d;if(u){let t=new U(n);r(this._docs[0])?this._searchStringList(e,{heap:t,ignoreFieldNorm:l}):this._searchObjectList(e,{heap:t,ignoreFieldNorm:l}),d=t.extractSorted(c)}else d=r(e)?r(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e),H(d,{ignoreFieldNorm:l}),s&&d.sort(c),i(n)&&n>-1&&(d=d.slice(0,n));return G(d,this._docs,{includeMatches:a,includeScore:o})}_searchStringList(e,{heap:t,ignoreFieldNorm:n}={}){let r=this._getSearcher(e),i=this.options.useTokenSearch&&this.options.tokenMatch===`all`,{records:a}=this._myIndex,o=t?null:[];return a.forEach(({v:e,i:a,n:s})=>{if(!c(e))return;let l=r.searchIn(e);if(l.isMatch){let r={score:l.score,value:e,norm:s,indices:l.indices};i&&(r.matchedMask=l.matchedMask,r.matchedTerms=l.matchedTerms,r.termCount=l.termCount);let c=[r];if(!i||this._coversAllTokens(c)){let r={item:e,idx:a,matches:c};t?(r.score=V(r.matches,{ignoreFieldNorm:n}),t.shouldInsert(r.score)&&t.insert(r)):o.push(r)}}}),o}_searchLogical(e){throw Error(`Logical search is not available`)}_searchObjectList(e,{heap:t,ignoreFieldNorm:n}={}){let r=this._getSearcher(e),i=this.options.useTokenSearch&&this.options.tokenMatch===`all`,{keys:a,records:o}=this._myIndex,s=t?null:[];return o.forEach(({$:e,i:o})=>{if(!c(e))return;let l=[],u=!1,d=!1;if(a.forEach((t,n)=>{let i=this._findMatches({key:t,value:e[n],searcher:r});i.length?(l.push(...i),i[0].hasInverse&&(d=!0)):u=!0}),!(d&&u)&&l.length&&(!i||this._coversAllTokens(l))){let r={idx:o,item:e,matches:l};t?(r.score=V(r.matches,{ignoreFieldNorm:n}),t.shouldInsert(r.score)&&t.insert(r)):s.push(r)}}),s}_findMatches({key:t,value:n,searcher:r}){if(!c(n))return[];let i=[];if(e(n))n.forEach(({v:e,i:n,n:a})=>{if(!c(e))return;let o=r.searchIn(e);if(o.isMatch){let r={score:o.score,key:t,value:e,idx:n,norm:a,indices:o.indices,hasInverse:o.hasInverse};o.termCount!==void 0&&(r.matchedMask=o.matchedMask,r.matchedTerms=o.matchedTerms,r.termCount=o.termCount),i.push(r)}});else{let{v:e,n:a}=n,o=r.searchIn(e);if(o.isMatch){let n={score:o.score,key:t,value:e,norm:a,indices:o.indices,hasInverse:o.hasInverse};o.termCount!==void 0&&(n.matchedMask=o.matchedMask,n.matchedTerms=o.matchedTerms,n.termCount=o.termCount),i.push(n)}}return i}_coversAllTokens(e){let t=e.length?e[0].termCount:void 0;if(t===void 0)return!0;if(t<=31){let n=0;for(let t=0;tz(e))};var ne=$;export{ne as default}; \ No newline at end of file diff --git a/dist/fuse.basic.mjs b/dist/fuse.basic.mjs index d3ffad71c..2eb2cea72 100644 --- a/dist/fuse.basic.mjs +++ b/dist/fuse.basic.mjs @@ -6,1676 +6,1178 @@ * * http://www.apache.org/licenses/LICENSE-2.0 */ - +//#region src/helpers/typeGuards.ts function isArray(value) { - return !Array.isArray ? getTag(value) === '[object Array]' : Array.isArray(value); + return !Array.isArray ? getTag(value) === "[object Array]" : Array.isArray(value); } function baseToString(value) { - // Exit early for strings to avoid a performance hit in some environments. - if (typeof value == 'string') { - return value; - } - if (typeof value === 'bigint') { - return value.toString(); - } - const result = value + ''; - return result == '0' && 1 / value == -Infinity ? '-0' : result; + if (typeof value == "string") return value; + if (typeof value === "bigint") return value.toString(); + const result = value + ""; + return result == "0" && 1 / value == -Infinity ? "-0" : result; } function toString(value) { - return value == null ? '' : baseToString(value); + return value == null ? "" : baseToString(value); } function isString(value) { - return typeof value === 'string'; + return typeof value === "string"; } function isNumber(value) { - return typeof value === 'number'; + return typeof value === "number"; } - -// Adapted from: https://github.com/lodash/lodash/blob/master/isBoolean.js function isBoolean(value) { - return value === true || value === false || isObjectLike(value) && getTag(value) == '[object Boolean]'; + return value === true || value === false || isObjectLike(value) && getTag(value) == "[object Boolean]"; } function isObject(value) { - return typeof value === 'object'; + return typeof value === "object"; } - -// Checks if `value` is object-like. function isObjectLike(value) { - return isObject(value) && value !== null; + return isObject(value) && value !== null; } function isDefined(value) { - return value !== undefined && value !== null; + return value !== void 0 && value !== null; } function isBlank(value) { - return !value.trim().length; + return !value.trim().length; } - -// Gets the `toStringTag` of `value`. -// Adapted from: https://github.com/lodash/lodash/blob/master/.internal/getTag.js function getTag(value) { - return value == null ? value === undefined ? '[object Undefined]' : '[object Null]' : Object.prototype.toString.call(value); + return value == null ? value === void 0 ? "[object Undefined]" : "[object Null]" : Object.prototype.toString.call(value); } -const EXTENDED_SEARCH_UNAVAILABLE = 'Extended search is not available'; -const LOGICAL_SEARCH_UNAVAILABLE = 'Logical search is not available'; -const TOKEN_SEARCH_UNAVAILABLE = 'Token search is not available'; +//#endregion +//#region src/core/errorMessages.ts +const EXTENDED_SEARCH_UNAVAILABLE = "Extended search is not available"; +const LOGICAL_SEARCH_UNAVAILABLE = "Logical search is not available"; +const TOKEN_SEARCH_UNAVAILABLE = "Token search is not available"; const INCORRECT_INDEX_TYPE = "Incorrect 'index' type"; -const INVALID_DOC_INDEX = 'Invalid doc index: must be a non-negative integer within the bounds of the docs array'; -const LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY = key => `Invalid value for key ${key}`; -const PATTERN_LENGTH_TOO_LARGE = max => `Pattern length exceeds max of ${max}.`; -const MISSING_KEY_PROPERTY = name => `Missing ${name} property in key`; -const INVALID_KEY_WEIGHT_VALUE = key => `Property 'weight' in key '${key}' must be a positive integer`; -const FUSE_MATCH_TOKEN_SEARCH_UNSUPPORTED = `Fuse.match does not support useTokenSearch: token search requires ` + `corpus-level statistics (df, fieldCount) that a one-off string ` + `comparison does not have. Use new Fuse(...).search(...) instead.`; - +const INVALID_DOC_INDEX = "Invalid doc index: must be a non-negative integer within the bounds of the docs array"; +const LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY = (key) => `Invalid value for key ${key}`; +const PATTERN_LENGTH_TOO_LARGE = (max) => `Pattern length exceeds max of ${max}.`; +const MISSING_KEY_PROPERTY = (name) => `Missing ${name} property in key`; +const INVALID_KEY_WEIGHT_VALUE = (key) => `Property 'weight' in key '${key}' must be a positive integer`; +const FUSE_MATCH_TOKEN_SEARCH_UNSUPPORTED = "Fuse.match does not support useTokenSearch: token search requires corpus-level statistics (df, fieldCount) that a one-off string comparison does not have. Use new Fuse(...).search(...) instead."; + +//#endregion +//#region src/tools/KeyStore.ts const hasOwn = Object.prototype.hasOwnProperty; -class KeyStore { - constructor(keys) { - this._keys = []; - this._keyMap = {}; - let totalWeight = 0; - keys.forEach(key => { - const obj = createKey(key); - this._keys.push(obj); - this._keyMap[obj.id] = obj; - totalWeight += obj.weight; - }); - - // Normalize weights so that their sum is equal to 1 - this._keys.forEach(key => { - key.weight /= totalWeight; - }); - } - get(keyId) { - return this._keyMap[keyId]; - } - keys() { - return this._keys; - } - toJSON() { - return JSON.stringify(this._keys); - } -} +var KeyStore = class { + constructor(keys) { + this._keys = []; + this._keyMap = {}; + let totalWeight = 0; + keys.forEach((key) => { + const obj = createKey(key); + this._keys.push(obj); + this._keyMap[obj.id] = obj; + totalWeight += obj.weight; + }); + this._keys.forEach((key) => { + key.weight /= totalWeight; + }); + } + get(keyId) { + return this._keyMap[keyId]; + } + keys() { + return this._keys; + } + toJSON() { + return JSON.stringify(this._keys); + } +}; function createKey(key) { - let path = null; - let id = null; - let src = null; - let weight = 1; - let getFn = null; - if (isString(key) || isArray(key)) { - src = key; - path = createKeyPath(key); - id = createKeyId(key); - } else { - if (!hasOwn.call(key, 'name')) { - throw new Error(MISSING_KEY_PROPERTY('name')); - } - const name = key.name; - src = name; - if (hasOwn.call(key, 'weight') && key.weight !== undefined) { - weight = key.weight; - if (weight <= 0) { - throw new Error(INVALID_KEY_WEIGHT_VALUE(createKeyId(name))); - } - } - path = createKeyPath(name); - id = createKeyId(name); - getFn = key.getFn ?? null; - } - return { - path: path, - id: id, - weight, - src: src, - getFn - }; + let path = null; + let id = null; + let src = null; + let weight = 1; + let getFn = null; + if (isString(key) || isArray(key)) { + src = key; + path = createKeyPath(key); + id = createKeyId(key); + } else { + if (!hasOwn.call(key, "name")) throw new Error(MISSING_KEY_PROPERTY("name")); + const name = key.name; + src = name; + if (hasOwn.call(key, "weight") && key.weight !== void 0) { + weight = key.weight; + if (weight <= 0) throw new Error(INVALID_KEY_WEIGHT_VALUE(createKeyId(name))); + } + path = createKeyPath(name); + id = createKeyId(name); + getFn = key.getFn ?? null; + } + return { + path, + id, + weight, + src, + getFn + }; } function createKeyPath(key) { - return isArray(key) ? key : key.split('.'); + return isArray(key) ? key : key.split("."); } function createKeyId(key) { - return isArray(key) ? key.join('.') : key; + return isArray(key) ? key.join(".") : key; } +//#endregion +//#region src/helpers/get.ts function get(obj, path) { - const list = []; - let arr = false; - const deepGet = (obj, path, index, arrayIndex) => { - if (!isDefined(obj)) { - return; - } - if (!path[index]) { - // If there's no path left, we've arrived at the object we care about. - list.push(arrayIndex !== undefined ? { - v: obj, - i: arrayIndex - } : obj); - } else { - const key = path[index]; - const value = obj[key]; - if (!isDefined(value)) { - return; - } - - // If we're at the last value in the path, and if it's a string/number/bool, - // add it to the list - if (index === path.length - 1 && (isString(value) || isNumber(value) || isBoolean(value) || typeof value === 'bigint')) { - list.push(arrayIndex !== undefined ? { - v: toString(value), - i: arrayIndex - } : toString(value)); - } else if (isArray(value)) { - arr = true; - // Search each item in the array. - for (let i = 0, len = value.length; i < len; i += 1) { - deepGet(value[i], path, index + 1, i); - } - } else if (path.length) { - // An object. Recurse further. - deepGet(value, path, index + 1, arrayIndex); - } - } - }; - - // Backwards compatibility (since path used to be a string) - deepGet(obj, isString(path) ? path.split('.') : path, 0); - return arr ? list : list[0]; + const list = []; + let arr = false; + const deepGet = (obj, path, index, arrayIndex) => { + if (!isDefined(obj)) return; + if (!path[index]) list.push(arrayIndex !== void 0 ? { + v: obj, + i: arrayIndex + } : obj); + else { + const value = obj[path[index]]; + if (!isDefined(value)) return; + if (index === path.length - 1 && (isString(value) || isNumber(value) || isBoolean(value) || typeof value === "bigint")) list.push(arrayIndex !== void 0 ? { + v: toString(value), + i: arrayIndex + } : toString(value)); + else if (isArray(value)) { + arr = true; + for (let i = 0, len = value.length; i < len; i += 1) deepGet(value[i], path, index + 1, i); + } else if (path.length) deepGet(value, path, index + 1, arrayIndex); + } + }; + deepGet(obj, isString(path) ? path.split(".") : path, 0); + return arr ? list : list[0]; } +//#endregion +//#region src/core/config.ts const MatchOptions = { - includeMatches: false, - findAllMatches: false, - minMatchCharLength: 1 + includeMatches: false, + findAllMatches: false, + minMatchCharLength: 1 }; const BasicOptions = { - isCaseSensitive: false, - ignoreDiacritics: false, - includeScore: false, - keys: [], - shouldSort: true, - sortFn: (a, b) => a.score === b.score ? a.idx < b.idx ? -1 : 1 : a.score < b.score ? -1 : 1 + isCaseSensitive: false, + ignoreDiacritics: false, + includeScore: false, + keys: [], + shouldSort: true, + sortFn: (a, b) => a.score === b.score ? a.idx < b.idx ? -1 : 1 : a.score < b.score ? -1 : 1 }; const FuzzyOptions = { - location: 0, - threshold: 0.6, - distance: 100 + location: 0, + threshold: .6, + distance: 100 }; const AdvancedOptions = { - useExtendedSearch: false, - useTokenSearch: false, - tokenize: undefined, - tokenMatch: 'any', - getFn: get, - ignoreLocation: false, - ignoreFieldNorm: false, - fieldNormWeight: 1 + useExtendedSearch: false, + useTokenSearch: false, + tokenize: void 0, + tokenMatch: "any", + getFn: get, + ignoreLocation: false, + ignoreFieldNorm: false, + fieldNormWeight: 1 }; const Config = Object.freeze({ - ...BasicOptions, - ...MatchOptions, - ...FuzzyOptions, - ...AdvancedOptions + ...BasicOptions, + ...MatchOptions, + ...FuzzyOptions, + ...AdvancedOptions }); -// Field-length norm: the shorter the field, the higher the weight. -// Set to 3 decimals to reduce index size. +//#endregion +//#region src/tools/fieldNorm.ts function norm(weight = 1, mantissa = 3) { - const cache = new Map(); - const m = Math.pow(10, mantissa); - return { - get(value) { - // Count words by counting space transitions — avoids allocating a regex match array - let numTokens = 1; - let inSpace = false; - for (let i = 0; i < value.length; i++) { - if (value.charCodeAt(i) === 32) { - if (!inSpace) { - numTokens++; - inSpace = true; - } - } else { - inSpace = false; - } - } - if (cache.has(numTokens)) { - return cache.get(numTokens); - } - - // Default function is 1/sqrt(x), weight makes that variable - const n = Math.round(m / Math.pow(numTokens, 0.5 * weight)) / m; - cache.set(numTokens, n); - return n; - }, - clear() { - cache.clear(); - } - }; + const cache = /* @__PURE__ */ new Map(); + const m = Math.pow(10, mantissa); + return { + get(value) { + let numTokens = 1; + let inSpace = false; + for (let i = 0; i < value.length; i++) if (value.charCodeAt(i) === 32) { + if (!inSpace) { + numTokens++; + inSpace = true; + } + } else inSpace = false; + if (cache.has(numTokens)) return cache.get(numTokens); + const n = Math.round(m / Math.pow(numTokens, .5 * weight)) / m; + cache.set(numTokens, n); + return n; + }, + clear() { + cache.clear(); + } + }; } -class FuseIndex { - constructor({ - getFn = Config.getFn, - fieldNormWeight = Config.fieldNormWeight - } = {}) { - this.norm = norm(fieldNormWeight, 3); - this.getFn = getFn; - this.isCreated = false; - this.docs = []; - this.keys = []; - this._keysMap = {}; - this.setIndexRecords(); - } - setSources(docs = []) { - this.docs = docs; - } - setIndexRecords(records = []) { - this.records = records; - } - setKeys(keys = []) { - this.keys = keys; - this._keysMap = {}; - keys.forEach((key, idx) => { - this._keysMap[key.id] = idx; - }); - } - create() { - if (this.isCreated || !this.docs.length) { - return; - } - this.isCreated = true; - const len = this.docs.length; - this.records = new Array(len); - let recordCount = 0; - - // List is Array - if (isString(this.docs[0])) { - for (let i = 0; i < len; i++) { - const record = this._createStringRecord(this.docs[i], i); - if (record) { - this.records[recordCount++] = record; - } - } - } else { - // List is Array - for (let i = 0; i < len; i++) { - this.records[recordCount++] = this._createObjectRecord(this.docs[i], i); - } - } - this.records.length = recordCount; - this.norm.clear(); - } - // Appends a record for `doc` at `docIndex` (the doc's position in the source - // array). Returns the appended record, or null when `doc` is a blank string - // (those are skipped at record creation; see `_createStringRecord`). Callers - // use the return value to gate downstream bookkeeping like the inverted - // index, which must not be touched when no record was produced. - add(doc, docIndex) { - if (!Number.isInteger(docIndex) || docIndex < 0) { - throw new Error(INVALID_DOC_INDEX); - } - if (isString(doc)) { - const record = this._createStringRecord(doc, docIndex); - if (record) { - this.records.push(record); - } - return record; - } - const record = this._createObjectRecord(doc, docIndex); - this.records.push(record); - return record; - } - // Removes the record for the doc at the specified source-array (docs) index. - // Blank string docs have no record; callers may pass such an index and the - // splice is a no-op, but subsequent records still need their .i decremented - // to track the docs array that the caller is splicing in parallel. - removeAt(idx) { - if (!Number.isInteger(idx) || idx < 0) { - throw new Error(INVALID_DOC_INDEX); - } - - // Find and remove the record at this doc-index, if one exists. Records are - // typically sorted by .i but the algorithm doesn't depend on it — parsed - // indexes via setIndexRecords may arrive in arbitrary order. - for (let i = 0, len = this.records.length; i < len; i += 1) { - if (this.records[i].i === idx) { - this.records.splice(i, 1); - break; - } - } - - // Decrement every record whose source-array index is now stale. - for (let i = 0, len = this.records.length; i < len; i += 1) { - if (this.records[i].i > idx) { - this.records[i].i -= 1; - } - } - } - // Removes records for the docs at the specified source-array indices, then - // shifts every surviving record's .i down by the count of removed indices - // strictly less than it (mirrors removeAndShiftInvertedIndex's shift math). - // Invalid entries (non-integer, negative) in `indices` are dropped silently - // — removeAll's natural use case is "caller passed a list of matched doc - // indices"; asymmetric throw-vs-no-op would be more surprising than a clean - // filter. - removeAll(indices) { - const toRemove = new Set(); - for (const v of indices) { - if (Number.isInteger(v) && v >= 0) { - toRemove.add(v); - } - } - if (toRemove.size === 0) { - return; - } - this.records = this.records.filter(r => !toRemove.has(r.i)); - const sorted = Array.from(toRemove).sort((a, b) => a - b); - for (const record of this.records) { - // shift = count of removed indices strictly less than record.i - let lo = 0; - let hi = sorted.length; - while (lo < hi) { - const mid = lo + hi >>> 1; - if (sorted[mid] < record.i) lo = mid + 1;else hi = mid; - } - record.i -= lo; - } - } - getValueForItemAtKeyId(item, keyId) { - return item[this._keysMap[keyId]]; - } - size() { - return this.records.length; - } - _createStringRecord(doc, docIndex) { - if (!isDefined(doc) || isBlank(doc)) { - return null; - } - return { - v: doc, - i: docIndex, - n: this.norm.get(doc) - }; - } - _createObjectRecord(doc, docIndex) { - const record = { - i: docIndex, - $: {} - }; - - // Iterate over every key (i.e, path), and fetch the value at that key - for (let keyIndex = 0, keyLen = this.keys.length; keyIndex < keyLen; keyIndex++) { - const key = this.keys[keyIndex]; - const value = key.getFn ? key.getFn(doc) : this.getFn(doc, key.path); - if (!isDefined(value)) { - continue; - } - if (isArray(value)) { - const subRecords = []; - for (let i = 0, len = value.length; i < len; i += 1) { - const item = value[i]; - if (!isDefined(item)) { - continue; - } - if (isString(item)) { - // Custom getFn returning plain string array (backward compat) - if (!isBlank(item)) { - const subRecord = { - v: item, - i: i, - n: this.norm.get(item) - }; - subRecords.push(subRecord); - } - } else if (isDefined(item.v)) { - // Default get() returns {v, i} objects with original array indices - const text = isString(item.v) ? item.v : toString(item.v); - if (!isBlank(text)) { - const subRecord = { - v: text, - i: item.i, - n: this.norm.get(text) - }; - subRecords.push(subRecord); - } - } - } - record.$[keyIndex] = subRecords; - } else if (isString(value) && !isBlank(value)) { - const subRecord = { - v: value, - n: this.norm.get(value) - }; - record.$[keyIndex] = subRecord; - } - } - return record; - } - toJSON() { - return { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - keys: this.keys.map(({ - getFn, - ...key - }) => key), - records: this.records - }; - } -} -function createIndex(keys, docs, { - getFn = Config.getFn, - fieldNormWeight = Config.fieldNormWeight -} = {}) { - const myIndex = new FuseIndex({ - getFn, - fieldNormWeight - }); - myIndex.setKeys(keys.map(createKey)); - myIndex.setSources(docs); - myIndex.create(); - return myIndex; +//#endregion +//#region src/tools/FuseIndex.ts +var FuseIndex = class { + constructor({ getFn = Config.getFn, fieldNormWeight = Config.fieldNormWeight } = {}) { + this.norm = norm(fieldNormWeight, 3); + this.getFn = getFn; + this.isCreated = false; + this.docs = []; + this.keys = []; + this._keysMap = {}; + this.setIndexRecords(); + } + setSources(docs = []) { + this.docs = docs; + } + setIndexRecords(records = []) { + this.records = records; + } + setKeys(keys = []) { + this.keys = keys; + this._keysMap = {}; + keys.forEach((key, idx) => { + this._keysMap[key.id] = idx; + }); + } + create() { + if (this.isCreated || !this.docs.length) return; + this.isCreated = true; + const len = this.docs.length; + this.records = new Array(len); + let recordCount = 0; + if (isString(this.docs[0])) for (let i = 0; i < len; i++) { + const record = this._createStringRecord(this.docs[i], i); + if (record) this.records[recordCount++] = record; + } + else for (let i = 0; i < len; i++) this.records[recordCount++] = this._createObjectRecord(this.docs[i], i); + this.records.length = recordCount; + this.norm.clear(); + } + add(doc, docIndex) { + if (!Number.isInteger(docIndex) || docIndex < 0) throw new Error(INVALID_DOC_INDEX); + if (isString(doc)) { + const record = this._createStringRecord(doc, docIndex); + if (record) this.records.push(record); + return record; + } + const record = this._createObjectRecord(doc, docIndex); + this.records.push(record); + return record; + } + removeAt(idx) { + if (!Number.isInteger(idx) || idx < 0) throw new Error(INVALID_DOC_INDEX); + for (let i = 0, len = this.records.length; i < len; i += 1) if (this.records[i].i === idx) { + this.records.splice(i, 1); + break; + } + for (let i = 0, len = this.records.length; i < len; i += 1) if (this.records[i].i > idx) this.records[i].i -= 1; + } + removeAll(indices) { + const toRemove = /* @__PURE__ */ new Set(); + for (const v of indices) if (Number.isInteger(v) && v >= 0) toRemove.add(v); + if (toRemove.size === 0) return; + this.records = this.records.filter((r) => !toRemove.has(r.i)); + const sorted = Array.from(toRemove).sort((a, b) => a - b); + for (const record of this.records) { + let lo = 0; + let hi = sorted.length; + while (lo < hi) { + const mid = lo + hi >>> 1; + if (sorted[mid] < record.i) lo = mid + 1; + else hi = mid; + } + record.i -= lo; + } + } + getValueForItemAtKeyId(item, keyId) { + return item[this._keysMap[keyId]]; + } + size() { + return this.records.length; + } + _createStringRecord(doc, docIndex) { + if (!isDefined(doc) || isBlank(doc)) return null; + return { + v: doc, + i: docIndex, + n: this.norm.get(doc) + }; + } + _createObjectRecord(doc, docIndex) { + const record = { + i: docIndex, + $: {} + }; + for (let keyIndex = 0, keyLen = this.keys.length; keyIndex < keyLen; keyIndex++) { + const key = this.keys[keyIndex]; + const value = key.getFn ? key.getFn(doc) : this.getFn(doc, key.path); + if (!isDefined(value)) continue; + if (isArray(value)) { + const subRecords = []; + for (let i = 0, len = value.length; i < len; i += 1) { + const item = value[i]; + if (!isDefined(item)) continue; + if (isString(item)) { + if (!isBlank(item)) { + const subRecord = { + v: item, + i, + n: this.norm.get(item) + }; + subRecords.push(subRecord); + } + } else if (isDefined(item.v)) { + const text = isString(item.v) ? item.v : toString(item.v); + if (!isBlank(text)) { + const subRecord = { + v: text, + i: item.i, + n: this.norm.get(text) + }; + subRecords.push(subRecord); + } + } + } + record.$[keyIndex] = subRecords; + } else if (isString(value) && !isBlank(value)) { + const subRecord = { + v: value, + n: this.norm.get(value) + }; + record.$[keyIndex] = subRecord; + } + } + return record; + } + toJSON() { + return { + keys: this.keys.map(({ getFn, ...key }) => key), + records: this.records + }; + } +}; +function createIndex(keys, docs, { getFn = Config.getFn, fieldNormWeight = Config.fieldNormWeight } = {}) { + const myIndex = new FuseIndex({ + getFn, + fieldNormWeight + }); + myIndex.setKeys(keys.map(createKey)); + myIndex.setSources(docs); + myIndex.create(); + return myIndex; } -function parseIndex(data, { - getFn = Config.getFn, - fieldNormWeight = Config.fieldNormWeight -} = {}) { - const { - keys, - records - } = data; - const myIndex = new FuseIndex({ - getFn, - fieldNormWeight - }); - myIndex.setKeys(keys); - myIndex.setIndexRecords(records); - return myIndex; +function parseIndex(data, { getFn = Config.getFn, fieldNormWeight = Config.fieldNormWeight } = {}) { + const { keys, records } = data; + const myIndex = new FuseIndex({ + getFn, + fieldNormWeight + }); + myIndex.setKeys(keys); + myIndex.setIndexRecords(records); + return myIndex; } +//#endregion +//#region src/search/bitap/convertMaskToIndices.ts function convertMaskToIndices(matchmask = [], minMatchCharLength = Config.minMatchCharLength) { - const indices = []; - let start = -1; - let end = -1; - let i = 0; - for (let len = matchmask.length; i < len; i += 1) { - const match = matchmask[i]; - if (match && start === -1) { - start = i; - } else if (!match && start !== -1) { - end = i - 1; - if (end - start + 1 >= minMatchCharLength) { - indices.push([start, end]); - } - start = -1; - } - } - - // (i-1 - start) + 1 => i - start - if (matchmask[i - 1] && i - start >= minMatchCharLength) { - indices.push([start, i - 1]); - } - return indices; + const indices = []; + let start = -1; + let end = -1; + let i = 0; + for (let len = matchmask.length; i < len; i += 1) { + const match = matchmask[i]; + if (match && start === -1) start = i; + else if (!match && start !== -1) { + end = i - 1; + if (end - start + 1 >= minMatchCharLength) indices.push([start, end]); + start = -1; + } + } + if (matchmask[i - 1] && i - start >= minMatchCharLength) indices.push([start, i - 1]); + return indices; } -// Machine word size -const MAX_BITS = 32; - -function search(text, pattern, patternAlphabet, { - location = Config.location, - distance = Config.distance, - threshold = Config.threshold, - findAllMatches = Config.findAllMatches, - minMatchCharLength = Config.minMatchCharLength, - includeMatches = Config.includeMatches, - ignoreLocation = Config.ignoreLocation -} = {}) { - if (pattern.length > MAX_BITS) { - throw new Error(PATTERN_LENGTH_TOO_LARGE(MAX_BITS)); - } - const patternLen = pattern.length; - // Set starting location at beginning text and initialize the alphabet. - const textLen = text.length; - // Handle the case when location > text.length - const expectedLocation = Math.max(0, Math.min(location, textLen)); - // Highest score beyond which we give up. - let currentThreshold = threshold; - // Is there a nearby exact match? (speedup) - let bestLocation = expectedLocation; - - // Inlined score computation — avoids object allocation per call in hot loops. - // See ./computeScore.ts for the documented version of this formula. - const calcScore = (errors, currentLocation) => { - const accuracy = errors / patternLen; - if (ignoreLocation) return accuracy; - const proximity = Math.abs(expectedLocation - currentLocation); - if (!distance) return proximity ? 1.0 : accuracy; - return accuracy + proximity / distance; - }; - - // Performance: only computer matches when the minMatchCharLength > 1 - // OR if `includeMatches` is true. - const computeMatches = minMatchCharLength > 1 || includeMatches; - // A mask of the matches, used for building the indices - const matchMask = computeMatches ? Array(textLen) : []; - let index; - - // Get all exact matches, here for speed up - while ((index = text.indexOf(pattern, bestLocation)) > -1) { - const score = calcScore(0, index); - currentThreshold = Math.min(score, currentThreshold); - bestLocation = index + patternLen; - if (computeMatches) { - let i = 0; - while (i < patternLen) { - matchMask[index + i] = 1; - i += 1; - } - } - } - - // Reset the best location - bestLocation = -1; - let lastBitArr = []; - let finalScore = 1; - let bestErrors = 0; - let binMax = patternLen + textLen; - const mask = 1 << patternLen - 1; - for (let i = 0; i < patternLen; i += 1) { - // Scan for the best match; each iteration allows for one more error. - // Run a binary search to determine how far from the match location we can stray - // at this error level. - let binMin = 0; - let binMid = binMax; - while (binMin < binMid) { - const score = calcScore(i, expectedLocation + binMid); - if (score <= currentThreshold) { - binMin = binMid; - } else { - binMax = binMid; - } - binMid = Math.floor((binMax - binMin) / 2 + binMin); - } - - // Use the result from this iteration as the maximum for the next. - binMax = binMid; - let start = Math.max(1, expectedLocation - binMid + 1); - const finish = findAllMatches ? textLen : Math.min(expectedLocation + binMid, textLen) + patternLen; - - // Initialize the bit array - const bitArr = Array(finish + 2); - bitArr[finish + 1] = (1 << i) - 1; - for (let j = finish; j >= start; j -= 1) { - const currentLocation = j - 1; - const charMatch = patternAlphabet[text[currentLocation]]; - - // First pass: exact match - bitArr[j] = (bitArr[j + 1] << 1 | 1) & charMatch; - - // Subsequent passes: fuzzy match - if (i) { - bitArr[j] |= (lastBitArr[j + 1] | lastBitArr[j]) << 1 | 1 | lastBitArr[j + 1]; - } - if (bitArr[j] & mask) { - finalScore = calcScore(i, currentLocation); - - // This match will almost certainly be better than any existing match. - // But check anyway. - if (finalScore <= currentThreshold) { - // Indeed it is - currentThreshold = finalScore; - bestLocation = currentLocation; - bestErrors = i; - - // Already passed `loc`, downhill from here on in. - if (bestLocation <= expectedLocation) { - break; - } - - // When passing `bestLocation`, don't exceed our current distance from `expectedLocation`. - start = Math.max(1, 2 * expectedLocation - bestLocation); - } - } - } - - // No hope for a (better) match at greater error levels. - const score = calcScore(i + 1, expectedLocation); - if (score > currentThreshold) { - break; - } - lastBitArr = bitArr; - } - - // Fill matchMask across the matched window only. Bitap anchors a match at - // bestLocation (the start), spanning patternLen characters plus up to - // bestErrors extra characters when errors are text-side insertions. Marking - // alphabet positions in that window keeps the highlight indices honest about - // what actually matched, instead of every pattern-alphabet character the - // scan happened to visit. - if (computeMatches && bestLocation >= 0) { - const matchEnd = Math.min(textLen - 1, bestLocation + patternLen - 1 + bestErrors); - for (let k = bestLocation; k <= matchEnd; k += 1) { - if (patternAlphabet[text[k]]) { - matchMask[k] = 1; - } - } - } - const result = { - isMatch: bestLocation >= 0, - // Count exact matches (those with a score of 0) to be "almost" exact - score: Math.max(0.001, finalScore) - }; - if (computeMatches) { - const indices = convertMaskToIndices(matchMask, minMatchCharLength); - if (!indices.length) { - result.isMatch = false; - } else if (includeMatches) { - result.indices = indices; - } - } - return result; +//#endregion +//#region src/search/bitap/search.ts +function search(text, pattern, patternAlphabet, { location = Config.location, distance = Config.distance, threshold = Config.threshold, findAllMatches = Config.findAllMatches, minMatchCharLength = Config.minMatchCharLength, includeMatches = Config.includeMatches, ignoreLocation = Config.ignoreLocation } = {}) { + if (pattern.length > 32) throw new Error(PATTERN_LENGTH_TOO_LARGE(32)); + const patternLen = pattern.length; + const textLen = text.length; + const expectedLocation = Math.max(0, Math.min(location, textLen)); + let currentThreshold = threshold; + let bestLocation = expectedLocation; + const calcScore = (errors, currentLocation) => { + const accuracy = errors / patternLen; + if (ignoreLocation) return accuracy; + const proximity = Math.abs(expectedLocation - currentLocation); + if (!distance) return proximity ? 1 : accuracy; + return accuracy + proximity / distance; + }; + const computeMatches = minMatchCharLength > 1 || includeMatches; + const matchMask = computeMatches ? Array(textLen) : []; + let index; + while ((index = text.indexOf(pattern, bestLocation)) > -1) { + const score = calcScore(0, index); + currentThreshold = Math.min(score, currentThreshold); + bestLocation = index + patternLen; + if (computeMatches) { + let i = 0; + while (i < patternLen) { + matchMask[index + i] = 1; + i += 1; + } + } + } + bestLocation = -1; + let lastBitArr = []; + let finalScore = 1; + let bestErrors = 0; + let binMax = patternLen + textLen; + const mask = 1 << patternLen - 1; + for (let i = 0; i < patternLen; i += 1) { + let binMin = 0; + let binMid = binMax; + while (binMin < binMid) { + if (calcScore(i, expectedLocation + binMid) <= currentThreshold) binMin = binMid; + else binMax = binMid; + binMid = Math.floor((binMax - binMin) / 2 + binMin); + } + binMax = binMid; + let start = Math.max(1, expectedLocation - binMid + 1); + const finish = findAllMatches ? textLen : Math.min(expectedLocation + binMid, textLen) + patternLen; + const bitArr = Array(finish + 2); + bitArr[finish + 1] = (1 << i) - 1; + for (let j = finish; j >= start; j -= 1) { + const currentLocation = j - 1; + const charMatch = patternAlphabet[text[currentLocation]]; + bitArr[j] = (bitArr[j + 1] << 1 | 1) & charMatch; + if (i) bitArr[j] |= (lastBitArr[j + 1] | lastBitArr[j]) << 1 | 1 | lastBitArr[j + 1]; + if (bitArr[j] & mask) { + finalScore = calcScore(i, currentLocation); + if (finalScore <= currentThreshold) { + currentThreshold = finalScore; + bestLocation = currentLocation; + bestErrors = i; + if (bestLocation <= expectedLocation) break; + start = Math.max(1, 2 * expectedLocation - bestLocation); + } + } + } + if (calcScore(i + 1, expectedLocation) > currentThreshold) break; + lastBitArr = bitArr; + } + if (computeMatches && bestLocation >= 0) { + const matchEnd = Math.min(textLen - 1, bestLocation + patternLen - 1 + bestErrors); + for (let k = bestLocation; k <= matchEnd; k += 1) if (patternAlphabet[text[k]]) matchMask[k] = 1; + } + const result = { + isMatch: bestLocation >= 0, + score: Math.max(.001, finalScore) + }; + if (computeMatches) { + const indices = convertMaskToIndices(matchMask, minMatchCharLength); + if (!indices.length) result.isMatch = false; + else if (includeMatches) result.indices = indices; + } + return result; } +//#endregion +//#region src/search/bitap/createPatternAlphabet.ts function createPatternAlphabet(pattern) { - const mask = {}; - for (let i = 0, len = pattern.length; i < len; i += 1) { - const char = pattern.charAt(i); - mask[char] = (mask[char] || 0) | 1 << len - i - 1; - } - return mask; + const mask = {}; + for (let i = 0, len = pattern.length; i < len; i += 1) { + const char = pattern.charAt(i); + mask[char] = (mask[char] || 0) | 1 << len - i - 1; + } + return mask; } +//#endregion +//#region src/helpers/mergeIndices.ts function mergeIndices(indices) { - if (indices.length <= 1) return indices; - indices.sort((a, b) => a[0] - b[0] || a[1] - b[1]); - const merged = [indices[0]]; - for (let i = 1, len = indices.length; i < len; i += 1) { - const last = merged[merged.length - 1]; - const curr = indices[i]; - if (curr[0] <= last[1] + 1) { - last[1] = Math.max(last[1], curr[1]); - } else { - merged.push(curr); - } - } - return merged; + if (indices.length <= 1) return indices; + indices.sort((a, b) => a[0] - b[0] || a[1] - b[1]); + const merged = [indices[0]]; + for (let i = 1, len = indices.length; i < len; i += 1) { + const last = merged[merged.length - 1]; + const curr = indices[i]; + if (curr[0] <= last[1] + 1) last[1] = Math.max(last[1], curr[1]); + else merged.push(curr); + } + return merged; } -// Characters that survive NFD normalization unchanged and need explicit mapping +//#endregion +//#region src/helpers/diacritics.ts const NON_DECOMPOSABLE_MAP = { - '\u0142': 'l', - // ł - '\u0141': 'L', - // Ł - '\u0111': 'd', - // đ - '\u0110': 'D', - // Đ - '\u00F8': 'o', - // ø - '\u00D8': 'O', - // Ø - '\u0127': 'h', - // ħ - '\u0126': 'H', - // Ħ - '\u0167': 't', - // ŧ - '\u0166': 'T', - // Ŧ - '\u0131': 'i', - // ı - '\u00DF': 'ss' // ß + "ł": "l", + "Ł": "L", + "đ": "d", + "Đ": "D", + "ø": "o", + "Ø": "O", + "ħ": "h", + "Ħ": "H", + "ŧ": "t", + "Ŧ": "T", + "ı": "i", + "ß": "ss" +}; +const NON_DECOMPOSABLE_RE = new RegExp("[" + Object.keys(NON_DECOMPOSABLE_MAP).join("") + "]", "g"); +const stripDiacritics = typeof String.prototype.normalize === "function" ? (str) => str.normalize("NFD").replace(/[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]/g, "").replace(NON_DECOMPOSABLE_RE, (ch) => NON_DECOMPOSABLE_MAP[ch]) : (str) => str; + +//#endregion +//#region src/search/bitap/index.ts +var BitapSearch = class { + constructor(pattern, { location = Config.location, threshold = Config.threshold, distance = Config.distance, includeMatches = Config.includeMatches, findAllMatches = Config.findAllMatches, minMatchCharLength = Config.minMatchCharLength, isCaseSensitive = Config.isCaseSensitive, ignoreDiacritics = Config.ignoreDiacritics, ignoreLocation = Config.ignoreLocation } = {}) { + this.options = { + location, + threshold, + distance, + includeMatches, + findAllMatches, + minMatchCharLength, + isCaseSensitive, + ignoreDiacritics, + ignoreLocation + }; + pattern = isCaseSensitive ? pattern : pattern.toLowerCase(); + pattern = ignoreDiacritics ? stripDiacritics(pattern) : pattern; + this.pattern = pattern; + this.chunks = []; + if (!this.pattern.length) return; + const addChunk = (pattern, startIndex) => { + this.chunks.push({ + pattern, + alphabet: createPatternAlphabet(pattern), + startIndex + }); + }; + const len = this.pattern.length; + if (len > 32) { + let i = 0; + const remainder = len % 32; + const end = len - remainder; + while (i < end) { + addChunk(this.pattern.substr(i, 32), i); + i += 32; + } + if (remainder) { + const startIndex = len - 32; + addChunk(this.pattern.substr(startIndex), startIndex); + } + } else addChunk(this.pattern, 0); + } + searchIn(text) { + const { isCaseSensitive, ignoreDiacritics, includeMatches } = this.options; + text = isCaseSensitive ? text : text.toLowerCase(); + text = ignoreDiacritics ? stripDiacritics(text) : text; + if (this.pattern === text) { + const result = { + isMatch: true, + score: 0 + }; + if (includeMatches) result.indices = [[0, text.length - 1]]; + return result; + } + const { location, distance, threshold, findAllMatches, minMatchCharLength, ignoreLocation } = this.options; + const allIndices = []; + let totalScore = 0; + let hasMatches = false; + this.chunks.forEach(({ pattern, alphabet, startIndex }) => { + const { isMatch, score, indices } = search(text, pattern, alphabet, { + location: location + startIndex, + distance, + threshold, + findAllMatches, + minMatchCharLength, + includeMatches, + ignoreLocation + }); + if (isMatch) hasMatches = true; + totalScore += score; + if (isMatch && indices) allIndices.push(...indices); + }); + const result = { + isMatch: hasMatches, + score: hasMatches ? totalScore / this.chunks.length : 1 + }; + if (hasMatches && includeMatches) result.indices = mergeIndices(allIndices); + return result; + } }; -const NON_DECOMPOSABLE_RE = new RegExp('[' + Object.keys(NON_DECOMPOSABLE_MAP).join('') + ']', 'g'); -const stripDiacritics = typeof String.prototype.normalize === 'function' ? str => str.normalize('NFD').replace(/[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]/g, '').replace(NON_DECOMPOSABLE_RE, ch => NON_DECOMPOSABLE_MAP[ch]) : str => str; - -class BitapSearch { - constructor(pattern, { - location = Config.location, - threshold = Config.threshold, - distance = Config.distance, - includeMatches = Config.includeMatches, - findAllMatches = Config.findAllMatches, - minMatchCharLength = Config.minMatchCharLength, - isCaseSensitive = Config.isCaseSensitive, - ignoreDiacritics = Config.ignoreDiacritics, - ignoreLocation = Config.ignoreLocation - } = {}) { - this.options = { - location, - threshold, - distance, - includeMatches, - findAllMatches, - minMatchCharLength, - isCaseSensitive, - ignoreDiacritics, - ignoreLocation - }; - pattern = isCaseSensitive ? pattern : pattern.toLowerCase(); - pattern = ignoreDiacritics ? stripDiacritics(pattern) : pattern; - this.pattern = pattern; - this.chunks = []; - if (!this.pattern.length) { - return; - } - const addChunk = (pattern, startIndex) => { - this.chunks.push({ - pattern, - alphabet: createPatternAlphabet(pattern), - startIndex - }); - }; - const len = this.pattern.length; - if (len > MAX_BITS) { - let i = 0; - const remainder = len % MAX_BITS; - const end = len - remainder; - while (i < end) { - addChunk(this.pattern.substr(i, MAX_BITS), i); - i += MAX_BITS; - } - if (remainder) { - const startIndex = len - MAX_BITS; - addChunk(this.pattern.substr(startIndex), startIndex); - } - } else { - addChunk(this.pattern, 0); - } - } - searchIn(text) { - const { - isCaseSensitive, - ignoreDiacritics, - includeMatches - } = this.options; - text = isCaseSensitive ? text : text.toLowerCase(); - text = ignoreDiacritics ? stripDiacritics(text) : text; - - // Exact match - if (this.pattern === text) { - const result = { - isMatch: true, - score: 0 - }; - if (includeMatches) { - result.indices = [[0, text.length - 1]]; - } - return result; - } - - // Otherwise, use Bitap algorithm - const { - location, - distance, - threshold, - findAllMatches, - minMatchCharLength, - ignoreLocation - } = this.options; - const allIndices = []; - let totalScore = 0; - let hasMatches = false; - this.chunks.forEach(({ - pattern, - alphabet, - startIndex - }) => { - const { - isMatch, - score, - indices - } = search(text, pattern, alphabet, { - location: location + startIndex, - distance, - threshold, - findAllMatches, - minMatchCharLength, - includeMatches, - ignoreLocation - }); - if (isMatch) { - hasMatches = true; - } - totalScore += score; - if (isMatch && indices) { - allIndices.push(...indices); - } - }); - const result = { - isMatch: hasMatches, - score: hasMatches ? totalScore / this.chunks.length : 1 - }; - if (hasMatches && includeMatches) { - result.indices = mergeIndices(allIndices); - } - return result; - } -} +//#endregion +//#region src/core/register.ts const registeredSearchers = []; function register(...args) { - registeredSearchers.push(...args); + registeredSearchers.push(...args); } function createSearcher(pattern, options) { - for (let i = 0, len = registeredSearchers.length; i < len; i += 1) { - const searcherClass = registeredSearchers[i]; - if (searcherClass.condition(pattern, options)) { - return new searcherClass(pattern, options); - } - } - return new BitapSearch(pattern, options); + for (let i = 0, len = registeredSearchers.length; i < len; i += 1) { + const searcherClass = registeredSearchers[i]; + if (searcherClass.condition(pattern, options)) return new searcherClass(pattern, options); + } + return new BitapSearch(pattern, options); } +//#endregion +//#region src/core/queryParser.ts const LogicalOperator = { - AND: '$and', - OR: '$or' + AND: "$and", + OR: "$or" }; const KeyType = { - PATH: '$path', - PATTERN: '$val' + PATH: "$path", + PATTERN: "$val" }; -const isExpression = query => !!(query[LogicalOperator.AND] || query[LogicalOperator.OR]); -const isPath = query => !!query[KeyType.PATH]; -const isLeaf = query => !isArray(query) && isObject(query) && !isExpression(query); -const convertToExplicit = query => ({ - [LogicalOperator.AND]: Object.keys(query).map(key => ({ - [key]: query[key] - })) -}); - -// When `auto` is `true`, the parse function will infer and initialize and add -// the appropriate `Searcher` instance -function parse(query, options, { - auto = true -} = {}) { - const next = query => { - // Keyless string entry: search across all keys - if (isString(query)) { - const obj = { - keyId: null, - pattern: query - }; - if (auto) { - obj.searcher = createSearcher(query, options); - } - return obj; - } - const keys = Object.keys(query); - const isQueryPath = isPath(query); - if (!isQueryPath && keys.length > 1 && !isExpression(query)) { - return next(convertToExplicit(query)); - } - if (isLeaf(query)) { - const key = isQueryPath ? query[KeyType.PATH] : keys[0]; - const pattern = isQueryPath ? query[KeyType.PATTERN] : query[key]; - if (!isString(pattern)) { - throw new Error(LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY(key)); - } - const obj = { - keyId: createKeyId(key), - pattern - }; - if (auto) { - obj.searcher = createSearcher(pattern, options); - } - return obj; - } - const node = { - children: [], - operator: keys[0] - }; - keys.forEach(key => { - const value = query[key]; - if (isArray(value)) { - value.forEach(item => { - node.children.push(next(item)); - }); - } - }); - return node; - }; - if (!isExpression(query)) { - query = convertToExplicit(query); - } - return next(query); +const isExpression = (query) => !!(query[LogicalOperator.AND] || query[LogicalOperator.OR]); +const isPath = (query) => !!query[KeyType.PATH]; +const isLeaf = (query) => !isArray(query) && isObject(query) && !isExpression(query); +const convertToExplicit = (query) => ({ [LogicalOperator.AND]: Object.keys(query).map((key) => ({ [key]: query[key] })) }); +function parse(query, options, { auto = true } = {}) { + const next = (query) => { + if (isString(query)) { + const obj = { + keyId: null, + pattern: query + }; + if (auto) obj.searcher = createSearcher(query, options); + return obj; + } + const keys = Object.keys(query); + const isQueryPath = isPath(query); + if (!isQueryPath && keys.length > 1 && !isExpression(query)) return next(convertToExplicit(query)); + if (isLeaf(query)) { + const key = isQueryPath ? query[KeyType.PATH] : keys[0]; + const pattern = isQueryPath ? query[KeyType.PATTERN] : query[key]; + if (!isString(pattern)) throw new Error(LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY(key)); + const obj = { + keyId: createKeyId(key), + pattern + }; + if (auto) obj.searcher = createSearcher(pattern, options); + return obj; + } + const node = { + children: [], + operator: keys[0] + }; + keys.forEach((key) => { + const value = query[key]; + if (isArray(value)) value.forEach((item) => { + node.children.push(next(item)); + }); + }); + return node; + }; + if (!isExpression(query)) query = convertToExplicit(query); + return next(query); } -function computeScoreSingle(matches, { - ignoreFieldNorm = Config.ignoreFieldNorm -}) { - let totalScore = 1; - matches.forEach(({ - key, - norm, - score - }) => { - const weight = key ? key.weight : null; - totalScore *= Math.pow(score === 0 && weight ? Number.EPSILON : score, (weight || 1) * (ignoreFieldNorm ? 1 : norm)); - }); - return totalScore; +//#endregion +//#region src/core/computeScore.ts +function computeScoreSingle(matches, { ignoreFieldNorm = Config.ignoreFieldNorm }) { + let totalScore = 1; + matches.forEach(({ key, norm, score }) => { + const weight = key ? key.weight : null; + totalScore *= Math.pow(score === 0 && weight ? Number.EPSILON : score, (weight || 1) * (ignoreFieldNorm ? 1 : norm)); + }); + return totalScore; } -function computeScore(results, { - ignoreFieldNorm = Config.ignoreFieldNorm -}) { - results.forEach(result => { - result.score = computeScoreSingle(result.matches, { - ignoreFieldNorm - }); - }); +function computeScore(results, { ignoreFieldNorm = Config.ignoreFieldNorm }) { + results.forEach((result) => { + result.score = computeScoreSingle(result.matches, { ignoreFieldNorm }); + }); } -// Max-heap by score: keeps the worst (highest) score at the top -// so we can efficiently evict it when a better result arrives. -class MaxHeap { - constructor(limit) { - this.limit = limit; - this.heap = []; - } - get size() { - return this.heap.length; - } - shouldInsert(score) { - return this.size < this.limit || score < this.heap[0].score; - } - insert(item) { - if (this.size < this.limit) { - this.heap.push(item); - this._bubbleUp(this.size - 1); - } else if (item.score < this.heap[0].score) { - this.heap[0] = item; - this._sinkDown(0); - } - } - extractSorted(sortFn) { - return this.heap.sort(sortFn); - } - _bubbleUp(i) { - const heap = this.heap; - while (i > 0) { - const parent = i - 1 >> 1; - if (heap[i].score <= heap[parent].score) break; - const tmp = heap[i]; - heap[i] = heap[parent]; - heap[parent] = tmp; - i = parent; - } - } - _sinkDown(i) { - const heap = this.heap; - const len = heap.length; - let largest = i; - do { - i = largest; - const left = 2 * i + 1; - const right = 2 * i + 2; - if (left < len && heap[left].score > heap[largest].score) { - largest = left; - } - if (right < len && heap[right].score > heap[largest].score) { - largest = right; - } - if (largest !== i) { - const tmp = heap[i]; - heap[i] = heap[largest]; - heap[largest] = tmp; - } - } while (largest !== i); - } -} +//#endregion +//#region src/tools/MaxHeap.ts +var MaxHeap = class { + constructor(limit) { + this.limit = limit; + this.heap = []; + } + get size() { + return this.heap.length; + } + shouldInsert(score) { + return this.size < this.limit || score < this.heap[0].score; + } + insert(item) { + if (this.size < this.limit) { + this.heap.push(item); + this._bubbleUp(this.size - 1); + } else if (item.score < this.heap[0].score) { + this.heap[0] = item; + this._sinkDown(0); + } + } + extractSorted(sortFn) { + return this.heap.sort(sortFn); + } + _bubbleUp(i) { + const heap = this.heap; + while (i > 0) { + const parent = i - 1 >> 1; + if (heap[i].score <= heap[parent].score) break; + const tmp = heap[i]; + heap[i] = heap[parent]; + heap[parent] = tmp; + i = parent; + } + } + _sinkDown(i) { + const heap = this.heap; + const len = heap.length; + let largest = i; + do { + i = largest; + const left = 2 * i + 1; + const right = 2 * i + 2; + if (left < len && heap[left].score > heap[largest].score) largest = left; + if (right < len && heap[right].score > heap[largest].score) largest = right; + if (largest !== i) { + const tmp = heap[i]; + heap[i] = heap[largest]; + heap[largest] = tmp; + } + } while (largest !== i); + } +}; +//#endregion +//#region src/core/formatMatches.ts function formatMatches(result) { - const matches = []; - result.matches.forEach(match => { - if (!isDefined(match.indices) || !match.indices.length) { - return; - } - const obj = { - indices: match.indices, - value: match.value - }; - if (match.key) { - // `key.id` is the canonical dotted-string identity (array paths joined - // with '.'); `key.src` is the raw user input and can be a string[]. - obj.key = match.key.id; - } - if (match.idx > -1) { - obj.refIndex = match.idx; - } - matches.push(obj); - }); - return matches; + const matches = []; + result.matches.forEach((match) => { + if (!isDefined(match.indices) || !match.indices.length) return; + const obj = { + indices: match.indices, + value: match.value + }; + if (match.key) obj.key = match.key.id; + if (match.idx > -1) obj.refIndex = match.idx; + matches.push(obj); + }); + return matches; } -function format(results, docs, { - includeMatches = Config.includeMatches, - includeScore = Config.includeScore -} = {}) { - return results.map(result => { - const { - idx - } = result; - const data = { - item: docs[idx], - refIndex: idx - }; - if (includeMatches) data.matches = formatMatches(result); - if (includeScore) data.score = result.score; - return data; - }); +//#endregion +//#region src/core/format.ts +function format(results, docs, { includeMatches = Config.includeMatches, includeScore = Config.includeScore } = {}) { + return results.map((result) => { + const { idx } = result; + const data = { + item: docs[idx], + refIndex: idx + }; + if (includeMatches) data.matches = formatMatches(result); + if (includeScore) data.score = result.score; + return data; + }); } -// Includes \p{M} (Mark) so combining marks stay attached to their base -// letter — without it, scripts like Devanagari and NFD-normalized Latin -// shatter (e.g. 'हिन्दी' → ['ह','न','द'], 'café'.normalize('NFD') → ['cafe']). +//#endregion +//#region src/search/token/analyzer.ts const DEFAULT_TOKEN = /[\p{L}\p{M}\p{N}_]+/gu; -const warned = new WeakSet(); +const warned = /* @__PURE__ */ new WeakSet(); function warnNonGlobal(regex) { - if (!warned.has(regex)) { - warned.add(regex); - console.warn(`[Fuse] tokenize regex ${regex} lacks the global flag; only the ` + `first match per text will be returned. Add the 'g' flag.`); - } + if (!warned.has(regex)) { + warned.add(regex); + console.warn(`[Fuse] tokenize regex ${regex} lacks the global flag; only the first match per text will be returned. Add the 'g' flag.`); + } } function resolveTokenize(tokenize) { - if (typeof tokenize === 'function') { - let validated = false; - return text => { - const result = tokenize(text); - if (!validated) { - validated = true; - if (!Array.isArray(result) || result.some(t => typeof t !== 'string')) { - throw new Error(`[Fuse] tokenize function must return string[]; received ${Array.isArray(result) ? 'array containing non-strings' : typeof result}.`); - } - } - return result; - }; - } - if (tokenize instanceof RegExp) { - if (!tokenize.global) warnNonGlobal(tokenize); - return text => text.match(tokenize) || []; - } - return text => text.match(DEFAULT_TOKEN) || []; + if (typeof tokenize === "function") { + let validated = false; + return (text) => { + const result = tokenize(text); + if (!validated) { + validated = true; + if (!Array.isArray(result) || result.some((t) => typeof t !== "string")) throw new Error(`[Fuse] tokenize function must return string[]; received ${Array.isArray(result) ? "array containing non-strings" : typeof result}.`); + } + return result; + }; + } + if (tokenize instanceof RegExp) { + if (!tokenize.global) warnNonGlobal(tokenize); + return (text) => text.match(tokenize) || []; + } + return (text) => text.match(DEFAULT_TOKEN) || []; } -function createAnalyzer({ - isCaseSensitive = false, - ignoreDiacritics = false, - tokenize -} = {}) { - const tokenizeFn = resolveTokenize(tokenize); - return { - tokenize(text) { - if (!isCaseSensitive) { - text = text.toLowerCase(); - } - if (ignoreDiacritics) { - text = stripDiacritics(text); - } - return tokenizeFn(text); - } - }; +function createAnalyzer({ isCaseSensitive = false, ignoreDiacritics = false, tokenize } = {}) { + const tokenizeFn = resolveTokenize(tokenize); + return { tokenize(text) { + if (!isCaseSensitive) text = text.toLowerCase(); + if (ignoreDiacritics) text = stripDiacritics(text); + return tokenizeFn(text); + } }; } -// `tokenMatch: 'all'` packs per-term coverage into a bitmask. JS bitwise ops -// are 32-bit *signed*, so bit 31 is the sign bit — only bits 0..30 are safe. -// Queries with more than this many terms fall back to a Set (no bit limit). -const MAX_MASK_TERMS = 31; - -// Stats-only inverted index for token search (per Plan 008 Direction B). -// -// The query path consumes only `df` and `fieldCount` (IDF weighting). The -// per-doc maps exist solely to keep `df` and `fieldCount` correct under -// `add` / `remove` / `removeAt`: -// -// docFieldCount[doc] = # distinct fields the doc contributed; subtracted -// from `fieldCount` on remove. -// docTermFieldHits[doc] = Map; each entry decrements `df[term]` by -// that count on remove. -// -// `df` is incremented once per (doc, term, field) at index time. Removing a -// doc decrements `df` by the same count, mirroring the increment exactly. - +//#endregion +//#region src/search/token/InvertedIndex.ts function addField(index, text, docIdx, analyzer) { - const tokens = analyzer.tokenize(text); - if (!tokens.length) return; - index.fieldCount++; - index.docFieldCount.set(docIdx, (index.docFieldCount.get(docIdx) || 0) + 1); - - // We count each (doc, term, field) once — repeated occurrences within the - // same field don't multiply df. - const distinctTerms = new Set(tokens); - let perDocTerms = index.docTermFieldHits.get(docIdx); - if (!perDocTerms) { - perDocTerms = new Map(); - index.docTermFieldHits.set(docIdx, perDocTerms); - } - for (const term of distinctTerms) { - perDocTerms.set(term, (perDocTerms.get(term) || 0) + 1); - index.df.set(term, (index.df.get(term) || 0) + 1); - } + const tokens = analyzer.tokenize(text); + if (!tokens.length) return; + index.fieldCount++; + index.docFieldCount.set(docIdx, (index.docFieldCount.get(docIdx) || 0) + 1); + const distinctTerms = new Set(tokens); + let perDocTerms = index.docTermFieldHits.get(docIdx); + if (!perDocTerms) { + perDocTerms = /* @__PURE__ */ new Map(); + index.docTermFieldHits.set(docIdx, perDocTerms); + } + for (const term of distinctTerms) { + perDocTerms.set(term, (perDocTerms.get(term) || 0) + 1); + index.df.set(term, (index.df.get(term) || 0) + 1); + } } function ingestRecord(index, record, keyCount, analyzer) { - const { - i: docIdx, - v, - $: fields - } = record; - if (v !== undefined) { - addField(index, v, docIdx, analyzer); - return; - } - if (!fields) return; - for (let keyIdx = 0; keyIdx < keyCount; keyIdx++) { - const value = fields[keyIdx]; - if (!value) continue; - if (Array.isArray(value)) { - for (const sub of value) addField(index, sub.v, docIdx, analyzer); - } else { - addField(index, value.v, docIdx, analyzer); - } - } + const { i: docIdx, v, $: fields } = record; + if (v !== void 0) { + addField(index, v, docIdx, analyzer); + return; + } + if (!fields) return; + for (let keyIdx = 0; keyIdx < keyCount; keyIdx++) { + const value = fields[keyIdx]; + if (!value) continue; + if (Array.isArray(value)) for (const sub of value) addField(index, sub.v, docIdx, analyzer); + else addField(index, value.v, docIdx, analyzer); + } } function buildInvertedIndex(records, keyCount, analyzer) { - const index = { - fieldCount: 0, - df: new Map(), - docFieldCount: new Map(), - docTermFieldHits: new Map() - }; - for (const record of records) { - ingestRecord(index, record, keyCount, analyzer); - } - return index; + const index = { + fieldCount: 0, + df: /* @__PURE__ */ new Map(), + docFieldCount: /* @__PURE__ */ new Map(), + docTermFieldHits: /* @__PURE__ */ new Map() + }; + for (const record of records) ingestRecord(index, record, keyCount, analyzer); + return index; } function addToInvertedIndex(index, record, keyCount, analyzer) { - ingestRecord(index, record, keyCount, analyzer); + ingestRecord(index, record, keyCount, analyzer); } function removeFromInvertedIndex(index, docIdx) { - const fieldCount = index.docFieldCount.get(docIdx); - if (fieldCount === undefined) return; - index.fieldCount -= fieldCount; - index.docFieldCount.delete(docIdx); - const perDocTerms = index.docTermFieldHits.get(docIdx); - if (!perDocTerms) return; - for (const [term, hits] of perDocTerms) { - const next = (index.df.get(term) || 0) - hits; - if (next <= 0) { - index.df.delete(term); - } else { - index.df.set(term, next); - } - } - index.docTermFieldHits.delete(docIdx); + const fieldCount = index.docFieldCount.get(docIdx); + if (fieldCount === void 0) return; + index.fieldCount -= fieldCount; + index.docFieldCount.delete(docIdx); + const perDocTerms = index.docTermFieldHits.get(docIdx); + if (!perDocTerms) return; + for (const [term, hits] of perDocTerms) { + const next = (index.df.get(term) || 0) - hits; + if (next <= 0) index.df.delete(term); + else index.df.set(term, next); + } + index.docTermFieldHits.delete(docIdx); } - -// Removes the given docIdx entries and renumbers the remaining per-doc maps -// so they stay in sync with FuseIndex's contiguous renumbering on remove. function removeAndShiftInvertedIndex(index, removedIndices) { - if (removedIndices.length === 0) return; - - // De-dup and sort so the shift computation is O(log k) per lookup. - const sorted = Array.from(new Set(removedIndices)).sort((a, b) => a - b); - for (const idx of sorted) { - removeFromInvertedIndex(index, idx); - } - - // For any surviving oldIdx, its new idx is oldIdx minus the number of - // removed indices strictly less than oldIdx. - const shift = oldIdx => { - let lo = 0; - let hi = sorted.length; - while (lo < hi) { - const mid = lo + hi >>> 1; - if (sorted[mid] < oldIdx) lo = mid + 1;else hi = mid; - } - return oldIdx - lo; - }; - const firstRemoved = sorted[0]; - const shiftedDocFieldCount = new Map(); - for (const [oldKey, count] of index.docFieldCount) { - shiftedDocFieldCount.set(oldKey > firstRemoved ? shift(oldKey) : oldKey, count); - } - index.docFieldCount = shiftedDocFieldCount; - const shiftedDocTermFieldHits = new Map(); - for (const [oldKey, terms] of index.docTermFieldHits) { - shiftedDocTermFieldHits.set(oldKey > firstRemoved ? shift(oldKey) : oldKey, terms); - } - index.docTermFieldHits = shiftedDocTermFieldHits; + if (removedIndices.length === 0) return; + const sorted = Array.from(new Set(removedIndices)).sort((a, b) => a - b); + for (const idx of sorted) removeFromInvertedIndex(index, idx); + const shift = (oldIdx) => { + let lo = 0; + let hi = sorted.length; + while (lo < hi) { + const mid = lo + hi >>> 1; + if (sorted[mid] < oldIdx) lo = mid + 1; + else hi = mid; + } + return oldIdx - lo; + }; + const firstRemoved = sorted[0]; + const shiftedDocFieldCount = /* @__PURE__ */ new Map(); + for (const [oldKey, count] of index.docFieldCount) shiftedDocFieldCount.set(oldKey > firstRemoved ? shift(oldKey) : oldKey, count); + index.docFieldCount = shiftedDocFieldCount; + const shiftedDocTermFieldHits = /* @__PURE__ */ new Map(); + for (const [oldKey, terms] of index.docTermFieldHits) shiftedDocTermFieldHits.set(oldKey > firstRemoved ? shift(oldKey) : oldKey, terms); + index.docTermFieldHits = shiftedDocTermFieldHits; } -class Fuse { - // Statics are assigned in entry.ts - - constructor(docs, options, index) { - this.options = { - ...Config, - ...options - }; - if (this.options.useExtendedSearch && true) { - throw new Error(EXTENDED_SEARCH_UNAVAILABLE); - } - if (this.options.useTokenSearch && true) { - throw new Error(TOKEN_SEARCH_UNAVAILABLE); - } - this._keyStore = new KeyStore(this.options.keys); - this._docs = docs; - this._myIndex = null; - this._invertedIndex = null; - this.setCollection(docs, index); - this._lastQuery = null; - this._lastSearcher = null; - } - _getSearcher(query) { - if (this._lastQuery === query) { - return this._lastSearcher; - } - const opts = this._invertedIndex ? { - ...this.options, - _invertedIndex: this._invertedIndex - } : this.options; - const searcher = createSearcher(query, opts); - this._lastQuery = query; - this._lastSearcher = searcher; - return searcher; - } - setCollection(docs, index) { - this._docs = docs; - if (index && !(index instanceof FuseIndex)) { - throw new Error(INCORRECT_INDEX_TYPE); - } - this._myIndex = index || createIndex(this.options.keys, this._docs, { - getFn: this.options.getFn, - fieldNormWeight: this.options.fieldNormWeight - }); - if (this.options.useTokenSearch) { - const analyzer = createAnalyzer({ - isCaseSensitive: this.options.isCaseSensitive, - ignoreDiacritics: this.options.ignoreDiacritics, - tokenize: this.options.tokenize - }); - this._invertedIndex = buildInvertedIndex(this._myIndex.records, this._myIndex.keys.length, analyzer); - } - this._invalidateSearcherCache(); - } - add(doc) { - if (!isDefined(doc)) { - return; - } - this._docs.push(doc); - const record = this._myIndex.add(doc, this._docs.length - 1); - - // Skip inverted-index bookkeeping when no record was appended (blank - // strings produce null). The previous code read `records[records.length-1]` - // unconditionally, which would re-ingest the previous doc on `add("")`. - if (this._invertedIndex && record) { - const analyzer = createAnalyzer({ - isCaseSensitive: this.options.isCaseSensitive, - ignoreDiacritics: this.options.ignoreDiacritics, - tokenize: this.options.tokenize - }); - addToInvertedIndex(this._invertedIndex, record, this._myIndex.keys.length, analyzer); - } - this._invalidateSearcherCache(); - } - remove(predicate = () => false) { - const results = []; - const indicesToRemove = []; - for (let i = 0, len = this._docs.length; i < len; i += 1) { - if (predicate(this._docs[i], i)) { - results.push(this._docs[i]); - indicesToRemove.push(i); - } - } - if (indicesToRemove.length) { - if (this._invertedIndex) { - removeAndShiftInvertedIndex(this._invertedIndex, indicesToRemove); - } - - // Filter docs in a single pass instead of reverse-splicing - const toRemove = new Set(indicesToRemove); - this._docs = this._docs.filter((_, i) => !toRemove.has(i)); - this._myIndex.removeAll(indicesToRemove); - this._invalidateSearcherCache(); - } - return results; - } - removeAt(idx) { - // Validate before any mutation. The previous code spliced `_docs` first - // and let FuseIndex.removeAt throw afterward — partial-state on invalid - // input. Atomic now. - if (!Number.isInteger(idx) || idx < 0 || idx >= this._docs.length) { - throw new Error(INVALID_DOC_INDEX); - } - if (this._invertedIndex) { - removeAndShiftInvertedIndex(this._invertedIndex, [idx]); - } - const doc = this._docs.splice(idx, 1)[0]; - this._myIndex.removeAt(idx); - this._invalidateSearcherCache(); - return doc; - } - _invalidateSearcherCache() { - this._lastQuery = null; - this._lastSearcher = null; - } - getIndex() { - return this._myIndex; - } - search(query, options) { - const { - limit = -1 - } = options || {}; - const { - includeMatches, - includeScore, - shouldSort, - sortFn, - ignoreFieldNorm - } = this.options; - - // Empty string query returns all docs (useful for search UIs) - if (isString(query) && !query.trim()) { - let docs = this._docs.map((item, idx) => ({ - item, - refIndex: idx - })); - if (isNumber(limit) && limit > -1) { - docs = docs.slice(0, limit); - } - return docs; - } - const useHeap = isNumber(limit) && limit > 0 && isString(query); - let results; - if (useHeap) { - const heap = new MaxHeap(limit); - if (isString(this._docs[0])) { - this._searchStringList(query, { - heap, - ignoreFieldNorm - }); - } else { - this._searchObjectList(query, { - heap, - ignoreFieldNorm - }); - } - results = heap.extractSorted(sortFn); - } else { - results = isString(query) ? isString(this._docs[0]) ? this._searchStringList(query) : this._searchObjectList(query) : this._searchLogical(query); - computeScore(results, { - ignoreFieldNorm - }); - if (shouldSort) { - results.sort(sortFn); - } - if (isNumber(limit) && limit > -1) { - results = results.slice(0, limit); - } - } - return format(results, this._docs, { - includeMatches, - includeScore - }); - } - _searchStringList(query, { - heap, - ignoreFieldNorm - } = {}) { - const searcher = this._getSearcher(query); - const requireAllTokens = this.options.useTokenSearch && this.options.tokenMatch === 'all'; - const { - records - } = this._myIndex; - const results = heap ? null : []; - - // Iterate over every string in the index - records.forEach(({ - v: text, - i: idx, - n: norm - }) => { - if (!isDefined(text)) { - return; - } - const searchResult = searcher.searchIn(text); - if (searchResult.isMatch) { - const match = { - score: searchResult.score, - value: text, - norm: norm, - indices: searchResult.indices - }; - if (requireAllTokens) { - match.matchedMask = searchResult.matchedMask; - match.matchedTerms = searchResult.matchedTerms; - match.termCount = searchResult.termCount; - } - const matches = [match]; - - // Record-level AND gate (token search `tokenMatch: 'all'`), applied - // before heap insertion so `limit` returns the same top-N as unlimited. - if (!requireAllTokens || this._coversAllTokens(matches)) { - const result = { - item: text, - idx, - matches - }; - if (heap) { - result.score = computeScoreSingle(result.matches, { - ignoreFieldNorm - }); - if (heap.shouldInsert(result.score)) { - heap.insert(result); - } - } else { - results.push(result); - } - } - } - }); - return results; - } - _searchLogical(query) { - { - throw new Error(LOGICAL_SEARCH_UNAVAILABLE); - } - } - - // When a search involves inverse patterns (e.g. !Syrup), the aggregation - // across keys switches from "ANY key matches" to "ALL keys must match." - // This is signaled by hasInverse on the SearchResult from ExtendedSearch. - // - // For mixed patterns like "^hello !Syrup", a key failure is ambiguous — - // it could be the positive or inverse term that failed. In that case we - // conservatively exclude the item, which is strictly better than the old - // behavior of including it. See: https://github.com/krisk/Fuse/issues/712 - _searchObjectList(query, { - heap, - ignoreFieldNorm - } = {}) { - const searcher = this._getSearcher(query); - const requireAllTokens = this.options.useTokenSearch && this.options.tokenMatch === 'all'; - const { - keys, - records - } = this._myIndex; - const results = heap ? null : []; - - // List is Array - records.forEach(({ - $: item, - i: idx - }) => { - if (!isDefined(item)) { - return; - } - const matches = []; - let anyKeyFailed = false; - let hasInverse = false; - - // Iterate over every key (i.e, path), and fetch the value at that key - keys.forEach((key, keyIndex) => { - const keyMatches = this._findMatches({ - key, - value: item[keyIndex], - searcher - }); - if (keyMatches.length) { - matches.push(...keyMatches); - if (keyMatches[0].hasInverse) { - hasInverse = true; - } - } else { - anyKeyFailed = true; - } - }); - - // If the search involves inverse patterns, ALL keys must match - if (hasInverse && anyKeyFailed) { - return; - } - - // Record-level AND gate (token search `tokenMatch: 'all'`): every query - // term must be covered across the record's field/array-element matches. - // Applied before heap insertion so `limit` returns the same top-N. - if (matches.length && (!requireAllTokens || this._coversAllTokens(matches))) { - const result = { - idx, - item, - matches - }; - if (heap) { - result.score = computeScoreSingle(result.matches, { - ignoreFieldNorm - }); - if (heap.shouldInsert(result.score)) { - heap.insert(result); - } - } else { - results.push(result); - } - } - }); - return results; - } - _findMatches({ - key, - value, - searcher - }) { - if (!isDefined(value)) { - return []; - } - const matches = []; - if (isArray(value)) { - value.forEach(({ - v: text, - i: idx, - n: norm - }) => { - if (!isDefined(text)) { - return; - } - const searchResult = searcher.searchIn(text); - if (searchResult.isMatch) { - const match = { - score: searchResult.score, - key, - value: text, - idx, - norm, - indices: searchResult.indices, - hasInverse: searchResult.hasInverse - }; - // Carry token-search AND coverage only when present, so the default - // (non-token / 'any') MatchScore keeps its original object shape. - if (searchResult.termCount !== undefined) { - match.matchedMask = searchResult.matchedMask; - match.matchedTerms = searchResult.matchedTerms; - match.termCount = searchResult.termCount; - } - matches.push(match); - } - }); - } else { - const { - v: text, - n: norm - } = value; - const searchResult = searcher.searchIn(text); - if (searchResult.isMatch) { - const match = { - score: searchResult.score, - key, - value: text, - norm, - indices: searchResult.indices, - hasInverse: searchResult.hasInverse - }; - if (searchResult.termCount !== undefined) { - match.matchedMask = searchResult.matchedMask; - match.matchedTerms = searchResult.matchedTerms; - match.termCount = searchResult.termCount; - } - matches.push(match); - } - } - return matches; - } - - // Record-level AND gate for token search (`tokenMatch: 'all'`). Returns true - // unless the matched terms across ALL of a record's field/array-element - // matches fail to cover every query term. `termCount` is only set by - // TokenSearch in 'all' mode, so non-token / 'any' searches always pass. - _coversAllTokens(matches) { - const termCount = matches.length ? matches[0].termCount : undefined; - if (termCount === undefined) { - return true; - } - if (termCount <= MAX_MASK_TERMS) { - let coverage = 0; - for (let i = 0; i < matches.length; i++) { - coverage |= matches[i].matchedMask || 0; - } - return coverage === 2 ** termCount - 1; - } - const coverage = new Set(); - for (let i = 0; i < matches.length; i++) { - const terms = matches[i].matchedTerms; - if (terms) { - for (const t of terms) { - coverage.add(t); - } - } - } - return coverage.size === termCount; - } -} +//#endregion +//#region src/core/index.ts +var Fuse = class { + constructor(docs, options, index) { + this.options = { + ...Config, + ...options + }; + if (this.options.useExtendedSearch && true) throw new Error(EXTENDED_SEARCH_UNAVAILABLE); + if (this.options.useTokenSearch && true) throw new Error(TOKEN_SEARCH_UNAVAILABLE); + this._keyStore = new KeyStore(this.options.keys); + this._docs = docs; + this._myIndex = null; + this._invertedIndex = null; + this.setCollection(docs, index); + this._lastQuery = null; + this._lastSearcher = null; + } + _getSearcher(query) { + if (this._lastQuery === query) return this._lastSearcher; + const searcher = createSearcher(query, this._invertedIndex ? { + ...this.options, + _invertedIndex: this._invertedIndex + } : this.options); + this._lastQuery = query; + this._lastSearcher = searcher; + return searcher; + } + setCollection(docs, index) { + this._docs = docs; + if (index && !(index instanceof FuseIndex)) throw new Error(INCORRECT_INDEX_TYPE); + this._myIndex = index || createIndex(this.options.keys, this._docs, { + getFn: this.options.getFn, + fieldNormWeight: this.options.fieldNormWeight + }); + if (this.options.useTokenSearch) { + const analyzer = createAnalyzer({ + isCaseSensitive: this.options.isCaseSensitive, + ignoreDiacritics: this.options.ignoreDiacritics, + tokenize: this.options.tokenize + }); + this._invertedIndex = buildInvertedIndex(this._myIndex.records, this._myIndex.keys.length, analyzer); + } + this._invalidateSearcherCache(); + } + add(doc) { + if (!isDefined(doc)) return; + this._docs.push(doc); + const record = this._myIndex.add(doc, this._docs.length - 1); + if (this._invertedIndex && record) { + const analyzer = createAnalyzer({ + isCaseSensitive: this.options.isCaseSensitive, + ignoreDiacritics: this.options.ignoreDiacritics, + tokenize: this.options.tokenize + }); + addToInvertedIndex(this._invertedIndex, record, this._myIndex.keys.length, analyzer); + } + this._invalidateSearcherCache(); + } + remove(predicate = () => false) { + const results = []; + const indicesToRemove = []; + for (let i = 0, len = this._docs.length; i < len; i += 1) if (predicate(this._docs[i], i)) { + results.push(this._docs[i]); + indicesToRemove.push(i); + } + if (indicesToRemove.length) { + if (this._invertedIndex) removeAndShiftInvertedIndex(this._invertedIndex, indicesToRemove); + const toRemove = new Set(indicesToRemove); + this._docs = this._docs.filter((_, i) => !toRemove.has(i)); + this._myIndex.removeAll(indicesToRemove); + this._invalidateSearcherCache(); + } + return results; + } + removeAt(idx) { + if (!Number.isInteger(idx) || idx < 0 || idx >= this._docs.length) throw new Error(INVALID_DOC_INDEX); + if (this._invertedIndex) removeAndShiftInvertedIndex(this._invertedIndex, [idx]); + const doc = this._docs.splice(idx, 1)[0]; + this._myIndex.removeAt(idx); + this._invalidateSearcherCache(); + return doc; + } + _invalidateSearcherCache() { + this._lastQuery = null; + this._lastSearcher = null; + } + getIndex() { + return this._myIndex; + } + search(query, options) { + const { limit = -1 } = options || {}; + const { includeMatches, includeScore, shouldSort, sortFn, ignoreFieldNorm } = this.options; + if (isString(query) && !query.trim()) { + let docs = this._docs.map((item, idx) => ({ + item, + refIndex: idx + })); + if (isNumber(limit) && limit > -1) docs = docs.slice(0, limit); + return docs; + } + const useHeap = isNumber(limit) && limit > 0 && isString(query); + let results; + if (useHeap) { + const heap = new MaxHeap(limit); + if (isString(this._docs[0])) this._searchStringList(query, { + heap, + ignoreFieldNorm + }); + else this._searchObjectList(query, { + heap, + ignoreFieldNorm + }); + results = heap.extractSorted(sortFn); + } else { + results = isString(query) ? isString(this._docs[0]) ? this._searchStringList(query) : this._searchObjectList(query) : this._searchLogical(query); + computeScore(results, { ignoreFieldNorm }); + if (shouldSort) results.sort(sortFn); + if (isNumber(limit) && limit > -1) results = results.slice(0, limit); + } + return format(results, this._docs, { + includeMatches, + includeScore + }); + } + _searchStringList(query, { heap, ignoreFieldNorm } = {}) { + const searcher = this._getSearcher(query); + const requireAllTokens = this.options.useTokenSearch && this.options.tokenMatch === "all"; + const { records } = this._myIndex; + const results = heap ? null : []; + records.forEach(({ v: text, i: idx, n: norm }) => { + if (!isDefined(text)) return; + const searchResult = searcher.searchIn(text); + if (searchResult.isMatch) { + const match = { + score: searchResult.score, + value: text, + norm, + indices: searchResult.indices + }; + if (requireAllTokens) { + match.matchedMask = searchResult.matchedMask; + match.matchedTerms = searchResult.matchedTerms; + match.termCount = searchResult.termCount; + } + const matches = [match]; + if (!requireAllTokens || this._coversAllTokens(matches)) { + const result = { + item: text, + idx, + matches + }; + if (heap) { + result.score = computeScoreSingle(result.matches, { ignoreFieldNorm }); + if (heap.shouldInsert(result.score)) heap.insert(result); + } else results.push(result); + } + } + }); + return results; + } + _searchLogical(query) { + throw new Error(LOGICAL_SEARCH_UNAVAILABLE); + } + _searchObjectList(query, { heap, ignoreFieldNorm } = {}) { + const searcher = this._getSearcher(query); + const requireAllTokens = this.options.useTokenSearch && this.options.tokenMatch === "all"; + const { keys, records } = this._myIndex; + const results = heap ? null : []; + records.forEach(({ $: item, i: idx }) => { + if (!isDefined(item)) return; + const matches = []; + let anyKeyFailed = false; + let hasInverse = false; + keys.forEach((key, keyIndex) => { + const keyMatches = this._findMatches({ + key, + value: item[keyIndex], + searcher + }); + if (keyMatches.length) { + matches.push(...keyMatches); + if (keyMatches[0].hasInverse) hasInverse = true; + } else anyKeyFailed = true; + }); + if (hasInverse && anyKeyFailed) return; + if (matches.length && (!requireAllTokens || this._coversAllTokens(matches))) { + const result = { + idx, + item, + matches + }; + if (heap) { + result.score = computeScoreSingle(result.matches, { ignoreFieldNorm }); + if (heap.shouldInsert(result.score)) heap.insert(result); + } else results.push(result); + } + }); + return results; + } + _findMatches({ key, value, searcher }) { + if (!isDefined(value)) return []; + const matches = []; + if (isArray(value)) value.forEach(({ v: text, i: idx, n: norm }) => { + if (!isDefined(text)) return; + const searchResult = searcher.searchIn(text); + if (searchResult.isMatch) { + const match = { + score: searchResult.score, + key, + value: text, + idx, + norm, + indices: searchResult.indices, + hasInverse: searchResult.hasInverse + }; + if (searchResult.termCount !== void 0) { + match.matchedMask = searchResult.matchedMask; + match.matchedTerms = searchResult.matchedTerms; + match.termCount = searchResult.termCount; + } + matches.push(match); + } + }); + else { + const { v: text, n: norm } = value; + const searchResult = searcher.searchIn(text); + if (searchResult.isMatch) { + const match = { + score: searchResult.score, + key, + value: text, + norm, + indices: searchResult.indices, + hasInverse: searchResult.hasInverse + }; + if (searchResult.termCount !== void 0) { + match.matchedMask = searchResult.matchedMask; + match.matchedTerms = searchResult.matchedTerms; + match.termCount = searchResult.termCount; + } + matches.push(match); + } + } + return matches; + } + _coversAllTokens(matches) { + const termCount = matches.length ? matches[0].termCount : void 0; + if (termCount === void 0) return true; + if (termCount <= 31) { + let coverage = 0; + for (let i = 0; i < matches.length; i++) coverage |= matches[i].matchedMask || 0; + return coverage === 2 ** termCount - 1; + } + const coverage = /* @__PURE__ */ new Set(); + for (let i = 0; i < matches.length; i++) { + const terms = matches[i].matchedTerms; + if (terms) for (const t of terms) coverage.add(t); + } + return coverage.size === termCount; + } +}; -Fuse.version = '7.4.1'; +//#endregion +//#region src/entry.ts +Fuse.version = "7.4.1"; Fuse.createIndex = createIndex; Fuse.parseIndex = parseIndex; Fuse.config = Config; -Fuse.match = function (pattern, text, options) { - // Token search needs corpus statistics (df, fieldCount) that a one-off - // string comparison can't provide. Reject it here so the contract is the - // same in the full and basic builds — without this guard, the full build - // crashes with an opaque TypeError and the basic build silently falls back - // to fuzzy matching. - if (options && options.useTokenSearch) { - throw new Error(FUSE_MATCH_TOKEN_SEARCH_UNSUPPORTED); - } - const searcher = createSearcher(pattern, { - ...Config, - ...options - }); - return searcher.searchIn(text); +Fuse.match = function(pattern, text, options) { + if (options && options.useTokenSearch) throw new Error(FUSE_MATCH_TOKEN_SEARCH_UNSUPPORTED); + return createSearcher(pattern, { + ...Config, + ...options + }).searchIn(text); }; -{ - Fuse.parseQuery = parse; -} -Fuse.use = function (...plugins) { - plugins.forEach(plugin => register(plugin)); +Fuse.parseQuery = parse; +Fuse.use = function(...plugins) { + plugins.forEach((plugin) => register(plugin)); }; +var entry_default = Fuse; -// Re-export public types - -export { Fuse as default }; +//#endregion +export { entry_default as default }; \ No newline at end of file diff --git a/dist/fuse.cjs b/dist/fuse.cjs index 51880e0c1..3971633a8 100644 --- a/dist/fuse.cjs +++ b/dist/fuse.cjs @@ -7,2255 +7,1618 @@ * http://www.apache.org/licenses/LICENSE-2.0 */ -'use strict'; - +//#region src/helpers/typeGuards.ts function isArray(value) { - return !Array.isArray ? getTag(value) === '[object Array]' : Array.isArray(value); + return !Array.isArray ? getTag(value) === "[object Array]" : Array.isArray(value); } function baseToString(value) { - // Exit early for strings to avoid a performance hit in some environments. - if (typeof value == 'string') { - return value; - } - if (typeof value === 'bigint') { - return value.toString(); - } - const result = value + ''; - return result == '0' && 1 / value == -Infinity ? '-0' : result; + if (typeof value == "string") return value; + if (typeof value === "bigint") return value.toString(); + const result = value + ""; + return result == "0" && 1 / value == -Infinity ? "-0" : result; } function toString(value) { - return value == null ? '' : baseToString(value); + return value == null ? "" : baseToString(value); } function isString(value) { - return typeof value === 'string'; + return typeof value === "string"; } function isNumber(value) { - return typeof value === 'number'; + return typeof value === "number"; } - -// Adapted from: https://github.com/lodash/lodash/blob/master/isBoolean.js function isBoolean(value) { - return value === true || value === false || isObjectLike(value) && getTag(value) == '[object Boolean]'; + return value === true || value === false || isObjectLike(value) && getTag(value) == "[object Boolean]"; } function isObject(value) { - return typeof value === 'object'; + return typeof value === "object"; } - -// Checks if `value` is object-like. function isObjectLike(value) { - return isObject(value) && value !== null; + return isObject(value) && value !== null; } function isDefined(value) { - return value !== undefined && value !== null; + return value !== void 0 && value !== null; } function isBlank(value) { - return !value.trim().length; + return !value.trim().length; } - -// Gets the `toStringTag` of `value`. -// Adapted from: https://github.com/lodash/lodash/blob/master/.internal/getTag.js function getTag(value) { - return value == null ? value === undefined ? '[object Undefined]' : '[object Null]' : Object.prototype.toString.call(value); + return value == null ? value === void 0 ? "[object Undefined]" : "[object Null]" : Object.prototype.toString.call(value); } +//#endregion +//#region src/core/errorMessages.ts const INCORRECT_INDEX_TYPE = "Incorrect 'index' type"; -const INVALID_DOC_INDEX = 'Invalid doc index: must be a non-negative integer within the bounds of the docs array'; -const LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY = key => `Invalid value for key ${key}`; -const PATTERN_LENGTH_TOO_LARGE = max => `Pattern length exceeds max of ${max}.`; -const MISSING_KEY_PROPERTY = name => `Missing ${name} property in key`; -const INVALID_KEY_WEIGHT_VALUE = key => `Property 'weight' in key '${key}' must be a positive integer`; -const FUSE_MATCH_TOKEN_SEARCH_UNSUPPORTED = `Fuse.match does not support useTokenSearch: token search requires ` + `corpus-level statistics (df, fieldCount) that a one-off string ` + `comparison does not have. Use new Fuse(...).search(...) instead.`; - +const INVALID_DOC_INDEX = "Invalid doc index: must be a non-negative integer within the bounds of the docs array"; +const LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY = (key) => `Invalid value for key ${key}`; +const PATTERN_LENGTH_TOO_LARGE = (max) => `Pattern length exceeds max of ${max}.`; +const MISSING_KEY_PROPERTY = (name) => `Missing ${name} property in key`; +const INVALID_KEY_WEIGHT_VALUE = (key) => `Property 'weight' in key '${key}' must be a positive integer`; +const FUSE_MATCH_TOKEN_SEARCH_UNSUPPORTED = "Fuse.match does not support useTokenSearch: token search requires corpus-level statistics (df, fieldCount) that a one-off string comparison does not have. Use new Fuse(...).search(...) instead."; + +//#endregion +//#region src/tools/KeyStore.ts const hasOwn = Object.prototype.hasOwnProperty; -class KeyStore { - constructor(keys) { - this._keys = []; - this._keyMap = {}; - let totalWeight = 0; - keys.forEach(key => { - const obj = createKey(key); - this._keys.push(obj); - this._keyMap[obj.id] = obj; - totalWeight += obj.weight; - }); - - // Normalize weights so that their sum is equal to 1 - this._keys.forEach(key => { - key.weight /= totalWeight; - }); - } - get(keyId) { - return this._keyMap[keyId]; - } - keys() { - return this._keys; - } - toJSON() { - return JSON.stringify(this._keys); - } -} +var KeyStore = class { + constructor(keys) { + this._keys = []; + this._keyMap = {}; + let totalWeight = 0; + keys.forEach((key) => { + const obj = createKey(key); + this._keys.push(obj); + this._keyMap[obj.id] = obj; + totalWeight += obj.weight; + }); + this._keys.forEach((key) => { + key.weight /= totalWeight; + }); + } + get(keyId) { + return this._keyMap[keyId]; + } + keys() { + return this._keys; + } + toJSON() { + return JSON.stringify(this._keys); + } +}; function createKey(key) { - let path = null; - let id = null; - let src = null; - let weight = 1; - let getFn = null; - if (isString(key) || isArray(key)) { - src = key; - path = createKeyPath(key); - id = createKeyId(key); - } else { - if (!hasOwn.call(key, 'name')) { - throw new Error(MISSING_KEY_PROPERTY('name')); - } - const name = key.name; - src = name; - if (hasOwn.call(key, 'weight') && key.weight !== undefined) { - weight = key.weight; - if (weight <= 0) { - throw new Error(INVALID_KEY_WEIGHT_VALUE(createKeyId(name))); - } - } - path = createKeyPath(name); - id = createKeyId(name); - getFn = key.getFn ?? null; - } - return { - path: path, - id: id, - weight, - src: src, - getFn - }; + let path = null; + let id = null; + let src = null; + let weight = 1; + let getFn = null; + if (isString(key) || isArray(key)) { + src = key; + path = createKeyPath(key); + id = createKeyId(key); + } else { + if (!hasOwn.call(key, "name")) throw new Error(MISSING_KEY_PROPERTY("name")); + const name = key.name; + src = name; + if (hasOwn.call(key, "weight") && key.weight !== void 0) { + weight = key.weight; + if (weight <= 0) throw new Error(INVALID_KEY_WEIGHT_VALUE(createKeyId(name))); + } + path = createKeyPath(name); + id = createKeyId(name); + getFn = key.getFn ?? null; + } + return { + path, + id, + weight, + src, + getFn + }; } function createKeyPath(key) { - return isArray(key) ? key : key.split('.'); + return isArray(key) ? key : key.split("."); } function createKeyId(key) { - return isArray(key) ? key.join('.') : key; + return isArray(key) ? key.join(".") : key; } +//#endregion +//#region src/helpers/get.ts function get(obj, path) { - const list = []; - let arr = false; - const deepGet = (obj, path, index, arrayIndex) => { - if (!isDefined(obj)) { - return; - } - if (!path[index]) { - // If there's no path left, we've arrived at the object we care about. - list.push(arrayIndex !== undefined ? { - v: obj, - i: arrayIndex - } : obj); - } else { - const key = path[index]; - const value = obj[key]; - if (!isDefined(value)) { - return; - } - - // If we're at the last value in the path, and if it's a string/number/bool, - // add it to the list - if (index === path.length - 1 && (isString(value) || isNumber(value) || isBoolean(value) || typeof value === 'bigint')) { - list.push(arrayIndex !== undefined ? { - v: toString(value), - i: arrayIndex - } : toString(value)); - } else if (isArray(value)) { - arr = true; - // Search each item in the array. - for (let i = 0, len = value.length; i < len; i += 1) { - deepGet(value[i], path, index + 1, i); - } - } else if (path.length) { - // An object. Recurse further. - deepGet(value, path, index + 1, arrayIndex); - } - } - }; - - // Backwards compatibility (since path used to be a string) - deepGet(obj, isString(path) ? path.split('.') : path, 0); - return arr ? list : list[0]; + const list = []; + let arr = false; + const deepGet = (obj, path, index, arrayIndex) => { + if (!isDefined(obj)) return; + if (!path[index]) list.push(arrayIndex !== void 0 ? { + v: obj, + i: arrayIndex + } : obj); + else { + const value = obj[path[index]]; + if (!isDefined(value)) return; + if (index === path.length - 1 && (isString(value) || isNumber(value) || isBoolean(value) || typeof value === "bigint")) list.push(arrayIndex !== void 0 ? { + v: toString(value), + i: arrayIndex + } : toString(value)); + else if (isArray(value)) { + arr = true; + for (let i = 0, len = value.length; i < len; i += 1) deepGet(value[i], path, index + 1, i); + } else if (path.length) deepGet(value, path, index + 1, arrayIndex); + } + }; + deepGet(obj, isString(path) ? path.split(".") : path, 0); + return arr ? list : list[0]; } +//#endregion +//#region src/core/config.ts const MatchOptions = { - includeMatches: false, - findAllMatches: false, - minMatchCharLength: 1 + includeMatches: false, + findAllMatches: false, + minMatchCharLength: 1 }; const BasicOptions = { - isCaseSensitive: false, - ignoreDiacritics: false, - includeScore: false, - keys: [], - shouldSort: true, - sortFn: (a, b) => a.score === b.score ? a.idx < b.idx ? -1 : 1 : a.score < b.score ? -1 : 1 + isCaseSensitive: false, + ignoreDiacritics: false, + includeScore: false, + keys: [], + shouldSort: true, + sortFn: (a, b) => a.score === b.score ? a.idx < b.idx ? -1 : 1 : a.score < b.score ? -1 : 1 }; const FuzzyOptions = { - location: 0, - threshold: 0.6, - distance: 100 + location: 0, + threshold: .6, + distance: 100 }; const AdvancedOptions = { - useExtendedSearch: false, - useTokenSearch: false, - tokenize: undefined, - tokenMatch: 'any', - getFn: get, - ignoreLocation: false, - ignoreFieldNorm: false, - fieldNormWeight: 1 + useExtendedSearch: false, + useTokenSearch: false, + tokenize: void 0, + tokenMatch: "any", + getFn: get, + ignoreLocation: false, + ignoreFieldNorm: false, + fieldNormWeight: 1 }; const Config = Object.freeze({ - ...BasicOptions, - ...MatchOptions, - ...FuzzyOptions, - ...AdvancedOptions + ...BasicOptions, + ...MatchOptions, + ...FuzzyOptions, + ...AdvancedOptions }); -// Field-length norm: the shorter the field, the higher the weight. -// Set to 3 decimals to reduce index size. +//#endregion +//#region src/tools/fieldNorm.ts function norm(weight = 1, mantissa = 3) { - const cache = new Map(); - const m = Math.pow(10, mantissa); - return { - get(value) { - // Count words by counting space transitions — avoids allocating a regex match array - let numTokens = 1; - let inSpace = false; - for (let i = 0; i < value.length; i++) { - if (value.charCodeAt(i) === 32) { - if (!inSpace) { - numTokens++; - inSpace = true; - } - } else { - inSpace = false; - } - } - if (cache.has(numTokens)) { - return cache.get(numTokens); - } - - // Default function is 1/sqrt(x), weight makes that variable - const n = Math.round(m / Math.pow(numTokens, 0.5 * weight)) / m; - cache.set(numTokens, n); - return n; - }, - clear() { - cache.clear(); - } - }; + const cache = /* @__PURE__ */ new Map(); + const m = Math.pow(10, mantissa); + return { + get(value) { + let numTokens = 1; + let inSpace = false; + for (let i = 0; i < value.length; i++) if (value.charCodeAt(i) === 32) { + if (!inSpace) { + numTokens++; + inSpace = true; + } + } else inSpace = false; + if (cache.has(numTokens)) return cache.get(numTokens); + const n = Math.round(m / Math.pow(numTokens, .5 * weight)) / m; + cache.set(numTokens, n); + return n; + }, + clear() { + cache.clear(); + } + }; } -class FuseIndex { - constructor({ - getFn = Config.getFn, - fieldNormWeight = Config.fieldNormWeight - } = {}) { - this.norm = norm(fieldNormWeight, 3); - this.getFn = getFn; - this.isCreated = false; - this.docs = []; - this.keys = []; - this._keysMap = {}; - this.setIndexRecords(); - } - setSources(docs = []) { - this.docs = docs; - } - setIndexRecords(records = []) { - this.records = records; - } - setKeys(keys = []) { - this.keys = keys; - this._keysMap = {}; - keys.forEach((key, idx) => { - this._keysMap[key.id] = idx; - }); - } - create() { - if (this.isCreated || !this.docs.length) { - return; - } - this.isCreated = true; - const len = this.docs.length; - this.records = new Array(len); - let recordCount = 0; - - // List is Array - if (isString(this.docs[0])) { - for (let i = 0; i < len; i++) { - const record = this._createStringRecord(this.docs[i], i); - if (record) { - this.records[recordCount++] = record; - } - } - } else { - // List is Array - for (let i = 0; i < len; i++) { - this.records[recordCount++] = this._createObjectRecord(this.docs[i], i); - } - } - this.records.length = recordCount; - this.norm.clear(); - } - // Appends a record for `doc` at `docIndex` (the doc's position in the source - // array). Returns the appended record, or null when `doc` is a blank string - // (those are skipped at record creation; see `_createStringRecord`). Callers - // use the return value to gate downstream bookkeeping like the inverted - // index, which must not be touched when no record was produced. - add(doc, docIndex) { - if (!Number.isInteger(docIndex) || docIndex < 0) { - throw new Error(INVALID_DOC_INDEX); - } - if (isString(doc)) { - const record = this._createStringRecord(doc, docIndex); - if (record) { - this.records.push(record); - } - return record; - } - const record = this._createObjectRecord(doc, docIndex); - this.records.push(record); - return record; - } - // Removes the record for the doc at the specified source-array (docs) index. - // Blank string docs have no record; callers may pass such an index and the - // splice is a no-op, but subsequent records still need their .i decremented - // to track the docs array that the caller is splicing in parallel. - removeAt(idx) { - if (!Number.isInteger(idx) || idx < 0) { - throw new Error(INVALID_DOC_INDEX); - } - - // Find and remove the record at this doc-index, if one exists. Records are - // typically sorted by .i but the algorithm doesn't depend on it — parsed - // indexes via setIndexRecords may arrive in arbitrary order. - for (let i = 0, len = this.records.length; i < len; i += 1) { - if (this.records[i].i === idx) { - this.records.splice(i, 1); - break; - } - } - - // Decrement every record whose source-array index is now stale. - for (let i = 0, len = this.records.length; i < len; i += 1) { - if (this.records[i].i > idx) { - this.records[i].i -= 1; - } - } - } - // Removes records for the docs at the specified source-array indices, then - // shifts every surviving record's .i down by the count of removed indices - // strictly less than it (mirrors removeAndShiftInvertedIndex's shift math). - // Invalid entries (non-integer, negative) in `indices` are dropped silently - // — removeAll's natural use case is "caller passed a list of matched doc - // indices"; asymmetric throw-vs-no-op would be more surprising than a clean - // filter. - removeAll(indices) { - const toRemove = new Set(); - for (const v of indices) { - if (Number.isInteger(v) && v >= 0) { - toRemove.add(v); - } - } - if (toRemove.size === 0) { - return; - } - this.records = this.records.filter(r => !toRemove.has(r.i)); - const sorted = Array.from(toRemove).sort((a, b) => a - b); - for (const record of this.records) { - // shift = count of removed indices strictly less than record.i - let lo = 0; - let hi = sorted.length; - while (lo < hi) { - const mid = lo + hi >>> 1; - if (sorted[mid] < record.i) lo = mid + 1;else hi = mid; - } - record.i -= lo; - } - } - getValueForItemAtKeyId(item, keyId) { - return item[this._keysMap[keyId]]; - } - size() { - return this.records.length; - } - _createStringRecord(doc, docIndex) { - if (!isDefined(doc) || isBlank(doc)) { - return null; - } - return { - v: doc, - i: docIndex, - n: this.norm.get(doc) - }; - } - _createObjectRecord(doc, docIndex) { - const record = { - i: docIndex, - $: {} - }; - - // Iterate over every key (i.e, path), and fetch the value at that key - for (let keyIndex = 0, keyLen = this.keys.length; keyIndex < keyLen; keyIndex++) { - const key = this.keys[keyIndex]; - const value = key.getFn ? key.getFn(doc) : this.getFn(doc, key.path); - if (!isDefined(value)) { - continue; - } - if (isArray(value)) { - const subRecords = []; - for (let i = 0, len = value.length; i < len; i += 1) { - const item = value[i]; - if (!isDefined(item)) { - continue; - } - if (isString(item)) { - // Custom getFn returning plain string array (backward compat) - if (!isBlank(item)) { - const subRecord = { - v: item, - i: i, - n: this.norm.get(item) - }; - subRecords.push(subRecord); - } - } else if (isDefined(item.v)) { - // Default get() returns {v, i} objects with original array indices - const text = isString(item.v) ? item.v : toString(item.v); - if (!isBlank(text)) { - const subRecord = { - v: text, - i: item.i, - n: this.norm.get(text) - }; - subRecords.push(subRecord); - } - } - } - record.$[keyIndex] = subRecords; - } else if (isString(value) && !isBlank(value)) { - const subRecord = { - v: value, - n: this.norm.get(value) - }; - record.$[keyIndex] = subRecord; - } - } - return record; - } - toJSON() { - return { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - keys: this.keys.map(({ - getFn, - ...key - }) => key), - records: this.records - }; - } -} -function createIndex(keys, docs, { - getFn = Config.getFn, - fieldNormWeight = Config.fieldNormWeight -} = {}) { - const myIndex = new FuseIndex({ - getFn, - fieldNormWeight - }); - myIndex.setKeys(keys.map(createKey)); - myIndex.setSources(docs); - myIndex.create(); - return myIndex; +//#endregion +//#region src/tools/FuseIndex.ts +var FuseIndex = class { + constructor({ getFn = Config.getFn, fieldNormWeight = Config.fieldNormWeight } = {}) { + this.norm = norm(fieldNormWeight, 3); + this.getFn = getFn; + this.isCreated = false; + this.docs = []; + this.keys = []; + this._keysMap = {}; + this.setIndexRecords(); + } + setSources(docs = []) { + this.docs = docs; + } + setIndexRecords(records = []) { + this.records = records; + } + setKeys(keys = []) { + this.keys = keys; + this._keysMap = {}; + keys.forEach((key, idx) => { + this._keysMap[key.id] = idx; + }); + } + create() { + if (this.isCreated || !this.docs.length) return; + this.isCreated = true; + const len = this.docs.length; + this.records = new Array(len); + let recordCount = 0; + if (isString(this.docs[0])) for (let i = 0; i < len; i++) { + const record = this._createStringRecord(this.docs[i], i); + if (record) this.records[recordCount++] = record; + } + else for (let i = 0; i < len; i++) this.records[recordCount++] = this._createObjectRecord(this.docs[i], i); + this.records.length = recordCount; + this.norm.clear(); + } + add(doc, docIndex) { + if (!Number.isInteger(docIndex) || docIndex < 0) throw new Error(INVALID_DOC_INDEX); + if (isString(doc)) { + const record = this._createStringRecord(doc, docIndex); + if (record) this.records.push(record); + return record; + } + const record = this._createObjectRecord(doc, docIndex); + this.records.push(record); + return record; + } + removeAt(idx) { + if (!Number.isInteger(idx) || idx < 0) throw new Error(INVALID_DOC_INDEX); + for (let i = 0, len = this.records.length; i < len; i += 1) if (this.records[i].i === idx) { + this.records.splice(i, 1); + break; + } + for (let i = 0, len = this.records.length; i < len; i += 1) if (this.records[i].i > idx) this.records[i].i -= 1; + } + removeAll(indices) { + const toRemove = /* @__PURE__ */ new Set(); + for (const v of indices) if (Number.isInteger(v) && v >= 0) toRemove.add(v); + if (toRemove.size === 0) return; + this.records = this.records.filter((r) => !toRemove.has(r.i)); + const sorted = Array.from(toRemove).sort((a, b) => a - b); + for (const record of this.records) { + let lo = 0; + let hi = sorted.length; + while (lo < hi) { + const mid = lo + hi >>> 1; + if (sorted[mid] < record.i) lo = mid + 1; + else hi = mid; + } + record.i -= lo; + } + } + getValueForItemAtKeyId(item, keyId) { + return item[this._keysMap[keyId]]; + } + size() { + return this.records.length; + } + _createStringRecord(doc, docIndex) { + if (!isDefined(doc) || isBlank(doc)) return null; + return { + v: doc, + i: docIndex, + n: this.norm.get(doc) + }; + } + _createObjectRecord(doc, docIndex) { + const record = { + i: docIndex, + $: {} + }; + for (let keyIndex = 0, keyLen = this.keys.length; keyIndex < keyLen; keyIndex++) { + const key = this.keys[keyIndex]; + const value = key.getFn ? key.getFn(doc) : this.getFn(doc, key.path); + if (!isDefined(value)) continue; + if (isArray(value)) { + const subRecords = []; + for (let i = 0, len = value.length; i < len; i += 1) { + const item = value[i]; + if (!isDefined(item)) continue; + if (isString(item)) { + if (!isBlank(item)) { + const subRecord = { + v: item, + i, + n: this.norm.get(item) + }; + subRecords.push(subRecord); + } + } else if (isDefined(item.v)) { + const text = isString(item.v) ? item.v : toString(item.v); + if (!isBlank(text)) { + const subRecord = { + v: text, + i: item.i, + n: this.norm.get(text) + }; + subRecords.push(subRecord); + } + } + } + record.$[keyIndex] = subRecords; + } else if (isString(value) && !isBlank(value)) { + const subRecord = { + v: value, + n: this.norm.get(value) + }; + record.$[keyIndex] = subRecord; + } + } + return record; + } + toJSON() { + return { + keys: this.keys.map(({ getFn, ...key }) => key), + records: this.records + }; + } +}; +function createIndex(keys, docs, { getFn = Config.getFn, fieldNormWeight = Config.fieldNormWeight } = {}) { + const myIndex = new FuseIndex({ + getFn, + fieldNormWeight + }); + myIndex.setKeys(keys.map(createKey)); + myIndex.setSources(docs); + myIndex.create(); + return myIndex; } -function parseIndex(data, { - getFn = Config.getFn, - fieldNormWeight = Config.fieldNormWeight -} = {}) { - const { - keys, - records - } = data; - const myIndex = new FuseIndex({ - getFn, - fieldNormWeight - }); - myIndex.setKeys(keys); - myIndex.setIndexRecords(records); - return myIndex; +function parseIndex(data, { getFn = Config.getFn, fieldNormWeight = Config.fieldNormWeight } = {}) { + const { keys, records } = data; + const myIndex = new FuseIndex({ + getFn, + fieldNormWeight + }); + myIndex.setKeys(keys); + myIndex.setIndexRecords(records); + return myIndex; } +//#endregion +//#region src/search/bitap/convertMaskToIndices.ts function convertMaskToIndices(matchmask = [], minMatchCharLength = Config.minMatchCharLength) { - const indices = []; - let start = -1; - let end = -1; - let i = 0; - for (let len = matchmask.length; i < len; i += 1) { - const match = matchmask[i]; - if (match && start === -1) { - start = i; - } else if (!match && start !== -1) { - end = i - 1; - if (end - start + 1 >= minMatchCharLength) { - indices.push([start, end]); - } - start = -1; - } - } - - // (i-1 - start) + 1 => i - start - if (matchmask[i - 1] && i - start >= minMatchCharLength) { - indices.push([start, i - 1]); - } - return indices; + const indices = []; + let start = -1; + let end = -1; + let i = 0; + for (let len = matchmask.length; i < len; i += 1) { + const match = matchmask[i]; + if (match && start === -1) start = i; + else if (!match && start !== -1) { + end = i - 1; + if (end - start + 1 >= minMatchCharLength) indices.push([start, end]); + start = -1; + } + } + if (matchmask[i - 1] && i - start >= minMatchCharLength) indices.push([start, i - 1]); + return indices; } -// Machine word size -const MAX_BITS = 32; - -function search(text, pattern, patternAlphabet, { - location = Config.location, - distance = Config.distance, - threshold = Config.threshold, - findAllMatches = Config.findAllMatches, - minMatchCharLength = Config.minMatchCharLength, - includeMatches = Config.includeMatches, - ignoreLocation = Config.ignoreLocation -} = {}) { - if (pattern.length > MAX_BITS) { - throw new Error(PATTERN_LENGTH_TOO_LARGE(MAX_BITS)); - } - const patternLen = pattern.length; - // Set starting location at beginning text and initialize the alphabet. - const textLen = text.length; - // Handle the case when location > text.length - const expectedLocation = Math.max(0, Math.min(location, textLen)); - // Highest score beyond which we give up. - let currentThreshold = threshold; - // Is there a nearby exact match? (speedup) - let bestLocation = expectedLocation; - - // Inlined score computation — avoids object allocation per call in hot loops. - // See ./computeScore.ts for the documented version of this formula. - const calcScore = (errors, currentLocation) => { - const accuracy = errors / patternLen; - if (ignoreLocation) return accuracy; - const proximity = Math.abs(expectedLocation - currentLocation); - if (!distance) return proximity ? 1.0 : accuracy; - return accuracy + proximity / distance; - }; - - // Performance: only computer matches when the minMatchCharLength > 1 - // OR if `includeMatches` is true. - const computeMatches = minMatchCharLength > 1 || includeMatches; - // A mask of the matches, used for building the indices - const matchMask = computeMatches ? Array(textLen) : []; - let index; - - // Get all exact matches, here for speed up - while ((index = text.indexOf(pattern, bestLocation)) > -1) { - const score = calcScore(0, index); - currentThreshold = Math.min(score, currentThreshold); - bestLocation = index + patternLen; - if (computeMatches) { - let i = 0; - while (i < patternLen) { - matchMask[index + i] = 1; - i += 1; - } - } - } - - // Reset the best location - bestLocation = -1; - let lastBitArr = []; - let finalScore = 1; - let bestErrors = 0; - let binMax = patternLen + textLen; - const mask = 1 << patternLen - 1; - for (let i = 0; i < patternLen; i += 1) { - // Scan for the best match; each iteration allows for one more error. - // Run a binary search to determine how far from the match location we can stray - // at this error level. - let binMin = 0; - let binMid = binMax; - while (binMin < binMid) { - const score = calcScore(i, expectedLocation + binMid); - if (score <= currentThreshold) { - binMin = binMid; - } else { - binMax = binMid; - } - binMid = Math.floor((binMax - binMin) / 2 + binMin); - } - - // Use the result from this iteration as the maximum for the next. - binMax = binMid; - let start = Math.max(1, expectedLocation - binMid + 1); - const finish = findAllMatches ? textLen : Math.min(expectedLocation + binMid, textLen) + patternLen; - - // Initialize the bit array - const bitArr = Array(finish + 2); - bitArr[finish + 1] = (1 << i) - 1; - for (let j = finish; j >= start; j -= 1) { - const currentLocation = j - 1; - const charMatch = patternAlphabet[text[currentLocation]]; - - // First pass: exact match - bitArr[j] = (bitArr[j + 1] << 1 | 1) & charMatch; - - // Subsequent passes: fuzzy match - if (i) { - bitArr[j] |= (lastBitArr[j + 1] | lastBitArr[j]) << 1 | 1 | lastBitArr[j + 1]; - } - if (bitArr[j] & mask) { - finalScore = calcScore(i, currentLocation); - - // This match will almost certainly be better than any existing match. - // But check anyway. - if (finalScore <= currentThreshold) { - // Indeed it is - currentThreshold = finalScore; - bestLocation = currentLocation; - bestErrors = i; - - // Already passed `loc`, downhill from here on in. - if (bestLocation <= expectedLocation) { - break; - } - - // When passing `bestLocation`, don't exceed our current distance from `expectedLocation`. - start = Math.max(1, 2 * expectedLocation - bestLocation); - } - } - } - - // No hope for a (better) match at greater error levels. - const score = calcScore(i + 1, expectedLocation); - if (score > currentThreshold) { - break; - } - lastBitArr = bitArr; - } - - // Fill matchMask across the matched window only. Bitap anchors a match at - // bestLocation (the start), spanning patternLen characters plus up to - // bestErrors extra characters when errors are text-side insertions. Marking - // alphabet positions in that window keeps the highlight indices honest about - // what actually matched, instead of every pattern-alphabet character the - // scan happened to visit. - if (computeMatches && bestLocation >= 0) { - const matchEnd = Math.min(textLen - 1, bestLocation + patternLen - 1 + bestErrors); - for (let k = bestLocation; k <= matchEnd; k += 1) { - if (patternAlphabet[text[k]]) { - matchMask[k] = 1; - } - } - } - const result = { - isMatch: bestLocation >= 0, - // Count exact matches (those with a score of 0) to be "almost" exact - score: Math.max(0.001, finalScore) - }; - if (computeMatches) { - const indices = convertMaskToIndices(matchMask, minMatchCharLength); - if (!indices.length) { - result.isMatch = false; - } else if (includeMatches) { - result.indices = indices; - } - } - return result; +//#endregion +//#region src/search/bitap/search.ts +function search(text, pattern, patternAlphabet, { location = Config.location, distance = Config.distance, threshold = Config.threshold, findAllMatches = Config.findAllMatches, minMatchCharLength = Config.minMatchCharLength, includeMatches = Config.includeMatches, ignoreLocation = Config.ignoreLocation } = {}) { + if (pattern.length > 32) throw new Error(PATTERN_LENGTH_TOO_LARGE(32)); + const patternLen = pattern.length; + const textLen = text.length; + const expectedLocation = Math.max(0, Math.min(location, textLen)); + let currentThreshold = threshold; + let bestLocation = expectedLocation; + const calcScore = (errors, currentLocation) => { + const accuracy = errors / patternLen; + if (ignoreLocation) return accuracy; + const proximity = Math.abs(expectedLocation - currentLocation); + if (!distance) return proximity ? 1 : accuracy; + return accuracy + proximity / distance; + }; + const computeMatches = minMatchCharLength > 1 || includeMatches; + const matchMask = computeMatches ? Array(textLen) : []; + let index; + while ((index = text.indexOf(pattern, bestLocation)) > -1) { + const score = calcScore(0, index); + currentThreshold = Math.min(score, currentThreshold); + bestLocation = index + patternLen; + if (computeMatches) { + let i = 0; + while (i < patternLen) { + matchMask[index + i] = 1; + i += 1; + } + } + } + bestLocation = -1; + let lastBitArr = []; + let finalScore = 1; + let bestErrors = 0; + let binMax = patternLen + textLen; + const mask = 1 << patternLen - 1; + for (let i = 0; i < patternLen; i += 1) { + let binMin = 0; + let binMid = binMax; + while (binMin < binMid) { + if (calcScore(i, expectedLocation + binMid) <= currentThreshold) binMin = binMid; + else binMax = binMid; + binMid = Math.floor((binMax - binMin) / 2 + binMin); + } + binMax = binMid; + let start = Math.max(1, expectedLocation - binMid + 1); + const finish = findAllMatches ? textLen : Math.min(expectedLocation + binMid, textLen) + patternLen; + const bitArr = Array(finish + 2); + bitArr[finish + 1] = (1 << i) - 1; + for (let j = finish; j >= start; j -= 1) { + const currentLocation = j - 1; + const charMatch = patternAlphabet[text[currentLocation]]; + bitArr[j] = (bitArr[j + 1] << 1 | 1) & charMatch; + if (i) bitArr[j] |= (lastBitArr[j + 1] | lastBitArr[j]) << 1 | 1 | lastBitArr[j + 1]; + if (bitArr[j] & mask) { + finalScore = calcScore(i, currentLocation); + if (finalScore <= currentThreshold) { + currentThreshold = finalScore; + bestLocation = currentLocation; + bestErrors = i; + if (bestLocation <= expectedLocation) break; + start = Math.max(1, 2 * expectedLocation - bestLocation); + } + } + } + if (calcScore(i + 1, expectedLocation) > currentThreshold) break; + lastBitArr = bitArr; + } + if (computeMatches && bestLocation >= 0) { + const matchEnd = Math.min(textLen - 1, bestLocation + patternLen - 1 + bestErrors); + for (let k = bestLocation; k <= matchEnd; k += 1) if (patternAlphabet[text[k]]) matchMask[k] = 1; + } + const result = { + isMatch: bestLocation >= 0, + score: Math.max(.001, finalScore) + }; + if (computeMatches) { + const indices = convertMaskToIndices(matchMask, minMatchCharLength); + if (!indices.length) result.isMatch = false; + else if (includeMatches) result.indices = indices; + } + return result; } +//#endregion +//#region src/search/bitap/createPatternAlphabet.ts function createPatternAlphabet(pattern) { - const mask = {}; - for (let i = 0, len = pattern.length; i < len; i += 1) { - const char = pattern.charAt(i); - mask[char] = (mask[char] || 0) | 1 << len - i - 1; - } - return mask; + const mask = {}; + for (let i = 0, len = pattern.length; i < len; i += 1) { + const char = pattern.charAt(i); + mask[char] = (mask[char] || 0) | 1 << len - i - 1; + } + return mask; } +//#endregion +//#region src/helpers/mergeIndices.ts function mergeIndices(indices) { - if (indices.length <= 1) return indices; - indices.sort((a, b) => a[0] - b[0] || a[1] - b[1]); - const merged = [indices[0]]; - for (let i = 1, len = indices.length; i < len; i += 1) { - const last = merged[merged.length - 1]; - const curr = indices[i]; - if (curr[0] <= last[1] + 1) { - last[1] = Math.max(last[1], curr[1]); - } else { - merged.push(curr); - } - } - return merged; + if (indices.length <= 1) return indices; + indices.sort((a, b) => a[0] - b[0] || a[1] - b[1]); + const merged = [indices[0]]; + for (let i = 1, len = indices.length; i < len; i += 1) { + const last = merged[merged.length - 1]; + const curr = indices[i]; + if (curr[0] <= last[1] + 1) last[1] = Math.max(last[1], curr[1]); + else merged.push(curr); + } + return merged; } -// Characters that survive NFD normalization unchanged and need explicit mapping +//#endregion +//#region src/helpers/diacritics.ts const NON_DECOMPOSABLE_MAP = { - '\u0142': 'l', - // ł - '\u0141': 'L', - // Ł - '\u0111': 'd', - // đ - '\u0110': 'D', - // Đ - '\u00F8': 'o', - // ø - '\u00D8': 'O', - // Ø - '\u0127': 'h', - // ħ - '\u0126': 'H', - // Ħ - '\u0167': 't', - // ŧ - '\u0166': 'T', - // Ŧ - '\u0131': 'i', - // ı - '\u00DF': 'ss' // ß + "ł": "l", + "Ł": "L", + "đ": "d", + "Đ": "D", + "ø": "o", + "Ø": "O", + "ħ": "h", + "Ħ": "H", + "ŧ": "t", + "Ŧ": "T", + "ı": "i", + "ß": "ss" +}; +const NON_DECOMPOSABLE_RE = new RegExp("[" + Object.keys(NON_DECOMPOSABLE_MAP).join("") + "]", "g"); +const stripDiacritics = typeof String.prototype.normalize === "function" ? (str) => str.normalize("NFD").replace(/[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]/g, "").replace(NON_DECOMPOSABLE_RE, (ch) => NON_DECOMPOSABLE_MAP[ch]) : (str) => str; + +//#endregion +//#region src/search/bitap/index.ts +var BitapSearch = class { + constructor(pattern, { location = Config.location, threshold = Config.threshold, distance = Config.distance, includeMatches = Config.includeMatches, findAllMatches = Config.findAllMatches, minMatchCharLength = Config.minMatchCharLength, isCaseSensitive = Config.isCaseSensitive, ignoreDiacritics = Config.ignoreDiacritics, ignoreLocation = Config.ignoreLocation } = {}) { + this.options = { + location, + threshold, + distance, + includeMatches, + findAllMatches, + minMatchCharLength, + isCaseSensitive, + ignoreDiacritics, + ignoreLocation + }; + pattern = isCaseSensitive ? pattern : pattern.toLowerCase(); + pattern = ignoreDiacritics ? stripDiacritics(pattern) : pattern; + this.pattern = pattern; + this.chunks = []; + if (!this.pattern.length) return; + const addChunk = (pattern, startIndex) => { + this.chunks.push({ + pattern, + alphabet: createPatternAlphabet(pattern), + startIndex + }); + }; + const len = this.pattern.length; + if (len > 32) { + let i = 0; + const remainder = len % 32; + const end = len - remainder; + while (i < end) { + addChunk(this.pattern.substr(i, 32), i); + i += 32; + } + if (remainder) { + const startIndex = len - 32; + addChunk(this.pattern.substr(startIndex), startIndex); + } + } else addChunk(this.pattern, 0); + } + searchIn(text) { + const { isCaseSensitive, ignoreDiacritics, includeMatches } = this.options; + text = isCaseSensitive ? text : text.toLowerCase(); + text = ignoreDiacritics ? stripDiacritics(text) : text; + if (this.pattern === text) { + const result = { + isMatch: true, + score: 0 + }; + if (includeMatches) result.indices = [[0, text.length - 1]]; + return result; + } + const { location, distance, threshold, findAllMatches, minMatchCharLength, ignoreLocation } = this.options; + const allIndices = []; + let totalScore = 0; + let hasMatches = false; + this.chunks.forEach(({ pattern, alphabet, startIndex }) => { + const { isMatch, score, indices } = search(text, pattern, alphabet, { + location: location + startIndex, + distance, + threshold, + findAllMatches, + minMatchCharLength, + includeMatches, + ignoreLocation + }); + if (isMatch) hasMatches = true; + totalScore += score; + if (isMatch && indices) allIndices.push(...indices); + }); + const result = { + isMatch: hasMatches, + score: hasMatches ? totalScore / this.chunks.length : 1 + }; + if (hasMatches && includeMatches) result.indices = mergeIndices(allIndices); + return result; + } }; -const NON_DECOMPOSABLE_RE = new RegExp('[' + Object.keys(NON_DECOMPOSABLE_MAP).join('') + ']', 'g'); -const stripDiacritics = typeof String.prototype.normalize === 'function' ? str => str.normalize('NFD').replace(/[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]/g, '').replace(NON_DECOMPOSABLE_RE, ch => NON_DECOMPOSABLE_MAP[ch]) : str => str; - -class BitapSearch { - constructor(pattern, { - location = Config.location, - threshold = Config.threshold, - distance = Config.distance, - includeMatches = Config.includeMatches, - findAllMatches = Config.findAllMatches, - minMatchCharLength = Config.minMatchCharLength, - isCaseSensitive = Config.isCaseSensitive, - ignoreDiacritics = Config.ignoreDiacritics, - ignoreLocation = Config.ignoreLocation - } = {}) { - this.options = { - location, - threshold, - distance, - includeMatches, - findAllMatches, - minMatchCharLength, - isCaseSensitive, - ignoreDiacritics, - ignoreLocation - }; - pattern = isCaseSensitive ? pattern : pattern.toLowerCase(); - pattern = ignoreDiacritics ? stripDiacritics(pattern) : pattern; - this.pattern = pattern; - this.chunks = []; - if (!this.pattern.length) { - return; - } - const addChunk = (pattern, startIndex) => { - this.chunks.push({ - pattern, - alphabet: createPatternAlphabet(pattern), - startIndex - }); - }; - const len = this.pattern.length; - if (len > MAX_BITS) { - let i = 0; - const remainder = len % MAX_BITS; - const end = len - remainder; - while (i < end) { - addChunk(this.pattern.substr(i, MAX_BITS), i); - i += MAX_BITS; - } - if (remainder) { - const startIndex = len - MAX_BITS; - addChunk(this.pattern.substr(startIndex), startIndex); - } - } else { - addChunk(this.pattern, 0); - } - } - searchIn(text) { - const { - isCaseSensitive, - ignoreDiacritics, - includeMatches - } = this.options; - text = isCaseSensitive ? text : text.toLowerCase(); - text = ignoreDiacritics ? stripDiacritics(text) : text; - - // Exact match - if (this.pattern === text) { - const result = { - isMatch: true, - score: 0 - }; - if (includeMatches) { - result.indices = [[0, text.length - 1]]; - } - return result; - } - - // Otherwise, use Bitap algorithm - const { - location, - distance, - threshold, - findAllMatches, - minMatchCharLength, - ignoreLocation - } = this.options; - const allIndices = []; - let totalScore = 0; - let hasMatches = false; - this.chunks.forEach(({ - pattern, - alphabet, - startIndex - }) => { - const { - isMatch, - score, - indices - } = search(text, pattern, alphabet, { - location: location + startIndex, - distance, - threshold, - findAllMatches, - minMatchCharLength, - includeMatches, - ignoreLocation - }); - if (isMatch) { - hasMatches = true; - } - totalScore += score; - if (isMatch && indices) { - allIndices.push(...indices); - } - }); - const result = { - isMatch: hasMatches, - score: hasMatches ? totalScore / this.chunks.length : 1 - }; - if (hasMatches && includeMatches) { - result.indices = mergeIndices(allIndices); - } - return result; - } -} - -// ── Matcher interface ───────────────────────────────────────────── -// -// Each matcher is a lightweight object with a type tag and a search -// function. No class hierarchy needed — the search logic for most -// matchers is a one-liner. - -// ── Matcher definition ──────────────────────────────────────────── -// -// A definition pairs the detection regexes (used by parseQuery to -// recognize string-syntax operators like ^, =, !) with a factory -// that creates a Matcher instance. - -// Whether a matcher type can return multiple index ranges -const MULTI_MATCH_TYPES = new Set(['fuzzy', 'include']); -// Whether a matcher type is an inverse match +//#endregion +//#region src/search/extended/matchers.ts +const MULTI_MATCH_TYPES = new Set(["fuzzy", "include"]); function isInverse(type) { - return type.startsWith('inverse'); + return type.startsWith("inverse"); } - -// ── Matcher definitions ─────────────────────────────────────────── -// -// Order matters — parseQuery tries each in sequence and uses the -// first match. FuzzyMatch (catch-all) must be last. - -// prettier-ignore const matchers = [ -// =term — exact match -{ - type: 'exact', - multiRegex: /^="(.*)"$/, - singleRegex: /^=(.*)$/, - create: pattern => ({ - type: 'exact', - search(text) { - const isMatch = text === pattern; - return { - isMatch, - score: isMatch ? 0 : 1, - indices: [0, pattern.length - 1] - }; - } - }) -}, -// 'term — include (substring) match -{ - type: 'include', - multiRegex: /^'"(.*)"$/, - singleRegex: /^'(.*)$/, - create: pattern => ({ - type: 'include', - search(text) { - let location = 0; - let index; - const indices = []; - const patternLen = pattern.length; - while ((index = text.indexOf(pattern, location)) > -1) { - location = index + patternLen; - indices.push([index, location - 1]); - } - const isMatch = !!indices.length; - return { - isMatch, - score: isMatch ? 0 : 1, - indices - }; - } - }) -}, -// ^term — prefix match -{ - type: 'prefix-exact', - multiRegex: /^\^"(.*)"$/, - singleRegex: /^\^(.*)$/, - create: pattern => ({ - type: 'prefix-exact', - search(text) { - const isMatch = text.startsWith(pattern); - return { - isMatch, - score: isMatch ? 0 : 1, - indices: [0, pattern.length - 1] - }; - } - }) -}, -// !^term — inverse prefix match -{ - type: 'inverse-prefix-exact', - multiRegex: /^!\^"(.*)"$/, - singleRegex: /^!\^(.*)$/, - create: pattern => ({ - type: 'inverse-prefix-exact', - search(text) { - const isMatch = !text.startsWith(pattern); - return { - isMatch, - score: isMatch ? 0 : 1, - indices: [0, text.length - 1] - }; - } - }) -}, -// !term$ — inverse suffix match -{ - type: 'inverse-suffix-exact', - multiRegex: /^!"(.*)"\$$/, - singleRegex: /^!(.*)\$$/, - create: pattern => ({ - type: 'inverse-suffix-exact', - search(text) { - const isMatch = !text.endsWith(pattern); - return { - isMatch, - score: isMatch ? 0 : 1, - indices: [0, text.length - 1] - }; - } - }) -}, -// term$ — suffix match -{ - type: 'suffix-exact', - multiRegex: /^"(.*)"\$$/, - singleRegex: /^(.*)\$$/, - create: pattern => ({ - type: 'suffix-exact', - search(text) { - const isMatch = text.endsWith(pattern); - return { - isMatch, - score: isMatch ? 0 : 1, - indices: [text.length - pattern.length, text.length - 1] - }; - } - }) -}, -// !term — inverse exact (does not contain) -{ - type: 'inverse-exact', - multiRegex: /^!"(.*)"$/, - singleRegex: /^!(.*)$/, - create: pattern => ({ - type: 'inverse-exact', - search(text) { - const isMatch = text.indexOf(pattern) === -1; - return { - isMatch, - score: isMatch ? 0 : 1, - indices: [0, text.length - 1] - }; - } - }) -}, -// term — fuzzy match (catch-all, must be last) -{ - type: 'fuzzy', - multiRegex: /^"(.*)"$/, - singleRegex: /^(.*)$/, - create: (pattern, options = {}) => { - const bitap = new BitapSearch(pattern, { - location: options.location ?? Config.location, - threshold: options.threshold ?? Config.threshold, - distance: options.distance ?? Config.distance, - includeMatches: options.includeMatches ?? Config.includeMatches, - findAllMatches: options.findAllMatches ?? Config.findAllMatches, - minMatchCharLength: options.minMatchCharLength ?? Config.minMatchCharLength, - isCaseSensitive: options.isCaseSensitive ?? Config.isCaseSensitive, - ignoreDiacritics: options.ignoreDiacritics ?? Config.ignoreDiacritics, - ignoreLocation: options.ignoreLocation ?? Config.ignoreLocation - }); - return { - type: 'fuzzy', - search(text) { - return bitap.searchIn(text); - } - }; - } -}]; - + { + type: "exact", + multiRegex: /^="(.*)"$/, + singleRegex: /^=(.*)$/, + create: (pattern) => ({ + type: "exact", + search(text) { + const isMatch = text === pattern; + return { + isMatch, + score: isMatch ? 0 : 1, + indices: [0, pattern.length - 1] + }; + } + }) + }, + { + type: "include", + multiRegex: /^'"(.*)"$/, + singleRegex: /^'(.*)$/, + create: (pattern) => ({ + type: "include", + search(text) { + let location = 0; + let index; + const indices = []; + const patternLen = pattern.length; + while ((index = text.indexOf(pattern, location)) > -1) { + location = index + patternLen; + indices.push([index, location - 1]); + } + const isMatch = !!indices.length; + return { + isMatch, + score: isMatch ? 0 : 1, + indices + }; + } + }) + }, + { + type: "prefix-exact", + multiRegex: /^\^"(.*)"$/, + singleRegex: /^\^(.*)$/, + create: (pattern) => ({ + type: "prefix-exact", + search(text) { + const isMatch = text.startsWith(pattern); + return { + isMatch, + score: isMatch ? 0 : 1, + indices: [0, pattern.length - 1] + }; + } + }) + }, + { + type: "inverse-prefix-exact", + multiRegex: /^!\^"(.*)"$/, + singleRegex: /^!\^(.*)$/, + create: (pattern) => ({ + type: "inverse-prefix-exact", + search(text) { + const isMatch = !text.startsWith(pattern); + return { + isMatch, + score: isMatch ? 0 : 1, + indices: [0, text.length - 1] + }; + } + }) + }, + { + type: "inverse-suffix-exact", + multiRegex: /^!"(.*)"\$$/, + singleRegex: /^!(.*)\$$/, + create: (pattern) => ({ + type: "inverse-suffix-exact", + search(text) { + const isMatch = !text.endsWith(pattern); + return { + isMatch, + score: isMatch ? 0 : 1, + indices: [0, text.length - 1] + }; + } + }) + }, + { + type: "suffix-exact", + multiRegex: /^"(.*)"\$$/, + singleRegex: /^(.*)\$$/, + create: (pattern) => ({ + type: "suffix-exact", + search(text) { + const isMatch = text.endsWith(pattern); + return { + isMatch, + score: isMatch ? 0 : 1, + indices: [text.length - pattern.length, text.length - 1] + }; + } + }) + }, + { + type: "inverse-exact", + multiRegex: /^!"(.*)"$/, + singleRegex: /^!(.*)$/, + create: (pattern) => ({ + type: "inverse-exact", + search(text) { + const isMatch = text.indexOf(pattern) === -1; + return { + isMatch, + score: isMatch ? 0 : 1, + indices: [0, text.length - 1] + }; + } + }) + }, + { + type: "fuzzy", + multiRegex: /^"(.*)"$/, + singleRegex: /^(.*)$/, + create: (pattern, options = {}) => { + const bitap = new BitapSearch(pattern, { + location: options.location ?? Config.location, + threshold: options.threshold ?? Config.threshold, + distance: options.distance ?? Config.distance, + includeMatches: options.includeMatches ?? Config.includeMatches, + findAllMatches: options.findAllMatches ?? Config.findAllMatches, + minMatchCharLength: options.minMatchCharLength ?? Config.minMatchCharLength, + isCaseSensitive: options.isCaseSensitive ?? Config.isCaseSensitive, + ignoreDiacritics: options.ignoreDiacritics ?? Config.ignoreDiacritics, + ignoreLocation: options.ignoreLocation ?? Config.ignoreLocation + }); + return { + type: "fuzzy", + search(text) { + return bitap.searchIn(text); + } + }; + } + } +]; + +//#endregion +//#region src/search/extended/parseQuery.ts const matchersLen = matchers.length; -const ESCAPED_PIPE = '\u0000'; // placeholder for escaped \| -const OR_TOKEN = '|'; - -// Tokenize a query string into individual search terms. -// Respects multi-match quoted tokens like ="said "test"" or ^"hello world"$ -// where inner spaces and quotes are part of the token. +const ESCAPED_PIPE = "\0"; +const OR_TOKEN = "|"; function tokenize(pattern) { - const tokens = []; - const len = pattern.length; - let i = 0; - while (i < len) { - // Skip spaces - while (i < len && pattern[i] === ' ') i++; - if (i >= len) break; - - // Scan past prefix characters (=, !, ^, ') to see if a quote follows - let j = i; - while (j < len && pattern[j] !== ' ' && pattern[j] !== '"') j++; - if (j < len && pattern[j] === '"') { - // Multi-match token: prefix + "content" (possibly with inner quotes) - // Find the closing " that ends this token: - // it must be followed by optional $, then space or end-of-string - j++; // skip opening quote - while (j < len) { - if (pattern[j] === '"') { - // Check if this is the closing quote - const next = j + 1; - if (next >= len || pattern[next] === ' ') { - j++; // include closing quote - break; - } - if (pattern[next] === '$' && (next + 1 >= len || pattern[next + 1] === ' ')) { - j += 2; // include "$ - break; - } - } - j++; - } - tokens.push(pattern.substring(i, j)); - i = j; - } else { - // Regular (unquoted) token: read until space or end - while (j < len && pattern[j] !== ' ') j++; - tokens.push(pattern.substring(i, j)); - i = j; - } - } - return tokens; + const tokens = []; + const len = pattern.length; + let i = 0; + while (i < len) { + while (i < len && pattern[i] === " ") i++; + if (i >= len) break; + let j = i; + while (j < len && pattern[j] !== " " && pattern[j] !== "\"") j++; + if (j < len && pattern[j] === "\"") { + j++; + while (j < len) { + if (pattern[j] === "\"") { + const next = j + 1; + if (next >= len || pattern[next] === " ") { + j++; + break; + } + if (pattern[next] === "$" && (next + 1 >= len || pattern[next + 1] === " ")) { + j += 2; + break; + } + } + j++; + } + tokens.push(pattern.substring(i, j)); + i = j; + } else { + while (j < len && pattern[j] !== " ") j++; + tokens.push(pattern.substring(i, j)); + i = j; + } + } + return tokens; } function getMatch(pattern, exp) { - const matches = pattern.match(exp); - return matches ? matches[1] : null; + const matches = pattern.match(exp); + return matches ? matches[1] : null; } - -// Return a 2D array representation of the query, for simpler parsing. -// Example: -// "^core go$ | rb$ | py$ xy$" => [["^core", "go$"], ["rb$"], ["py$", "xy$"]] function parseQuery(pattern, options = {}) { - // Replace escaped \| with placeholder before splitting on | - const escaped = pattern.replace(/\\\|/g, ESCAPED_PIPE); - return escaped.split(OR_TOKEN).map(item => { - // Restore escaped pipes in each OR group - const restored = item.replace(/\u0000/g, '|'); - const query = tokenize(restored.trim()).filter(item => item && !!item.trim()); - const results = []; - for (let i = 0, len = query.length; i < len; i += 1) { - const queryItem = query[i]; - - // 1. Handle multiple query match (i.e, ones that are quoted, like `"hello world"`) - let found = false; - let idx = -1; - while (!found && ++idx < matchersLen) { - const def = matchers[idx]; - const token = getMatch(queryItem, def.multiRegex); - if (token) { - results.push(def.create(token, options)); - found = true; - } - } - if (found) { - continue; - } - - // 2. Handle single query matches (i.e, ones that are *not* quoted) - idx = -1; - while (++idx < matchersLen) { - const def = matchers[idx]; - const token = getMatch(queryItem, def.singleRegex); - if (token) { - results.push(def.create(token, options)); - break; - } - } - } - return results; - }); + return pattern.replace(/\\\|/g, ESCAPED_PIPE).split(OR_TOKEN).map((item) => { + const query = tokenize(item.replace(/\u0000/g, "|").trim()).filter((item) => item && !!item.trim()); + const results = []; + for (let i = 0, len = query.length; i < len; i += 1) { + const queryItem = query[i]; + let found = false; + let idx = -1; + while (!found && ++idx < matchersLen) { + const def = matchers[idx]; + const token = getMatch(queryItem, def.multiRegex); + if (token) { + results.push(def.create(token, options)); + found = true; + } + } + if (found) continue; + idx = -1; + while (++idx < matchersLen) { + const def = matchers[idx]; + const token = getMatch(queryItem, def.singleRegex); + if (token) { + results.push(def.create(token, options)); + break; + } + } + } + return results; + }); } -class ExtendedSearch { - constructor(pattern, { - isCaseSensitive = Config.isCaseSensitive, - ignoreDiacritics = Config.ignoreDiacritics, - includeMatches = Config.includeMatches, - minMatchCharLength = Config.minMatchCharLength, - ignoreLocation = Config.ignoreLocation, - findAllMatches = Config.findAllMatches, - location = Config.location, - threshold = Config.threshold, - distance = Config.distance - } = {}) { - this.query = null; - this.options = { - isCaseSensitive, - ignoreDiacritics, - includeMatches, - minMatchCharLength, - findAllMatches, - ignoreLocation, - location, - threshold, - distance - }; - pattern = isCaseSensitive ? pattern : pattern.toLowerCase(); - pattern = ignoreDiacritics ? stripDiacritics(pattern) : pattern; - this.pattern = pattern; - this.query = parseQuery(this.pattern, this.options); - } - static condition(_, options) { - return options.useExtendedSearch; - } - - // Note: searchIn operates on a single text value and sets hasInverse on the - // result when inverse patterns are involved. _searchObjectList uses this to - // switch from "ANY key" to "ALL keys" aggregation. See #712. - searchIn(text) { - const query = this.query; - if (!query) { - return { - isMatch: false, - score: 1 - }; - } - const { - includeMatches, - isCaseSensitive, - ignoreDiacritics - } = this.options; - text = isCaseSensitive ? text : text.toLowerCase(); - text = ignoreDiacritics ? stripDiacritics(text) : text; - let numMatches = 0; - const allIndices = []; - let totalScore = 0; - let hasInverse = false; - - // ORs - for (let i = 0, qLen = query.length; i < qLen; i += 1) { - const searchers = query[i]; - - // Reset indices - allIndices.length = 0; - numMatches = 0; - hasInverse = false; - - // ANDs - for (let j = 0, pLen = searchers.length; j < pLen; j += 1) { - const matcher = searchers[j]; - const { - isMatch, - indices, - score - } = matcher.search(text); - if (isMatch) { - numMatches += 1; - totalScore += score; - if (isInverse(matcher.type)) { - hasInverse = true; - } - if (includeMatches) { - if (MULTI_MATCH_TYPES.has(matcher.type)) { - allIndices.push(...indices); - } else { - allIndices.push(indices); - } - } - } else { - totalScore = 0; - numMatches = 0; - allIndices.length = 0; - hasInverse = false; - break; - } - } - - // OR condition, so if TRUE, return - if (numMatches) { - const result = { - isMatch: true, - score: totalScore / numMatches - }; - if (hasInverse) { - result.hasInverse = true; - } - if (includeMatches) { - result.indices = mergeIndices(allIndices); - } - return result; - } - } - - // Nothing was matched - return { - isMatch: false, - score: 1 - }; - } -} +//#endregion +//#region src/search/extended/index.ts +var ExtendedSearch = class { + constructor(pattern, { isCaseSensitive = Config.isCaseSensitive, ignoreDiacritics = Config.ignoreDiacritics, includeMatches = Config.includeMatches, minMatchCharLength = Config.minMatchCharLength, ignoreLocation = Config.ignoreLocation, findAllMatches = Config.findAllMatches, location = Config.location, threshold = Config.threshold, distance = Config.distance } = {}) { + this.query = null; + this.options = { + isCaseSensitive, + ignoreDiacritics, + includeMatches, + minMatchCharLength, + findAllMatches, + ignoreLocation, + location, + threshold, + distance + }; + pattern = isCaseSensitive ? pattern : pattern.toLowerCase(); + pattern = ignoreDiacritics ? stripDiacritics(pattern) : pattern; + this.pattern = pattern; + this.query = parseQuery(this.pattern, this.options); + } + static condition(_, options) { + return options.useExtendedSearch; + } + searchIn(text) { + const query = this.query; + if (!query) return { + isMatch: false, + score: 1 + }; + const { includeMatches, isCaseSensitive, ignoreDiacritics } = this.options; + text = isCaseSensitive ? text : text.toLowerCase(); + text = ignoreDiacritics ? stripDiacritics(text) : text; + let numMatches = 0; + const allIndices = []; + let totalScore = 0; + let hasInverse = false; + for (let i = 0, qLen = query.length; i < qLen; i += 1) { + const searchers = query[i]; + allIndices.length = 0; + numMatches = 0; + hasInverse = false; + for (let j = 0, pLen = searchers.length; j < pLen; j += 1) { + const matcher = searchers[j]; + const { isMatch, indices, score } = matcher.search(text); + if (isMatch) { + numMatches += 1; + totalScore += score; + if (isInverse(matcher.type)) hasInverse = true; + if (includeMatches) if (MULTI_MATCH_TYPES.has(matcher.type)) allIndices.push(...indices); + else allIndices.push(indices); + } else { + totalScore = 0; + numMatches = 0; + allIndices.length = 0; + hasInverse = false; + break; + } + } + if (numMatches) { + const result = { + isMatch: true, + score: totalScore / numMatches + }; + if (hasInverse) result.hasInverse = true; + if (includeMatches) result.indices = mergeIndices(allIndices); + return result; + } + } + return { + isMatch: false, + score: 1 + }; + } +}; +//#endregion +//#region src/core/register.ts const registeredSearchers = []; function register(...args) { - registeredSearchers.push(...args); + registeredSearchers.push(...args); } function createSearcher(pattern, options) { - for (let i = 0, len = registeredSearchers.length; i < len; i += 1) { - const searcherClass = registeredSearchers[i]; - if (searcherClass.condition(pattern, options)) { - return new searcherClass(pattern, options); - } - } - return new BitapSearch(pattern, options); + for (let i = 0, len = registeredSearchers.length; i < len; i += 1) { + const searcherClass = registeredSearchers[i]; + if (searcherClass.condition(pattern, options)) return new searcherClass(pattern, options); + } + return new BitapSearch(pattern, options); } +//#endregion +//#region src/core/queryParser.ts const LogicalOperator = { - AND: '$and', - OR: '$or' + AND: "$and", + OR: "$or" }; const KeyType = { - PATH: '$path', - PATTERN: '$val' + PATH: "$path", + PATTERN: "$val" }; -const isExpression = query => !!(query[LogicalOperator.AND] || query[LogicalOperator.OR]); -const isPath = query => !!query[KeyType.PATH]; -const isLeaf = query => !isArray(query) && isObject(query) && !isExpression(query); -const convertToExplicit = query => ({ - [LogicalOperator.AND]: Object.keys(query).map(key => ({ - [key]: query[key] - })) -}); - -// When `auto` is `true`, the parse function will infer and initialize and add -// the appropriate `Searcher` instance -function parse(query, options, { - auto = true -} = {}) { - const next = query => { - // Keyless string entry: search across all keys - if (isString(query)) { - const obj = { - keyId: null, - pattern: query - }; - if (auto) { - obj.searcher = createSearcher(query, options); - } - return obj; - } - const keys = Object.keys(query); - const isQueryPath = isPath(query); - if (!isQueryPath && keys.length > 1 && !isExpression(query)) { - return next(convertToExplicit(query)); - } - if (isLeaf(query)) { - const key = isQueryPath ? query[KeyType.PATH] : keys[0]; - const pattern = isQueryPath ? query[KeyType.PATTERN] : query[key]; - if (!isString(pattern)) { - throw new Error(LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY(key)); - } - const obj = { - keyId: createKeyId(key), - pattern - }; - if (auto) { - obj.searcher = createSearcher(pattern, options); - } - return obj; - } - const node = { - children: [], - operator: keys[0] - }; - keys.forEach(key => { - const value = query[key]; - if (isArray(value)) { - value.forEach(item => { - node.children.push(next(item)); - }); - } - }); - return node; - }; - if (!isExpression(query)) { - query = convertToExplicit(query); - } - return next(query); +const isExpression = (query) => !!(query[LogicalOperator.AND] || query[LogicalOperator.OR]); +const isPath = (query) => !!query[KeyType.PATH]; +const isLeaf = (query) => !isArray(query) && isObject(query) && !isExpression(query); +const convertToExplicit = (query) => ({ [LogicalOperator.AND]: Object.keys(query).map((key) => ({ [key]: query[key] })) }); +function parse(query, options, { auto = true } = {}) { + const next = (query) => { + if (isString(query)) { + const obj = { + keyId: null, + pattern: query + }; + if (auto) obj.searcher = createSearcher(query, options); + return obj; + } + const keys = Object.keys(query); + const isQueryPath = isPath(query); + if (!isQueryPath && keys.length > 1 && !isExpression(query)) return next(convertToExplicit(query)); + if (isLeaf(query)) { + const key = isQueryPath ? query[KeyType.PATH] : keys[0]; + const pattern = isQueryPath ? query[KeyType.PATTERN] : query[key]; + if (!isString(pattern)) throw new Error(LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY(key)); + const obj = { + keyId: createKeyId(key), + pattern + }; + if (auto) obj.searcher = createSearcher(pattern, options); + return obj; + } + const node = { + children: [], + operator: keys[0] + }; + keys.forEach((key) => { + const value = query[key]; + if (isArray(value)) value.forEach((item) => { + node.children.push(next(item)); + }); + }); + return node; + }; + if (!isExpression(query)) query = convertToExplicit(query); + return next(query); } -function computeScoreSingle(matches, { - ignoreFieldNorm = Config.ignoreFieldNorm -}) { - let totalScore = 1; - matches.forEach(({ - key, - norm, - score - }) => { - const weight = key ? key.weight : null; - totalScore *= Math.pow(score === 0 && weight ? Number.EPSILON : score, (weight || 1) * (ignoreFieldNorm ? 1 : norm)); - }); - return totalScore; +//#endregion +//#region src/core/computeScore.ts +function computeScoreSingle(matches, { ignoreFieldNorm = Config.ignoreFieldNorm }) { + let totalScore = 1; + matches.forEach(({ key, norm, score }) => { + const weight = key ? key.weight : null; + totalScore *= Math.pow(score === 0 && weight ? Number.EPSILON : score, (weight || 1) * (ignoreFieldNorm ? 1 : norm)); + }); + return totalScore; } -function computeScore(results, { - ignoreFieldNorm = Config.ignoreFieldNorm -}) { - results.forEach(result => { - result.score = computeScoreSingle(result.matches, { - ignoreFieldNorm - }); - }); +function computeScore(results, { ignoreFieldNorm = Config.ignoreFieldNorm }) { + results.forEach((result) => { + result.score = computeScoreSingle(result.matches, { ignoreFieldNorm }); + }); } -// Max-heap by score: keeps the worst (highest) score at the top -// so we can efficiently evict it when a better result arrives. -class MaxHeap { - constructor(limit) { - this.limit = limit; - this.heap = []; - } - get size() { - return this.heap.length; - } - shouldInsert(score) { - return this.size < this.limit || score < this.heap[0].score; - } - insert(item) { - if (this.size < this.limit) { - this.heap.push(item); - this._bubbleUp(this.size - 1); - } else if (item.score < this.heap[0].score) { - this.heap[0] = item; - this._sinkDown(0); - } - } - extractSorted(sortFn) { - return this.heap.sort(sortFn); - } - _bubbleUp(i) { - const heap = this.heap; - while (i > 0) { - const parent = i - 1 >> 1; - if (heap[i].score <= heap[parent].score) break; - const tmp = heap[i]; - heap[i] = heap[parent]; - heap[parent] = tmp; - i = parent; - } - } - _sinkDown(i) { - const heap = this.heap; - const len = heap.length; - let largest = i; - do { - i = largest; - const left = 2 * i + 1; - const right = 2 * i + 2; - if (left < len && heap[left].score > heap[largest].score) { - largest = left; - } - if (right < len && heap[right].score > heap[largest].score) { - largest = right; - } - if (largest !== i) { - const tmp = heap[i]; - heap[i] = heap[largest]; - heap[largest] = tmp; - } - } while (largest !== i); - } -} +//#endregion +//#region src/tools/MaxHeap.ts +var MaxHeap = class { + constructor(limit) { + this.limit = limit; + this.heap = []; + } + get size() { + return this.heap.length; + } + shouldInsert(score) { + return this.size < this.limit || score < this.heap[0].score; + } + insert(item) { + if (this.size < this.limit) { + this.heap.push(item); + this._bubbleUp(this.size - 1); + } else if (item.score < this.heap[0].score) { + this.heap[0] = item; + this._sinkDown(0); + } + } + extractSorted(sortFn) { + return this.heap.sort(sortFn); + } + _bubbleUp(i) { + const heap = this.heap; + while (i > 0) { + const parent = i - 1 >> 1; + if (heap[i].score <= heap[parent].score) break; + const tmp = heap[i]; + heap[i] = heap[parent]; + heap[parent] = tmp; + i = parent; + } + } + _sinkDown(i) { + const heap = this.heap; + const len = heap.length; + let largest = i; + do { + i = largest; + const left = 2 * i + 1; + const right = 2 * i + 2; + if (left < len && heap[left].score > heap[largest].score) largest = left; + if (right < len && heap[right].score > heap[largest].score) largest = right; + if (largest !== i) { + const tmp = heap[i]; + heap[i] = heap[largest]; + heap[largest] = tmp; + } + } while (largest !== i); + } +}; +//#endregion +//#region src/core/formatMatches.ts function formatMatches(result) { - const matches = []; - result.matches.forEach(match => { - if (!isDefined(match.indices) || !match.indices.length) { - return; - } - const obj = { - indices: match.indices, - value: match.value - }; - if (match.key) { - // `key.id` is the canonical dotted-string identity (array paths joined - // with '.'); `key.src` is the raw user input and can be a string[]. - obj.key = match.key.id; - } - if (match.idx > -1) { - obj.refIndex = match.idx; - } - matches.push(obj); - }); - return matches; + const matches = []; + result.matches.forEach((match) => { + if (!isDefined(match.indices) || !match.indices.length) return; + const obj = { + indices: match.indices, + value: match.value + }; + if (match.key) obj.key = match.key.id; + if (match.idx > -1) obj.refIndex = match.idx; + matches.push(obj); + }); + return matches; } -function format(results, docs, { - includeMatches = Config.includeMatches, - includeScore = Config.includeScore -} = {}) { - return results.map(result => { - const { - idx - } = result; - const data = { - item: docs[idx], - refIndex: idx - }; - if (includeMatches) data.matches = formatMatches(result); - if (includeScore) data.score = result.score; - return data; - }); +//#endregion +//#region src/core/format.ts +function format(results, docs, { includeMatches = Config.includeMatches, includeScore = Config.includeScore } = {}) { + return results.map((result) => { + const { idx } = result; + const data = { + item: docs[idx], + refIndex: idx + }; + if (includeMatches) data.matches = formatMatches(result); + if (includeScore) data.score = result.score; + return data; + }); } -// Includes \p{M} (Mark) so combining marks stay attached to their base -// letter — without it, scripts like Devanagari and NFD-normalized Latin -// shatter (e.g. 'हिन्दी' → ['ह','न','द'], 'café'.normalize('NFD') → ['cafe']). +//#endregion +//#region src/search/token/analyzer.ts const DEFAULT_TOKEN = /[\p{L}\p{M}\p{N}_]+/gu; -const warned = new WeakSet(); +const warned = /* @__PURE__ */ new WeakSet(); function warnNonGlobal(regex) { - if (!warned.has(regex)) { - warned.add(regex); - console.warn(`[Fuse] tokenize regex ${regex} lacks the global flag; only the ` + `first match per text will be returned. Add the 'g' flag.`); - } + if (!warned.has(regex)) { + warned.add(regex); + console.warn(`[Fuse] tokenize regex ${regex} lacks the global flag; only the first match per text will be returned. Add the 'g' flag.`); + } } function resolveTokenize(tokenize) { - if (typeof tokenize === 'function') { - let validated = false; - return text => { - const result = tokenize(text); - if (!validated) { - validated = true; - if (!Array.isArray(result) || result.some(t => typeof t !== 'string')) { - throw new Error(`[Fuse] tokenize function must return string[]; received ${Array.isArray(result) ? 'array containing non-strings' : typeof result}.`); - } - } - return result; - }; - } - if (tokenize instanceof RegExp) { - if (!tokenize.global) warnNonGlobal(tokenize); - return text => text.match(tokenize) || []; - } - return text => text.match(DEFAULT_TOKEN) || []; + if (typeof tokenize === "function") { + let validated = false; + return (text) => { + const result = tokenize(text); + if (!validated) { + validated = true; + if (!Array.isArray(result) || result.some((t) => typeof t !== "string")) throw new Error(`[Fuse] tokenize function must return string[]; received ${Array.isArray(result) ? "array containing non-strings" : typeof result}.`); + } + return result; + }; + } + if (tokenize instanceof RegExp) { + if (!tokenize.global) warnNonGlobal(tokenize); + return (text) => text.match(tokenize) || []; + } + return (text) => text.match(DEFAULT_TOKEN) || []; } -function createAnalyzer({ - isCaseSensitive = false, - ignoreDiacritics = false, - tokenize -} = {}) { - const tokenizeFn = resolveTokenize(tokenize); - return { - tokenize(text) { - if (!isCaseSensitive) { - text = text.toLowerCase(); - } - if (ignoreDiacritics) { - text = stripDiacritics(text); - } - return tokenizeFn(text); - } - }; +function createAnalyzer({ isCaseSensitive = false, ignoreDiacritics = false, tokenize } = {}) { + const tokenizeFn = resolveTokenize(tokenize); + return { tokenize(text) { + if (!isCaseSensitive) text = text.toLowerCase(); + if (ignoreDiacritics) text = stripDiacritics(text); + return tokenizeFn(text); + } }; } -// `tokenMatch: 'all'` packs per-term coverage into a bitmask. JS bitwise ops -// are 32-bit *signed*, so bit 31 is the sign bit — only bits 0..30 are safe. -// Queries with more than this many terms fall back to a Set (no bit limit). +//#endregion +//#region src/search/token/index.ts const MAX_MASK_TERMS = 31; -class TokenSearch { - // `tokenMatch: 'all'` (AND) coverage. When true, searchIn reports which - // query terms matched each text so the core loop can require record-level - // coverage of every term. Bitmask is the ≤31-term fast path; Set is the - // ≥32-term fallback (JS bitwise ops are 32-bit signed). - - static condition(_, options) { - return options.useTokenSearch; - } - constructor(pattern, options) { - this.options = options; - this.analyzer = createAnalyzer({ - isCaseSensitive: options.isCaseSensitive, - ignoreDiacritics: options.ignoreDiacritics, - tokenize: options.tokenize - }); - const queryTerms = this.analyzer.tokenize(pattern); - const invertedIndex = options._invertedIndex; - const { - df, - fieldCount - } = invertedIndex; - this.termSearchers = []; - this.idfWeights = []; - for (const term of queryTerms) { - this.termSearchers.push(new BitapSearch(term, { - location: options.location, - threshold: options.threshold, - distance: options.distance, - includeMatches: options.includeMatches, - findAllMatches: options.findAllMatches, - minMatchCharLength: options.minMatchCharLength, - isCaseSensitive: options.isCaseSensitive, - ignoreDiacritics: options.ignoreDiacritics, - ignoreLocation: true - })); - const docFreq = df.get(term) || 0; - const idf = Math.log(1 + (fieldCount - docFreq + 0.5) / (docFreq + 0.5)); - this.idfWeights.push(idf); - } - this.combineAll = options.tokenMatch === 'all'; - this.numTerms = this.termSearchers.length; - this.useMask = this.numTerms <= MAX_MASK_TERMS; - } - searchIn(text) { - if (!this.termSearchers.length) { - return { - isMatch: false, - score: 1 - }; - } - const allIndices = []; - let weightedScore = 0; - let maxPossibleScore = 0; - let matchedCount = 0; - - // `tokenMatch: 'all'` coverage for this text (untouched in the default - // 'any' path, so it allocates nothing there). - let matchedMask = 0; - const matchedTerms = this.combineAll && !this.useMask ? new Set() : null; - for (let i = 0; i < this.termSearchers.length; i++) { - const result = this.termSearchers[i].searchIn(text); - const idf = this.idfWeights[i]; - maxPossibleScore += idf; - if (result.isMatch) { - matchedCount++; - weightedScore += idf * (1 - result.score); - if (result.indices) { - allIndices.push(...result.indices); - } - if (this.combineAll) { - if (this.useMask) { - matchedMask |= 1 << i; - } else { - matchedTerms.add(i); - } - } - } - } - if (matchedCount === 0) { - return { - isMatch: false, - score: 1 - }; - } - const normalized = maxPossibleScore > 0 ? 1 - weightedScore / maxPossibleScore : 0; - const searchResult = { - isMatch: true, - score: Math.max(0.001, normalized) - }; - if (this.options.includeMatches && allIndices.length) { - searchResult.indices = mergeIndices(allIndices); - } - - // Report term coverage so the core loop can enforce record-level AND. - if (this.combineAll) { - if (this.useMask) { - searchResult.matchedMask = matchedMask; - } else { - searchResult.matchedTerms = matchedTerms; - } - searchResult.termCount = this.numTerms; - } - return searchResult; - } -} - -// Stats-only inverted index for token search (per Plan 008 Direction B). -// -// The query path consumes only `df` and `fieldCount` (IDF weighting). The -// per-doc maps exist solely to keep `df` and `fieldCount` correct under -// `add` / `remove` / `removeAt`: -// -// docFieldCount[doc] = # distinct fields the doc contributed; subtracted -// from `fieldCount` on remove. -// docTermFieldHits[doc] = Map; each entry decrements `df[term]` by -// that count on remove. -// -// `df` is incremented once per (doc, term, field) at index time. Removing a -// doc decrements `df` by the same count, mirroring the increment exactly. +var TokenSearch = class { + static condition(_, options) { + return options.useTokenSearch; + } + constructor(pattern, options) { + this.options = options; + this.analyzer = createAnalyzer({ + isCaseSensitive: options.isCaseSensitive, + ignoreDiacritics: options.ignoreDiacritics, + tokenize: options.tokenize + }); + const queryTerms = this.analyzer.tokenize(pattern); + const { df, fieldCount } = options._invertedIndex; + this.termSearchers = []; + this.idfWeights = []; + for (const term of queryTerms) { + this.termSearchers.push(new BitapSearch(term, { + location: options.location, + threshold: options.threshold, + distance: options.distance, + includeMatches: options.includeMatches, + findAllMatches: options.findAllMatches, + minMatchCharLength: options.minMatchCharLength, + isCaseSensitive: options.isCaseSensitive, + ignoreDiacritics: options.ignoreDiacritics, + ignoreLocation: true + })); + const docFreq = df.get(term) || 0; + const idf = Math.log(1 + (fieldCount - docFreq + .5) / (docFreq + .5)); + this.idfWeights.push(idf); + } + this.combineAll = options.tokenMatch === "all"; + this.numTerms = this.termSearchers.length; + this.useMask = this.numTerms <= 31; + } + searchIn(text) { + if (!this.termSearchers.length) return { + isMatch: false, + score: 1 + }; + const allIndices = []; + let weightedScore = 0; + let maxPossibleScore = 0; + let matchedCount = 0; + let matchedMask = 0; + const matchedTerms = this.combineAll && !this.useMask ? /* @__PURE__ */ new Set() : null; + for (let i = 0; i < this.termSearchers.length; i++) { + const result = this.termSearchers[i].searchIn(text); + const idf = this.idfWeights[i]; + maxPossibleScore += idf; + if (result.isMatch) { + matchedCount++; + weightedScore += idf * (1 - result.score); + if (result.indices) allIndices.push(...result.indices); + if (this.combineAll) if (this.useMask) matchedMask |= 1 << i; + else matchedTerms.add(i); + } + } + if (matchedCount === 0) return { + isMatch: false, + score: 1 + }; + const normalized = maxPossibleScore > 0 ? 1 - weightedScore / maxPossibleScore : 0; + const searchResult = { + isMatch: true, + score: Math.max(.001, normalized) + }; + if (this.options.includeMatches && allIndices.length) searchResult.indices = mergeIndices(allIndices); + if (this.combineAll) { + if (this.useMask) searchResult.matchedMask = matchedMask; + else searchResult.matchedTerms = matchedTerms; + searchResult.termCount = this.numTerms; + } + return searchResult; + } +}; +//#endregion +//#region src/search/token/InvertedIndex.ts function addField(index, text, docIdx, analyzer) { - const tokens = analyzer.tokenize(text); - if (!tokens.length) return; - index.fieldCount++; - index.docFieldCount.set(docIdx, (index.docFieldCount.get(docIdx) || 0) + 1); - - // We count each (doc, term, field) once — repeated occurrences within the - // same field don't multiply df. - const distinctTerms = new Set(tokens); - let perDocTerms = index.docTermFieldHits.get(docIdx); - if (!perDocTerms) { - perDocTerms = new Map(); - index.docTermFieldHits.set(docIdx, perDocTerms); - } - for (const term of distinctTerms) { - perDocTerms.set(term, (perDocTerms.get(term) || 0) + 1); - index.df.set(term, (index.df.get(term) || 0) + 1); - } + const tokens = analyzer.tokenize(text); + if (!tokens.length) return; + index.fieldCount++; + index.docFieldCount.set(docIdx, (index.docFieldCount.get(docIdx) || 0) + 1); + const distinctTerms = new Set(tokens); + let perDocTerms = index.docTermFieldHits.get(docIdx); + if (!perDocTerms) { + perDocTerms = /* @__PURE__ */ new Map(); + index.docTermFieldHits.set(docIdx, perDocTerms); + } + for (const term of distinctTerms) { + perDocTerms.set(term, (perDocTerms.get(term) || 0) + 1); + index.df.set(term, (index.df.get(term) || 0) + 1); + } } function ingestRecord(index, record, keyCount, analyzer) { - const { - i: docIdx, - v, - $: fields - } = record; - if (v !== undefined) { - addField(index, v, docIdx, analyzer); - return; - } - if (!fields) return; - for (let keyIdx = 0; keyIdx < keyCount; keyIdx++) { - const value = fields[keyIdx]; - if (!value) continue; - if (Array.isArray(value)) { - for (const sub of value) addField(index, sub.v, docIdx, analyzer); - } else { - addField(index, value.v, docIdx, analyzer); - } - } + const { i: docIdx, v, $: fields } = record; + if (v !== void 0) { + addField(index, v, docIdx, analyzer); + return; + } + if (!fields) return; + for (let keyIdx = 0; keyIdx < keyCount; keyIdx++) { + const value = fields[keyIdx]; + if (!value) continue; + if (Array.isArray(value)) for (const sub of value) addField(index, sub.v, docIdx, analyzer); + else addField(index, value.v, docIdx, analyzer); + } } function buildInvertedIndex(records, keyCount, analyzer) { - const index = { - fieldCount: 0, - df: new Map(), - docFieldCount: new Map(), - docTermFieldHits: new Map() - }; - for (const record of records) { - ingestRecord(index, record, keyCount, analyzer); - } - return index; + const index = { + fieldCount: 0, + df: /* @__PURE__ */ new Map(), + docFieldCount: /* @__PURE__ */ new Map(), + docTermFieldHits: /* @__PURE__ */ new Map() + }; + for (const record of records) ingestRecord(index, record, keyCount, analyzer); + return index; } function addToInvertedIndex(index, record, keyCount, analyzer) { - ingestRecord(index, record, keyCount, analyzer); + ingestRecord(index, record, keyCount, analyzer); } function removeFromInvertedIndex(index, docIdx) { - const fieldCount = index.docFieldCount.get(docIdx); - if (fieldCount === undefined) return; - index.fieldCount -= fieldCount; - index.docFieldCount.delete(docIdx); - const perDocTerms = index.docTermFieldHits.get(docIdx); - if (!perDocTerms) return; - for (const [term, hits] of perDocTerms) { - const next = (index.df.get(term) || 0) - hits; - if (next <= 0) { - index.df.delete(term); - } else { - index.df.set(term, next); - } - } - index.docTermFieldHits.delete(docIdx); + const fieldCount = index.docFieldCount.get(docIdx); + if (fieldCount === void 0) return; + index.fieldCount -= fieldCount; + index.docFieldCount.delete(docIdx); + const perDocTerms = index.docTermFieldHits.get(docIdx); + if (!perDocTerms) return; + for (const [term, hits] of perDocTerms) { + const next = (index.df.get(term) || 0) - hits; + if (next <= 0) index.df.delete(term); + else index.df.set(term, next); + } + index.docTermFieldHits.delete(docIdx); } - -// Removes the given docIdx entries and renumbers the remaining per-doc maps -// so they stay in sync with FuseIndex's contiguous renumbering on remove. function removeAndShiftInvertedIndex(index, removedIndices) { - if (removedIndices.length === 0) return; - - // De-dup and sort so the shift computation is O(log k) per lookup. - const sorted = Array.from(new Set(removedIndices)).sort((a, b) => a - b); - for (const idx of sorted) { - removeFromInvertedIndex(index, idx); - } - - // For any surviving oldIdx, its new idx is oldIdx minus the number of - // removed indices strictly less than oldIdx. - const shift = oldIdx => { - let lo = 0; - let hi = sorted.length; - while (lo < hi) { - const mid = lo + hi >>> 1; - if (sorted[mid] < oldIdx) lo = mid + 1;else hi = mid; - } - return oldIdx - lo; - }; - const firstRemoved = sorted[0]; - const shiftedDocFieldCount = new Map(); - for (const [oldKey, count] of index.docFieldCount) { - shiftedDocFieldCount.set(oldKey > firstRemoved ? shift(oldKey) : oldKey, count); - } - index.docFieldCount = shiftedDocFieldCount; - const shiftedDocTermFieldHits = new Map(); - for (const [oldKey, terms] of index.docTermFieldHits) { - shiftedDocTermFieldHits.set(oldKey > firstRemoved ? shift(oldKey) : oldKey, terms); - } - index.docTermFieldHits = shiftedDocTermFieldHits; + if (removedIndices.length === 0) return; + const sorted = Array.from(new Set(removedIndices)).sort((a, b) => a - b); + for (const idx of sorted) removeFromInvertedIndex(index, idx); + const shift = (oldIdx) => { + let lo = 0; + let hi = sorted.length; + while (lo < hi) { + const mid = lo + hi >>> 1; + if (sorted[mid] < oldIdx) lo = mid + 1; + else hi = mid; + } + return oldIdx - lo; + }; + const firstRemoved = sorted[0]; + const shiftedDocFieldCount = /* @__PURE__ */ new Map(); + for (const [oldKey, count] of index.docFieldCount) shiftedDocFieldCount.set(oldKey > firstRemoved ? shift(oldKey) : oldKey, count); + index.docFieldCount = shiftedDocFieldCount; + const shiftedDocTermFieldHits = /* @__PURE__ */ new Map(); + for (const [oldKey, terms] of index.docTermFieldHits) shiftedDocTermFieldHits.set(oldKey > firstRemoved ? shift(oldKey) : oldKey, terms); + index.docTermFieldHits = shiftedDocTermFieldHits; } -class Fuse { - // Statics are assigned in entry.ts - - constructor(docs, options, index) { - this.options = { - ...Config, - ...options - }; - if (this.options.useExtendedSearch && false) ; - if (this.options.useTokenSearch && false) ; - this._keyStore = new KeyStore(this.options.keys); - this._docs = docs; - this._myIndex = null; - this._invertedIndex = null; - this.setCollection(docs, index); - this._lastQuery = null; - this._lastSearcher = null; - } - _getSearcher(query) { - if (this._lastQuery === query) { - return this._lastSearcher; - } - const opts = this._invertedIndex ? { - ...this.options, - _invertedIndex: this._invertedIndex - } : this.options; - const searcher = createSearcher(query, opts); - this._lastQuery = query; - this._lastSearcher = searcher; - return searcher; - } - setCollection(docs, index) { - this._docs = docs; - if (index && !(index instanceof FuseIndex)) { - throw new Error(INCORRECT_INDEX_TYPE); - } - this._myIndex = index || createIndex(this.options.keys, this._docs, { - getFn: this.options.getFn, - fieldNormWeight: this.options.fieldNormWeight - }); - if (this.options.useTokenSearch) { - const analyzer = createAnalyzer({ - isCaseSensitive: this.options.isCaseSensitive, - ignoreDiacritics: this.options.ignoreDiacritics, - tokenize: this.options.tokenize - }); - this._invertedIndex = buildInvertedIndex(this._myIndex.records, this._myIndex.keys.length, analyzer); - } - this._invalidateSearcherCache(); - } - add(doc) { - if (!isDefined(doc)) { - return; - } - this._docs.push(doc); - const record = this._myIndex.add(doc, this._docs.length - 1); - - // Skip inverted-index bookkeeping when no record was appended (blank - // strings produce null). The previous code read `records[records.length-1]` - // unconditionally, which would re-ingest the previous doc on `add("")`. - if (this._invertedIndex && record) { - const analyzer = createAnalyzer({ - isCaseSensitive: this.options.isCaseSensitive, - ignoreDiacritics: this.options.ignoreDiacritics, - tokenize: this.options.tokenize - }); - addToInvertedIndex(this._invertedIndex, record, this._myIndex.keys.length, analyzer); - } - this._invalidateSearcherCache(); - } - remove(predicate = () => false) { - const results = []; - const indicesToRemove = []; - for (let i = 0, len = this._docs.length; i < len; i += 1) { - if (predicate(this._docs[i], i)) { - results.push(this._docs[i]); - indicesToRemove.push(i); - } - } - if (indicesToRemove.length) { - if (this._invertedIndex) { - removeAndShiftInvertedIndex(this._invertedIndex, indicesToRemove); - } - - // Filter docs in a single pass instead of reverse-splicing - const toRemove = new Set(indicesToRemove); - this._docs = this._docs.filter((_, i) => !toRemove.has(i)); - this._myIndex.removeAll(indicesToRemove); - this._invalidateSearcherCache(); - } - return results; - } - removeAt(idx) { - // Validate before any mutation. The previous code spliced `_docs` first - // and let FuseIndex.removeAt throw afterward — partial-state on invalid - // input. Atomic now. - if (!Number.isInteger(idx) || idx < 0 || idx >= this._docs.length) { - throw new Error(INVALID_DOC_INDEX); - } - if (this._invertedIndex) { - removeAndShiftInvertedIndex(this._invertedIndex, [idx]); - } - const doc = this._docs.splice(idx, 1)[0]; - this._myIndex.removeAt(idx); - this._invalidateSearcherCache(); - return doc; - } - _invalidateSearcherCache() { - this._lastQuery = null; - this._lastSearcher = null; - } - getIndex() { - return this._myIndex; - } - search(query, options) { - const { - limit = -1 - } = options || {}; - const { - includeMatches, - includeScore, - shouldSort, - sortFn, - ignoreFieldNorm - } = this.options; - - // Empty string query returns all docs (useful for search UIs) - if (isString(query) && !query.trim()) { - let docs = this._docs.map((item, idx) => ({ - item, - refIndex: idx - })); - if (isNumber(limit) && limit > -1) { - docs = docs.slice(0, limit); - } - return docs; - } - const useHeap = isNumber(limit) && limit > 0 && isString(query); - let results; - if (useHeap) { - const heap = new MaxHeap(limit); - if (isString(this._docs[0])) { - this._searchStringList(query, { - heap, - ignoreFieldNorm - }); - } else { - this._searchObjectList(query, { - heap, - ignoreFieldNorm - }); - } - results = heap.extractSorted(sortFn); - } else { - results = isString(query) ? isString(this._docs[0]) ? this._searchStringList(query) : this._searchObjectList(query) : this._searchLogical(query); - computeScore(results, { - ignoreFieldNorm - }); - if (shouldSort) { - results.sort(sortFn); - } - if (isNumber(limit) && limit > -1) { - results = results.slice(0, limit); - } - } - return format(results, this._docs, { - includeMatches, - includeScore - }); - } - _searchStringList(query, { - heap, - ignoreFieldNorm - } = {}) { - const searcher = this._getSearcher(query); - const requireAllTokens = this.options.useTokenSearch && this.options.tokenMatch === 'all'; - const { - records - } = this._myIndex; - const results = heap ? null : []; - - // Iterate over every string in the index - records.forEach(({ - v: text, - i: idx, - n: norm - }) => { - if (!isDefined(text)) { - return; - } - const searchResult = searcher.searchIn(text); - if (searchResult.isMatch) { - const match = { - score: searchResult.score, - value: text, - norm: norm, - indices: searchResult.indices - }; - if (requireAllTokens) { - match.matchedMask = searchResult.matchedMask; - match.matchedTerms = searchResult.matchedTerms; - match.termCount = searchResult.termCount; - } - const matches = [match]; - - // Record-level AND gate (token search `tokenMatch: 'all'`), applied - // before heap insertion so `limit` returns the same top-N as unlimited. - if (!requireAllTokens || this._coversAllTokens(matches)) { - const result = { - item: text, - idx, - matches - }; - if (heap) { - result.score = computeScoreSingle(result.matches, { - ignoreFieldNorm - }); - if (heap.shouldInsert(result.score)) { - heap.insert(result); - } - } else { - results.push(result); - } - } - } - }); - return results; - } - _searchLogical(query) { - const expression = parse(query, this.options); - const evaluate = (node, item, idx) => { - if (!('children' in node)) { - const { - keyId, - searcher - } = node; - let matches; - if (keyId === null) { - // Keyless entry: search across all keys - matches = []; - this._myIndex.keys.forEach((key, keyIndex) => { - matches.push(...this._findMatches({ - key, - value: item[keyIndex], - searcher: searcher - })); - }); - } else { - matches = this._findMatches({ - key: this._keyStore.get(keyId), - value: this._myIndex.getValueForItemAtKeyId(item, keyId), - searcher: searcher - }); - } - if (matches && matches.length) { - return [{ - idx, - item, - matches - }]; - } - return []; - } - const { - children, - operator - } = node; - const res = []; - for (let i = 0, len = children.length; i < len; i += 1) { - const child = children[i]; - const result = evaluate(child, item, idx); - if (result.length) { - res.push(...result); - } else if (operator === LogicalOperator.AND) { - return []; - } - } - return res; - }; - const records = this._myIndex.records; - const resultMap = new Map(); - const results = []; - records.forEach(({ - $: item, - i: idx - }) => { - if (isDefined(item)) { - const expResults = evaluate(expression, item, idx); - if (expResults.length) { - // Dedupe when adding - if (!resultMap.has(idx)) { - resultMap.set(idx, { - idx, - item, - matches: [] - }); - results.push(resultMap.get(idx)); - } - expResults.forEach(({ - matches - }) => { - resultMap.get(idx).matches.push(...matches); - }); - } - } - }); - return results; - } - - // When a search involves inverse patterns (e.g. !Syrup), the aggregation - // across keys switches from "ANY key matches" to "ALL keys must match." - // This is signaled by hasInverse on the SearchResult from ExtendedSearch. - // - // For mixed patterns like "^hello !Syrup", a key failure is ambiguous — - // it could be the positive or inverse term that failed. In that case we - // conservatively exclude the item, which is strictly better than the old - // behavior of including it. See: https://github.com/krisk/Fuse/issues/712 - _searchObjectList(query, { - heap, - ignoreFieldNorm - } = {}) { - const searcher = this._getSearcher(query); - const requireAllTokens = this.options.useTokenSearch && this.options.tokenMatch === 'all'; - const { - keys, - records - } = this._myIndex; - const results = heap ? null : []; - - // List is Array - records.forEach(({ - $: item, - i: idx - }) => { - if (!isDefined(item)) { - return; - } - const matches = []; - let anyKeyFailed = false; - let hasInverse = false; - - // Iterate over every key (i.e, path), and fetch the value at that key - keys.forEach((key, keyIndex) => { - const keyMatches = this._findMatches({ - key, - value: item[keyIndex], - searcher - }); - if (keyMatches.length) { - matches.push(...keyMatches); - if (keyMatches[0].hasInverse) { - hasInverse = true; - } - } else { - anyKeyFailed = true; - } - }); - - // If the search involves inverse patterns, ALL keys must match - if (hasInverse && anyKeyFailed) { - return; - } - - // Record-level AND gate (token search `tokenMatch: 'all'`): every query - // term must be covered across the record's field/array-element matches. - // Applied before heap insertion so `limit` returns the same top-N. - if (matches.length && (!requireAllTokens || this._coversAllTokens(matches))) { - const result = { - idx, - item, - matches - }; - if (heap) { - result.score = computeScoreSingle(result.matches, { - ignoreFieldNorm - }); - if (heap.shouldInsert(result.score)) { - heap.insert(result); - } - } else { - results.push(result); - } - } - }); - return results; - } - _findMatches({ - key, - value, - searcher - }) { - if (!isDefined(value)) { - return []; - } - const matches = []; - if (isArray(value)) { - value.forEach(({ - v: text, - i: idx, - n: norm - }) => { - if (!isDefined(text)) { - return; - } - const searchResult = searcher.searchIn(text); - if (searchResult.isMatch) { - const match = { - score: searchResult.score, - key, - value: text, - idx, - norm, - indices: searchResult.indices, - hasInverse: searchResult.hasInverse - }; - // Carry token-search AND coverage only when present, so the default - // (non-token / 'any') MatchScore keeps its original object shape. - if (searchResult.termCount !== undefined) { - match.matchedMask = searchResult.matchedMask; - match.matchedTerms = searchResult.matchedTerms; - match.termCount = searchResult.termCount; - } - matches.push(match); - } - }); - } else { - const { - v: text, - n: norm - } = value; - const searchResult = searcher.searchIn(text); - if (searchResult.isMatch) { - const match = { - score: searchResult.score, - key, - value: text, - norm, - indices: searchResult.indices, - hasInverse: searchResult.hasInverse - }; - if (searchResult.termCount !== undefined) { - match.matchedMask = searchResult.matchedMask; - match.matchedTerms = searchResult.matchedTerms; - match.termCount = searchResult.termCount; - } - matches.push(match); - } - } - return matches; - } - - // Record-level AND gate for token search (`tokenMatch: 'all'`). Returns true - // unless the matched terms across ALL of a record's field/array-element - // matches fail to cover every query term. `termCount` is only set by - // TokenSearch in 'all' mode, so non-token / 'any' searches always pass. - _coversAllTokens(matches) { - const termCount = matches.length ? matches[0].termCount : undefined; - if (termCount === undefined) { - return true; - } - if (termCount <= MAX_MASK_TERMS) { - let coverage = 0; - for (let i = 0; i < matches.length; i++) { - coverage |= matches[i].matchedMask || 0; - } - return coverage === 2 ** termCount - 1; - } - const coverage = new Set(); - for (let i = 0; i < matches.length; i++) { - const terms = matches[i].matchedTerms; - if (terms) { - for (const t of terms) { - coverage.add(t); - } - } - } - return coverage.size === termCount; - } -} +//#endregion +//#region src/core/index.ts +var Fuse = class { + constructor(docs, options, index) { + this.options = { + ...Config, + ...options + }; + if (this.options.useExtendedSearch && false); + if (this.options.useTokenSearch && false); + this._keyStore = new KeyStore(this.options.keys); + this._docs = docs; + this._myIndex = null; + this._invertedIndex = null; + this.setCollection(docs, index); + this._lastQuery = null; + this._lastSearcher = null; + } + _getSearcher(query) { + if (this._lastQuery === query) return this._lastSearcher; + const searcher = createSearcher(query, this._invertedIndex ? { + ...this.options, + _invertedIndex: this._invertedIndex + } : this.options); + this._lastQuery = query; + this._lastSearcher = searcher; + return searcher; + } + setCollection(docs, index) { + this._docs = docs; + if (index && !(index instanceof FuseIndex)) throw new Error(INCORRECT_INDEX_TYPE); + this._myIndex = index || createIndex(this.options.keys, this._docs, { + getFn: this.options.getFn, + fieldNormWeight: this.options.fieldNormWeight + }); + if (this.options.useTokenSearch) { + const analyzer = createAnalyzer({ + isCaseSensitive: this.options.isCaseSensitive, + ignoreDiacritics: this.options.ignoreDiacritics, + tokenize: this.options.tokenize + }); + this._invertedIndex = buildInvertedIndex(this._myIndex.records, this._myIndex.keys.length, analyzer); + } + this._invalidateSearcherCache(); + } + add(doc) { + if (!isDefined(doc)) return; + this._docs.push(doc); + const record = this._myIndex.add(doc, this._docs.length - 1); + if (this._invertedIndex && record) { + const analyzer = createAnalyzer({ + isCaseSensitive: this.options.isCaseSensitive, + ignoreDiacritics: this.options.ignoreDiacritics, + tokenize: this.options.tokenize + }); + addToInvertedIndex(this._invertedIndex, record, this._myIndex.keys.length, analyzer); + } + this._invalidateSearcherCache(); + } + remove(predicate = () => false) { + const results = []; + const indicesToRemove = []; + for (let i = 0, len = this._docs.length; i < len; i += 1) if (predicate(this._docs[i], i)) { + results.push(this._docs[i]); + indicesToRemove.push(i); + } + if (indicesToRemove.length) { + if (this._invertedIndex) removeAndShiftInvertedIndex(this._invertedIndex, indicesToRemove); + const toRemove = new Set(indicesToRemove); + this._docs = this._docs.filter((_, i) => !toRemove.has(i)); + this._myIndex.removeAll(indicesToRemove); + this._invalidateSearcherCache(); + } + return results; + } + removeAt(idx) { + if (!Number.isInteger(idx) || idx < 0 || idx >= this._docs.length) throw new Error(INVALID_DOC_INDEX); + if (this._invertedIndex) removeAndShiftInvertedIndex(this._invertedIndex, [idx]); + const doc = this._docs.splice(idx, 1)[0]; + this._myIndex.removeAt(idx); + this._invalidateSearcherCache(); + return doc; + } + _invalidateSearcherCache() { + this._lastQuery = null; + this._lastSearcher = null; + } + getIndex() { + return this._myIndex; + } + search(query, options) { + const { limit = -1 } = options || {}; + const { includeMatches, includeScore, shouldSort, sortFn, ignoreFieldNorm } = this.options; + if (isString(query) && !query.trim()) { + let docs = this._docs.map((item, idx) => ({ + item, + refIndex: idx + })); + if (isNumber(limit) && limit > -1) docs = docs.slice(0, limit); + return docs; + } + const useHeap = isNumber(limit) && limit > 0 && isString(query); + let results; + if (useHeap) { + const heap = new MaxHeap(limit); + if (isString(this._docs[0])) this._searchStringList(query, { + heap, + ignoreFieldNorm + }); + else this._searchObjectList(query, { + heap, + ignoreFieldNorm + }); + results = heap.extractSorted(sortFn); + } else { + results = isString(query) ? isString(this._docs[0]) ? this._searchStringList(query) : this._searchObjectList(query) : this._searchLogical(query); + computeScore(results, { ignoreFieldNorm }); + if (shouldSort) results.sort(sortFn); + if (isNumber(limit) && limit > -1) results = results.slice(0, limit); + } + return format(results, this._docs, { + includeMatches, + includeScore + }); + } + _searchStringList(query, { heap, ignoreFieldNorm } = {}) { + const searcher = this._getSearcher(query); + const requireAllTokens = this.options.useTokenSearch && this.options.tokenMatch === "all"; + const { records } = this._myIndex; + const results = heap ? null : []; + records.forEach(({ v: text, i: idx, n: norm }) => { + if (!isDefined(text)) return; + const searchResult = searcher.searchIn(text); + if (searchResult.isMatch) { + const match = { + score: searchResult.score, + value: text, + norm, + indices: searchResult.indices + }; + if (requireAllTokens) { + match.matchedMask = searchResult.matchedMask; + match.matchedTerms = searchResult.matchedTerms; + match.termCount = searchResult.termCount; + } + const matches = [match]; + if (!requireAllTokens || this._coversAllTokens(matches)) { + const result = { + item: text, + idx, + matches + }; + if (heap) { + result.score = computeScoreSingle(result.matches, { ignoreFieldNorm }); + if (heap.shouldInsert(result.score)) heap.insert(result); + } else results.push(result); + } + } + }); + return results; + } + _searchLogical(query) { + const expression = parse(query, this.options); + const evaluate = (node, item, idx) => { + if (!("children" in node)) { + const { keyId, searcher } = node; + let matches; + if (keyId === null) { + matches = []; + this._myIndex.keys.forEach((key, keyIndex) => { + matches.push(...this._findMatches({ + key, + value: item[keyIndex], + searcher + })); + }); + } else matches = this._findMatches({ + key: this._keyStore.get(keyId), + value: this._myIndex.getValueForItemAtKeyId(item, keyId), + searcher + }); + if (matches && matches.length) return [{ + idx, + item, + matches + }]; + return []; + } + const { children, operator } = node; + const res = []; + for (let i = 0, len = children.length; i < len; i += 1) { + const child = children[i]; + const result = evaluate(child, item, idx); + if (result.length) res.push(...result); + else if (operator === LogicalOperator.AND) return []; + } + return res; + }; + const records = this._myIndex.records; + const resultMap = /* @__PURE__ */ new Map(); + const results = []; + records.forEach(({ $: item, i: idx }) => { + if (isDefined(item)) { + const expResults = evaluate(expression, item, idx); + if (expResults.length) { + if (!resultMap.has(idx)) { + resultMap.set(idx, { + idx, + item, + matches: [] + }); + results.push(resultMap.get(idx)); + } + expResults.forEach(({ matches }) => { + resultMap.get(idx).matches.push(...matches); + }); + } + } + }); + return results; + } + _searchObjectList(query, { heap, ignoreFieldNorm } = {}) { + const searcher = this._getSearcher(query); + const requireAllTokens = this.options.useTokenSearch && this.options.tokenMatch === "all"; + const { keys, records } = this._myIndex; + const results = heap ? null : []; + records.forEach(({ $: item, i: idx }) => { + if (!isDefined(item)) return; + const matches = []; + let anyKeyFailed = false; + let hasInverse = false; + keys.forEach((key, keyIndex) => { + const keyMatches = this._findMatches({ + key, + value: item[keyIndex], + searcher + }); + if (keyMatches.length) { + matches.push(...keyMatches); + if (keyMatches[0].hasInverse) hasInverse = true; + } else anyKeyFailed = true; + }); + if (hasInverse && anyKeyFailed) return; + if (matches.length && (!requireAllTokens || this._coversAllTokens(matches))) { + const result = { + idx, + item, + matches + }; + if (heap) { + result.score = computeScoreSingle(result.matches, { ignoreFieldNorm }); + if (heap.shouldInsert(result.score)) heap.insert(result); + } else results.push(result); + } + }); + return results; + } + _findMatches({ key, value, searcher }) { + if (!isDefined(value)) return []; + const matches = []; + if (isArray(value)) value.forEach(({ v: text, i: idx, n: norm }) => { + if (!isDefined(text)) return; + const searchResult = searcher.searchIn(text); + if (searchResult.isMatch) { + const match = { + score: searchResult.score, + key, + value: text, + idx, + norm, + indices: searchResult.indices, + hasInverse: searchResult.hasInverse + }; + if (searchResult.termCount !== void 0) { + match.matchedMask = searchResult.matchedMask; + match.matchedTerms = searchResult.matchedTerms; + match.termCount = searchResult.termCount; + } + matches.push(match); + } + }); + else { + const { v: text, n: norm } = value; + const searchResult = searcher.searchIn(text); + if (searchResult.isMatch) { + const match = { + score: searchResult.score, + key, + value: text, + norm, + indices: searchResult.indices, + hasInverse: searchResult.hasInverse + }; + if (searchResult.termCount !== void 0) { + match.matchedMask = searchResult.matchedMask; + match.matchedTerms = searchResult.matchedTerms; + match.termCount = searchResult.termCount; + } + matches.push(match); + } + } + return matches; + } + _coversAllTokens(matches) { + const termCount = matches.length ? matches[0].termCount : void 0; + if (termCount === void 0) return true; + if (termCount <= 31) { + let coverage = 0; + for (let i = 0; i < matches.length; i++) coverage |= matches[i].matchedMask || 0; + return coverage === 2 ** termCount - 1; + } + const coverage = /* @__PURE__ */ new Set(); + for (let i = 0; i < matches.length; i++) { + const terms = matches[i].matchedTerms; + if (terms) for (const t of terms) coverage.add(t); + } + return coverage.size === termCount; + } +}; -Fuse.version = '7.4.1'; +//#endregion +//#region src/entry.ts +Fuse.version = "7.4.1"; Fuse.createIndex = createIndex; Fuse.parseIndex = parseIndex; Fuse.config = Config; -Fuse.match = function (pattern, text, options) { - // Token search needs corpus statistics (df, fieldCount) that a one-off - // string comparison can't provide. Reject it here so the contract is the - // same in the full and basic builds — without this guard, the full build - // crashes with an opaque TypeError and the basic build silently falls back - // to fuzzy matching. - if (options && options.useTokenSearch) { - throw new Error(FUSE_MATCH_TOKEN_SEARCH_UNSUPPORTED); - } - const searcher = createSearcher(pattern, { - ...Config, - ...options - }); - return searcher.searchIn(text); +Fuse.match = function(pattern, text, options) { + if (options && options.useTokenSearch) throw new Error(FUSE_MATCH_TOKEN_SEARCH_UNSUPPORTED); + return createSearcher(pattern, { + ...Config, + ...options + }).searchIn(text); }; -{ - Fuse.parseQuery = parse; -} -{ - register(ExtendedSearch); -} -{ - register(TokenSearch); -} -Fuse.use = function (...plugins) { - plugins.forEach(plugin => register(plugin)); +Fuse.parseQuery = parse; +register(ExtendedSearch); +register(TokenSearch); +Fuse.use = function(...plugins) { + plugins.forEach((plugin) => register(plugin)); }; +var entry_default = Fuse; -// Re-export public types - -module.exports = Fuse; +//#endregion +module.exports = entry_default; \ No newline at end of file diff --git a/dist/fuse.d.ts b/dist/fuse.d.ts index 305d432df..893e99281 100644 --- a/dist/fuse.d.ts +++ b/dist/fuse.d.ts @@ -1,38 +1,38 @@ -// Type definitions for Fuse.js v7.4.1 -// TypeScript v6.0.3 + +//#region src/types.d.ts type RangeTuple = [number, number]; interface SearchResult { - isMatch: boolean; - score: number; - indices?: ReadonlyArray; - /** @internal Aggregation flag for extended-search inverse terms. */ - hasInverse?: boolean; - /** - * @internal Token-search `tokenMatch: 'all'` coverage for this text. - * `matchedMask` bit `i` ⇒ query term `i` matched here (≤31-term fast path); - * `matchedTerms` is the equivalent set for the ≥32-term fallback. - */ - matchedMask?: number; - /** @internal */ - matchedTerms?: Set; - /** @internal Query token count; descriptor for the record-level AND gate. */ - termCount?: number; + isMatch: boolean; + score: number; + indices?: ReadonlyArray; + /** @internal Aggregation flag for extended-search inverse terms. */ + hasInverse?: boolean; + /** + * @internal Token-search `tokenMatch: 'all'` coverage for this text. + * `matchedMask` bit `i` ⇒ query term `i` matched here (≤31-term fast path); + * `matchedTerms` is the equivalent set for the ≥32-term fallback. + */ + matchedMask?: number; + /** @internal */ + matchedTerms?: Set; + /** @internal Query token count; descriptor for the record-level AND gate. */ + termCount?: number; } interface Searcher { - searchIn(text: string): SearchResult; + searchIn(text: string): SearchResult; } interface FuseOptionKeyObject { - name: string | string[]; - weight?: number; - getFn?: (obj: T) => ReadonlyArray | string | null | undefined; + name: string | string[]; + weight?: number; + getFn?: (obj: T) => ReadonlyArray | string | null | undefined; } type FuseOptionKey = FuseOptionKeyObject | string | string[]; interface KeyObject { - path: string[]; - id: string; - weight: number; - src: string | string[]; - getFn?: ((obj: any) => ReadonlyArray | string | null | undefined) | null; + path: string[]; + id: string; + weight: number; + src: string | string[]; + getFn?: ((obj: any) => ReadonlyArray | string | null | undefined) | null; } type FuseGetFunction = (obj: T, path: string | string[]) => ReadonlyArray | string; type GetFunction = (obj: any, path: string | string[]) => any; @@ -44,290 +44,317 @@ type GetFunction = (obj: any, path: string | string[]) => any; */ type FuseTokenizeFunction = (text: string) => string[]; interface NormInterface { - get(value: string): number; - clear(): void; + get(value: string): number; + clear(): void; } interface RecordEntryObject { - /** The text value */ - v: string; - /** The field-length norm */ - n: number; + /** The text value */ + v: string; + /** The field-length norm */ + n: number; } type RecordEntryArrayItem = ReadonlyArray; type RecordEntry = { - [key: string]: RecordEntryObject | RecordEntryArrayItem; + [key: string]: RecordEntryObject | RecordEntryArrayItem; }; interface FuseIndexObjectRecord { - /** The index of the record in the source list */ - i: number; - $: RecordEntry; + /** The index of the record in the source list */ + i: number; + $: RecordEntry; } interface FuseIndexStringRecord { - /** The index of the record in the source list */ - i: number; - /** The text value */ - v: string; - /** The field-length norm */ - n: number; + /** The index of the record in the source list */ + i: number; + /** The text value */ + v: string; + /** The field-length norm */ + n: number; } type FuseIndexRecords = ReadonlyArray | ReadonlyArray; interface IndexRecord { - i: number; - v?: string; - n?: number; - $?: Record; + i: number; + v?: string; + n?: number; + $?: Record; } interface SubRecord { - v: string; - i?: number; - n: number; + v: string; + i?: number; + n: number; } type FuseSortFunctionItem = { - [key: string]: { - $: string; - } | { - $: string; - idx: number; - }[]; + [key: string]: { + $: string; + } | { + $: string; + idx: number; + }[]; }; type FuseSortFunctionMatch = { - score: number; - key: KeyObject; - value: string; - indices: ReadonlyArray[]; + score: number; + key: KeyObject; + value: string; + indices: ReadonlyArray[]; }; type FuseSortFunctionMatchList = FuseSortFunctionMatch & { - idx: number; + idx: number; }; type FuseSortFunctionArg = { - idx: number; - item: FuseSortFunctionItem; - score: number; - matches?: (FuseSortFunctionMatch | FuseSortFunctionMatchList)[]; + idx: number; + item: FuseSortFunctionItem; + score: number; + matches?: (FuseSortFunctionMatch | FuseSortFunctionMatchList)[]; }; type FuseSortFunction = (a: FuseSortFunctionArg, b: FuseSortFunctionArg) => number; interface MatchScore { - score: number; - key?: KeyObject | null; - value: string; - idx?: number; - hasInverse?: boolean; - norm: number; - indices?: ReadonlyArray; - /** @internal Token-search `tokenMatch: 'all'` coverage carried up for record-level gating. */ - matchedMask?: number; - /** @internal */ - matchedTerms?: Set; - /** @internal */ - termCount?: number; + score: number; + key?: KeyObject | null; + value: string; + idx?: number; + hasInverse?: boolean; + norm: number; + indices?: ReadonlyArray; + /** @internal Token-search `tokenMatch: 'all'` coverage carried up for record-level gating. */ + matchedMask?: number; + /** @internal */ + matchedTerms?: Set; + /** @internal */ + termCount?: number; } interface InternalResult { - idx: number; - item: any; - score?: number; - matches: MatchScore[]; + idx: number; + item: any; + score?: number; + matches: MatchScore[]; } interface IFuseOptions { - /** Indicates whether comparisons should be case sensitive. */ - isCaseSensitive?: boolean; - /** Indicates whether comparisons should ignore diacritics (accents). */ - ignoreDiacritics?: boolean; - /** Determines how close the match must be to the fuzzy location. */ - distance?: number; - /** When true, the matching function will continue to the end of a search pattern even if a perfect match has already been located in the string. */ - findAllMatches?: boolean; - /** The function to use to retrieve an object's value at the provided path. */ - getFn?: FuseGetFunction; - /** When `true`, search will ignore `location` and `distance`. */ - ignoreLocation?: boolean; - /** When `true`, the calculation for the relevance score will ignore the field-length norm. */ - ignoreFieldNorm?: boolean; - /** Determines how much the field-length norm affects scoring. */ - fieldNormWeight?: number; - /** Whether the matches should be included in the result set. */ - includeMatches?: boolean; - /** Whether the score should be included in the result set. */ - includeScore?: boolean; - /** List of keys that will be searched. */ - keys?: Array>; - /** Determines approximately where in the text is the pattern expected to be found. */ - location?: number; - /** Only the matches whose length exceeds this value will be returned. */ - minMatchCharLength?: number; - /** Whether to sort the result list, by score. */ - shouldSort?: boolean; - /** The function to use to sort all the results. */ - sortFn?: FuseSortFunction; - /** At what point does the match algorithm give up. */ - threshold?: number; - /** When `true`, it enables the use of unix-like search commands. */ - useExtendedSearch?: boolean; - /** When `true`, enables token search with TF-IDF scoring. */ - useTokenSearch?: boolean; - /** - * Tokenizer used by `useTokenSearch`, applied identically at index-build - * and query time. Accepts either a global `RegExp` or a function returning - * `string[]`. Defaults to `/[\p{L}\p{M}\p{N}_]+/gu`, which handles CJK, - * Cyrillic, Greek, Arabic, Hebrew, Devanagari, etc. out of the box. Use a - * function form (e.g. wrapping `Intl.Segmenter`) for word-segmentation in - * scripts without whitespace boundaries. - */ - tokenize?: RegExp | FuseTokenizeFunction; - /** - * How the words of a multi-word query combine, for `useTokenSearch` only. - * `'any'` (default) returns a record if it matches any query word (OR); - * `'all'` returns it only when every query word matches somewhere in the - * record — any field or array element (AND). Use `'all'` for filtering, - * where adding a word should narrow the results. Has no effect unless - * `useTokenSearch` is `true`. - */ - tokenMatch?: 'all' | 'any'; + /** Indicates whether comparisons should be case sensitive. */ + isCaseSensitive?: boolean; + /** Indicates whether comparisons should ignore diacritics (accents). */ + ignoreDiacritics?: boolean; + /** Determines how close the match must be to the fuzzy location. */ + distance?: number; + /** When true, the matching function will continue to the end of a search pattern even if a perfect match has already been located in the string. */ + findAllMatches?: boolean; + /** The function to use to retrieve an object's value at the provided path. */ + getFn?: FuseGetFunction; + /** When `true`, search will ignore `location` and `distance`. */ + ignoreLocation?: boolean; + /** When `true`, the calculation for the relevance score will ignore the field-length norm. */ + ignoreFieldNorm?: boolean; + /** Determines how much the field-length norm affects scoring. */ + fieldNormWeight?: number; + /** Whether the matches should be included in the result set. */ + includeMatches?: boolean; + /** Whether the score should be included in the result set. */ + includeScore?: boolean; + /** List of keys that will be searched. */ + keys?: Array>; + /** Determines approximately where in the text is the pattern expected to be found. */ + location?: number; + /** Only the matches whose length exceeds this value will be returned. */ + minMatchCharLength?: number; + /** Whether to sort the result list, by score. */ + shouldSort?: boolean; + /** The function to use to sort all the results. */ + sortFn?: FuseSortFunction; + /** At what point does the match algorithm give up. */ + threshold?: number; + /** When `true`, it enables the use of unix-like search commands. */ + useExtendedSearch?: boolean; + /** When `true`, enables token search with TF-IDF scoring. */ + useTokenSearch?: boolean; + /** + * Tokenizer used by `useTokenSearch`, applied identically at index-build + * and query time. Accepts either a global `RegExp` or a function returning + * `string[]`. Defaults to `/[\p{L}\p{M}\p{N}_]+/gu`, which handles CJK, + * Cyrillic, Greek, Arabic, Hebrew, Devanagari, etc. out of the box. Use a + * function form (e.g. wrapping `Intl.Segmenter`) for word-segmentation in + * scripts without whitespace boundaries. + */ + tokenize?: RegExp | FuseTokenizeFunction; + /** + * How the words of a multi-word query combine, for `useTokenSearch` only. + * `'any'` (default) returns a record if it matches any query word (OR); + * `'all'` returns it only when every query word matches somewhere in the + * record — any field or array element (AND). Use `'all'` for filtering, + * where adding a word should narrow the results. Has no effect unless + * `useTokenSearch` is `true`. + */ + tokenMatch?: 'all' | 'any'; } interface FuseIndexOptions { - getFn?: FuseGetFunction; - fieldNormWeight?: number; + getFn?: FuseGetFunction; + fieldNormWeight?: number; } interface FuseSearchOptions { - limit?: number; + limit?: number; } interface FuseResultMatch { - indices: ReadonlyArray; - key?: string; - refIndex?: number; - value?: string; + indices: ReadonlyArray; + key?: string; + refIndex?: number; + value?: string; } interface FuseResult { - item: T; - refIndex: number; - score?: number; - matches?: ReadonlyArray; + item: T; + refIndex: number; + score?: number; + matches?: ReadonlyArray; } type Expression = string | { - [key: string]: string; + [key: string]: string; } | { - $path: ReadonlyArray; - $val: string; + $path: ReadonlyArray; + $val: string; } | { - $and?: Expression[]; + $and?: Expression[]; } | { - $or?: Expression[]; + $or?: Expression[]; }; - +//#endregion +//#region src/tools/FuseIndex.d.ts declare class FuseIndex { - norm: NormInterface; - getFn: GetFunction; - isCreated: boolean; - docs: ReadonlyArray; + norm: NormInterface; + getFn: GetFunction; + isCreated: boolean; + docs: ReadonlyArray; + records: IndexRecord[]; + keys: KeyObject[]; + _keysMap: Record; + constructor({ + getFn, + fieldNormWeight + }?: FuseIndexOptions); + setSources(docs?: ReadonlyArray): void; + setIndexRecords(records?: IndexRecord[]): void; + setKeys(keys?: KeyObject[]): void; + create(): void; + add(doc: T, docIndex: number): IndexRecord | null; + removeAt(idx: number): void; + removeAll(indices: number[]): void; + getValueForItemAtKeyId(item: any, keyId: string): any; + size(): number; + _createStringRecord(doc: string, docIndex: number): IndexRecord | null; + _createObjectRecord(doc: any, docIndex: number): IndexRecord; + toJSON(): { + keys: ReadonlyArray>; records: IndexRecord[]; - keys: KeyObject[]; - _keysMap: Record; - constructor({ getFn, fieldNormWeight }?: FuseIndexOptions); - setSources(docs?: ReadonlyArray): void; - setIndexRecords(records?: IndexRecord[]): void; - setKeys(keys?: KeyObject[]): void; - create(): void; - add(doc: T, docIndex: number): IndexRecord | null; - removeAt(idx: number): void; - removeAll(indices: number[]): void; - getValueForItemAtKeyId(item: any, keyId: string): any; - size(): number; - _createStringRecord(doc: string, docIndex: number): IndexRecord | null; - _createObjectRecord(doc: any, docIndex: number): IndexRecord; - toJSON(): { - keys: ReadonlyArray>; - records: IndexRecord[]; - }; + }; } -declare function createIndex(keys: FuseOptionKey[], docs: ReadonlyArray, { getFn, fieldNormWeight }?: FuseIndexOptions): FuseIndex; +declare function createIndex(keys: FuseOptionKey[], docs: ReadonlyArray, { + getFn, + fieldNormWeight +}?: FuseIndexOptions): FuseIndex; declare function parseIndex(data: { - keys: ReadonlyArray; - records: IndexRecord[]; -}, { getFn, fieldNormWeight }?: FuseIndexOptions): FuseIndex; - + keys: ReadonlyArray; + records: IndexRecord[]; +}, { + getFn, + fieldNormWeight +}?: FuseIndexOptions): FuseIndex; +//#endregion +//#region src/tools/KeyStore.d.ts declare class KeyStore { - _keys: KeyObject[]; - _keyMap: Record; - constructor(keys: FuseOptionKey[]); - get(keyId: string): KeyObject; - keys(): KeyObject[]; - toJSON(): string; + _keys: KeyObject[]; + _keyMap: Record; + constructor(keys: FuseOptionKey[]); + get(keyId: string): KeyObject; + keys(): KeyObject[]; + toJSON(): string; } - +//#endregion +//#region src/core/queryParser.d.ts interface ParsedLeaf { - keyId: string | null; - pattern: string; - searcher?: Searcher; + keyId: string | null; + pattern: string; + searcher?: Searcher; } interface ParsedOperator { - children: ParsedNode[]; - operator: string; + children: ParsedNode[]; + operator: string; } type ParsedNode = ParsedLeaf | ParsedOperator; -declare function parse(query: Expression, options: any, { auto }?: { - auto?: boolean | undefined; +declare function parse(query: Expression, options: any, { + auto +}?: { + auto?: boolean | undefined; }): ParsedNode; - +//#endregion +//#region src/core/config.d.ts declare const Config: Required>; - +//#endregion +//#region src/tools/MaxHeap.d.ts declare class MaxHeap { - limit: number; - heap: InternalResult[]; - constructor(limit: number); - get size(): number; - shouldInsert(score: number): boolean; - insert(item: InternalResult): void; - extractSorted(sortFn: (a: InternalResult, b: InternalResult) => number): InternalResult[]; - _bubbleUp(i: number): void; - _sinkDown(i: number): void; + limit: number; + heap: InternalResult[]; + constructor(limit: number); + get size(): number; + shouldInsert(score: number): boolean; + insert(item: InternalResult): void; + extractSorted(sortFn: (a: InternalResult, b: InternalResult) => number): InternalResult[]; + _bubbleUp(i: number): void; + _sinkDown(i: number): void; } - +//#endregion +//#region src/search/token/InvertedIndex.d.ts interface InvertedIndexData { - fieldCount: number; - df: Map; - docFieldCount: Map; - docTermFieldHits: Map>; + fieldCount: number; + df: Map; + docFieldCount: Map; + docTermFieldHits: Map>; } - +//#endregion +//#region src/core/index.d.ts interface HeapSearchOptions { - heap?: MaxHeap; - ignoreFieldNorm?: boolean; + heap?: MaxHeap; + ignoreFieldNorm?: boolean; } declare class Fuse { - options: Required>; - _keyStore: KeyStore; - _docs: T[]; - _myIndex: FuseIndex; - _invertedIndex: InvertedIndexData | null; - _lastQuery: string | null; - _lastSearcher: Searcher | null; - static version: string; - static createIndex: typeof createIndex; - static parseIndex: typeof parseIndex; - static config: typeof Config; - static parseQuery: typeof parse; - static use: (...plugins: any[]) => void; - static match: (pattern: string, text: string, options?: IFuseOptions) => SearchResult; - constructor(docs: ReadonlyArray, options?: IFuseOptions, index?: FuseIndex); - _getSearcher(query: string): Searcher; - setCollection(docs: ReadonlyArray, index?: FuseIndex): void; - add(doc: T): void; - remove(predicate?: (doc: T, idx: number) => boolean): T[]; - removeAt(idx: number): T; - _invalidateSearcherCache(): void; - getIndex(): FuseIndex; - search(query: string | Expression, options?: FuseSearchOptions): FuseResult[]; - _searchStringList(query: string, { heap, ignoreFieldNorm }?: HeapSearchOptions): InternalResult[] | null; - _searchLogical(query: Expression): InternalResult[]; - _searchObjectList(query: string, { heap, ignoreFieldNorm }?: HeapSearchOptions): InternalResult[] | null; - _findMatches({ key, value, searcher }: { - key: KeyObject | null; - value: SubRecord | SubRecord[] | undefined; - searcher: Searcher; - }): MatchScore[]; - _coversAllTokens(matches: MatchScore[]): boolean; + options: Required>; + _keyStore: KeyStore; + _docs: T[]; + _myIndex: FuseIndex; + _invertedIndex: InvertedIndexData | null; + _lastQuery: string | null; + _lastSearcher: Searcher | null; + static version: string; + static createIndex: typeof createIndex; + static parseIndex: typeof parseIndex; + static config: typeof Config; + static parseQuery: typeof parse; + static use: (...plugins: any[]) => void; + static match: (pattern: string, text: string, options?: IFuseOptions) => SearchResult; + constructor(docs: ReadonlyArray, options?: IFuseOptions, index?: FuseIndex); + _getSearcher(query: string): Searcher; + setCollection(docs: ReadonlyArray, index?: FuseIndex): void; + add(doc: T): void; + remove(predicate?: (doc: T, idx: number) => boolean): T[]; + removeAt(idx: number): T; + _invalidateSearcherCache(): void; + getIndex(): FuseIndex; + search(query: string | Expression, options?: FuseSearchOptions): FuseResult[]; + _searchStringList(query: string, { + heap, + ignoreFieldNorm + }?: HeapSearchOptions): InternalResult[] | null; + _searchLogical(query: Expression): InternalResult[]; + _searchObjectList(query: string, { + heap, + ignoreFieldNorm + }?: HeapSearchOptions): InternalResult[] | null; + _findMatches({ + key, + value, + searcher + }: { + key: KeyObject | null; + value: SubRecord | SubRecord[] | undefined; + searcher: Searcher; + }): MatchScore[]; + _coversAllTokens(matches: MatchScore[]): boolean; } - -export { FuseIndex, Fuse as default }; -export type { Expression, FuseGetFunction, FuseIndexObjectRecord, FuseIndexOptions, FuseIndexRecords, FuseIndexStringRecord, FuseOptionKey, FuseOptionKeyObject, FuseResult, FuseResultMatch, FuseSearchOptions, FuseSortFunction, FuseSortFunctionArg, FuseSortFunctionItem, FuseSortFunctionMatch, FuseSortFunctionMatchList, FuseTokenizeFunction, IFuseOptions, RangeTuple, RecordEntry, RecordEntryArrayItem, RecordEntryObject, SearchResult }; +//#endregion +export { type Expression, type FuseGetFunction, type FuseIndex, type FuseIndexObjectRecord, type FuseIndexOptions, type FuseIndexRecords, type FuseIndexStringRecord, type FuseOptionKey, type FuseOptionKeyObject, type FuseResult, type FuseResultMatch, type FuseSearchOptions, type FuseSortFunction, type FuseSortFunctionArg, type FuseSortFunctionItem, type FuseSortFunctionMatch, type FuseSortFunctionMatchList, type FuseTokenizeFunction, type IFuseOptions, type RangeTuple, type RecordEntry, type RecordEntryArrayItem, type RecordEntryObject, type SearchResult, Fuse as default }; \ No newline at end of file diff --git a/dist/fuse.min.cjs b/dist/fuse.min.cjs index 350130073..53dbeb3a3 100644 --- a/dist/fuse.min.cjs +++ b/dist/fuse.min.cjs @@ -6,4 +6,4 @@ * * http://www.apache.org/licenses/LICENSE-2.0 */ -"use strict";function e(e){return Array.isArray?Array.isArray(e):"[object Array]"===u(e)}function t(e){return null==e?"":function(e){if("string"==typeof e)return e;if("bigint"==typeof e)return e.toString();const t=e+"";return"0"==t&&1/e==-1/0?"-0":t}(e)}function s(e){return"string"==typeof e}function i(e){return"number"==typeof e}function n(e){return!0===e||!1===e||function(e){return r(e)&&null!==e}(e)&&"[object Boolean]"==u(e)}function r(e){return"object"==typeof e}function o(e){return null!=e}function c(e){return!e.trim().length}function u(e){return null==e?void 0===e?"[object Undefined]":"[object Null]":Object.prototype.toString.call(e)}const h="Invalid doc index: must be a non-negative integer within the bounds of the docs array",a=Object.prototype.hasOwnProperty;class l{constructor(e){this._keys=[],this._keyMap={};let t=0;e.forEach(e=>{const s=d(e);this._keys.push(s),this._keyMap[s.id]=s,t+=s.weight}),this._keys.forEach(e=>{e.weight/=t})}get(e){return this._keyMap[e]}keys(){return this._keys}toJSON(){return JSON.stringify(this._keys)}}function d(t){let i=null,n=null,r=null,o=1,c=null;if(s(t)||e(t))r=t,i=f(t),n=g(t);else{if(!a.call(t,"name"))throw new Error((e=>`Missing ${e} property in key`)("name"));const e=t.name;if(r=e,a.call(t,"weight")&&void 0!==t.weight&&(o=t.weight,o<=0))throw new Error((e=>`Property 'weight' in key '${e}' must be a positive integer`)(g(e)));i=f(e),n=g(e),c=t.getFn??null}return{path:i,id:n,weight:o,src:r,getFn:c}}function f(t){return e(t)?t:t.split(".")}function g(t){return e(t)?t.join("."):t}const m={useExtendedSearch:!1,useTokenSearch:!1,tokenize:void 0,tokenMatch:"any",getFn:function(r,c){const u=[];let h=!1;const a=(r,c,l,d)=>{if(o(r))if(c[l]){const f=r[c[l]];if(!o(f))return;if(l===c.length-1&&(s(f)||i(f)||n(f)||"bigint"==typeof f))u.push(void 0!==d?{v:t(f),i:d}:t(f));else if(e(f)){h=!0;for(let e=0,t=f.length;ee.score===t.score?e.idx{this._keysMap[e.id]=t})}create(){if(this.isCreated||!this.docs.length)return;this.isCreated=!0;const e=this.docs.length;this.records=new Array(e);let t=0;if(s(this.docs[0]))for(let s=0;se&&(this.records[t].i-=1)}removeAll(e){const t=new Set;for(const s of e)Number.isInteger(s)&&s>=0&&t.add(s);if(0===t.size)return;this.records=this.records.filter(e=>!t.has(e.i));const s=Array.from(t).sort((e,t)=>e-t);for(const e of this.records){let t=0,i=s.length;for(;t>>1;s[n]t),records:this.records}}}function C(e,t,{getFn:s=p.getFn,fieldNormWeight:i=p.fieldNormWeight}={}){const n=new A({getFn:s,fieldNormWeight:i});return n.setKeys(e.map(d)),n.setSources(t),n.create(),n}const M=32;function y(e,t,s,{location:i=p.location,distance:n=p.distance,threshold:r=p.threshold,findAllMatches:o=p.findAllMatches,minMatchCharLength:c=p.minMatchCharLength,includeMatches:u=p.includeMatches,ignoreLocation:h=p.ignoreLocation}={}){if(t.length>M)throw new Error(`Pattern length exceeds max of ${M}.`);const a=t.length,l=e.length,d=Math.max(0,Math.min(i,l));let f=r,g=d;const m=(e,t)=>{const s=e/a;if(h)return s;const i=Math.abs(d-t);return n?s+i/n:i?1:s},A=c>1||u,C=A?Array(l):[];let y;for(;(y=e.indexOf(t,g))>-1;){const e=m(0,y);if(f=Math.min(e,f),g=y+a,A){let e=0;for(;e=r;i-=1){const n=i-1,o=s[e[n]];if(u[i]=(u[i+1]<<1|1)&o,t&&(u[i]|=(F[i+1]|F[i])<<1|1|F[i+1]),u[i]&D&&(x=m(t,n),x<=f)){if(f=x,g=n,v=t,g<=d)break;r=Math.max(1,2*d-g)}}if(m(t+1,d)>f)break;F=u}if(A&&g>=0){const t=Math.min(l-1,g+a-1+v);for(let i=g;i<=t;i+=1)s[e[i]]&&(C[i]=1)}const _={isMatch:g>=0,score:Math.max(.001,x)};if(A){const e=function(e=[],t=p.minMatchCharLength){const s=[];let i=-1,n=-1,r=0;for(let o=e.length;r=t&&s.push([i,n]),i=-1)}return e[r-1]&&r-i>=t&&s.push([i,r-1]),s}(C,c);e.length?u&&(_.indices=e):_.isMatch=!1}return _}function F(e){const t={};for(let s=0,i=e.length;se[0]-t[0]||e[1]-t[1]);const t=[e[0]];for(let s=1,i=e.length;se.normalize("NFD").replace(/[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]/g,"").replace(k,e=>v[e]):e=>e;class _{constructor(e,{location:t=p.location,threshold:s=p.threshold,distance:i=p.distance,includeMatches:n=p.includeMatches,findAllMatches:r=p.findAllMatches,minMatchCharLength:o=p.minMatchCharLength,isCaseSensitive:c=p.isCaseSensitive,ignoreDiacritics:u=p.ignoreDiacritics,ignoreLocation:h=p.ignoreLocation}={}){if(this.options={location:t,threshold:s,distance:i,includeMatches:n,findAllMatches:r,minMatchCharLength:o,isCaseSensitive:c,ignoreDiacritics:u,ignoreLocation:h},e=c?e:e.toLowerCase(),e=u?D(e):e,this.pattern=e,this.chunks=[],!this.pattern.length)return;const a=(e,t)=>{this.chunks.push({pattern:e,alphabet:F(e),startIndex:t})},l=this.pattern.length;if(l>M){let e=0;const t=l%M,s=l-t;for(;e{const{isMatch:g,score:m,indices:p}=y(e,t,s,{location:n+f,distance:r,threshold:o,findAllMatches:c,minMatchCharLength:u,includeMatches:i,ignoreLocation:h});g&&(d=!0),l+=m,g&&p&&a.push(...p)});const f={isMatch:d,score:d?l/this.chunks.length:1};return d&&i&&(f.indices=x(a)),f}}const E=new Set(["fuzzy","include"]);function B(e){return e.startsWith("inverse")}const S=[{type:"exact",multiRegex:/^="(.*)"$/,singleRegex:/^=(.*)$/,create:e=>({type:"exact",search(t){const s=t===e;return{isMatch:s,score:s?0:1,indices:[0,e.length-1]}}})},{type:"include",multiRegex:/^'"(.*)"$/,singleRegex:/^'(.*)$/,create:e=>({type:"include",search(t){let s,i=0;const n=[],r=e.length;for(;(s=t.indexOf(e,i))>-1;)i=s+r,n.push([s,i-1]);const o=!!n.length;return{isMatch:o,score:o?0:1,indices:n}}})},{type:"prefix-exact",multiRegex:/^\^"(.*)"$/,singleRegex:/^\^(.*)$/,create:e=>({type:"prefix-exact",search(t){const s=t.startsWith(e);return{isMatch:s,score:s?0:1,indices:[0,e.length-1]}}})},{type:"inverse-prefix-exact",multiRegex:/^!\^"(.*)"$/,singleRegex:/^!\^(.*)$/,create:e=>({type:"inverse-prefix-exact",search(t){const s=!t.startsWith(e);return{isMatch:s,score:s?0:1,indices:[0,t.length-1]}}})},{type:"inverse-suffix-exact",multiRegex:/^!"(.*)"\$$/,singleRegex:/^!(.*)\$$/,create:e=>({type:"inverse-suffix-exact",search(t){const s=!t.endsWith(e);return{isMatch:s,score:s?0:1,indices:[0,t.length-1]}}})},{type:"suffix-exact",multiRegex:/^"(.*)"\$$/,singleRegex:/^(.*)\$$/,create:e=>({type:"suffix-exact",search(t){const s=t.endsWith(e);return{isMatch:s,score:s?0:1,indices:[t.length-e.length,t.length-1]}}})},{type:"inverse-exact",multiRegex:/^!"(.*)"$/,singleRegex:/^!(.*)$/,create:e=>({type:"inverse-exact",search(t){const s=-1===t.indexOf(e);return{isMatch:s,score:s?0:1,indices:[0,t.length-1]}}})},{type:"fuzzy",multiRegex:/^"(.*)"$/,singleRegex:/^(.*)$/,create:(e,t={})=>{const s=new _(e,{location:t.location??p.location,threshold:t.threshold??p.threshold,distance:t.distance??p.distance,includeMatches:t.includeMatches??p.includeMatches,findAllMatches:t.findAllMatches??p.findAllMatches,minMatchCharLength:t.minMatchCharLength??p.minMatchCharLength,isCaseSensitive:t.isCaseSensitive??p.isCaseSensitive,ignoreDiacritics:t.ignoreDiacritics??p.ignoreDiacritics,ignoreLocation:t.ignoreLocation??p.ignoreLocation});return{type:"fuzzy",search:e=>s.searchIn(e)}}}],I=S.length;function w(e,t){const s=e.match(t);return s?s[1]:null}class b{constructor(e,{isCaseSensitive:t=p.isCaseSensitive,ignoreDiacritics:s=p.ignoreDiacritics,includeMatches:i=p.includeMatches,minMatchCharLength:n=p.minMatchCharLength,ignoreLocation:r=p.ignoreLocation,findAllMatches:o=p.findAllMatches,location:c=p.location,threshold:u=p.threshold,distance:h=p.distance}={}){this.query=null,this.options={isCaseSensitive:t,ignoreDiacritics:s,includeMatches:i,minMatchCharLength:n,findAllMatches:o,ignoreLocation:r,location:c,threshold:u,distance:h},e=t?e:e.toLowerCase(),e=s?D(e):e,this.pattern=e,this.query=function(e,t={}){return e.replace(/\\\|/g,"\0").split("|").map(e=>{const s=function(e){const t=[],s=e.length;let i=0;for(;i=s)break;let n=i;for(;n=s||" "===e[t]){n++;break}if("$"===e[t]&&(t+1>=s||" "===e[t+1])){n+=2;break}}n++}t.push(e.substring(i,n)),i=n}else{for(;ne&&!!e.trim()),i=[];for(let e=0,n=s.length;e!(!e[T]&&!e[z]),W=e=>({[T]:Object.keys(e).map(t=>({[t]:e[t]}))});function H(t,i,{auto:n=!0}={}){const o=t=>{if(s(t)){const e={keyId:null,pattern:t};return n&&(e.searcher=$(t,i)),e}const c=Object.keys(t),u=(e=>!!e[R])(t);if(!u&&c.length>1&&!j(t))return o(W(t));if((t=>!e(t)&&r(t)&&!j(t))(t)){const e=u?t[R]:c[0],r=u?t[O]:t[e];if(!s(r))throw new Error((e=>`Invalid value for key ${e}`)(e));const o={keyId:g(e),pattern:r};return n&&(o.searcher=$(r,i)),o}const h={children:[],operator:c[0]};return c.forEach(s=>{const i=t[s];e(i)&&i.forEach(e=>{h.children.push(o(e))})}),h};return j(t)||(t=W(t)),o(t)}function K(e,{ignoreFieldNorm:t=p.ignoreFieldNorm}){let s=1;return e.forEach(({key:e,norm:i,score:n})=>{const r=e?e.weight:null;s*=Math.pow(0===n&&r?Number.EPSILON:n,(r||1)*(t?1:i))}),s}class q{constructor(e){this.limit=e,this.heap=[]}get size(){return this.heap.length}shouldInsert(e){return this.size0;){const s=e-1>>1;if(t[e].score<=t[s].score)break;const i=t[e];t[e]=t[s],t[s]=i,e=s}}_sinkDown(e){const t=this.heap,s=t.length;let i=e;do{const n=2*(e=i)+1,r=2*e+2;if(nt[i].score&&(i=n),rt[i].score&&(i=r),i!==e){const s=t[e];t[e]=t[i],t[i]=s}}while(i!==e)}}function P(e,t,{includeMatches:s=p.includeMatches,includeScore:i=p.includeScore}={}){return e.map(e=>{const{idx:n}=e,r={item:t[n],refIndex:n};return s&&(r.matches=function(e){const t=[];return e.matches.forEach(e=>{if(!o(e.indices)||!e.indices.length)return;const s={indices:e.indices,value:e.value};e.key&&(s.key=e.key.id),e.idx>-1&&(s.refIndex=e.idx),t.push(s)}),t}(e)),i&&(r.score=e.score),r})}const Q=/[\p{L}\p{M}\p{N}_]+/gu;function U({isCaseSensitive:e=!1,ignoreDiacritics:t=!1,tokenize:s}={}){const i=function(e){return"function"==typeof e?t=>e(t):e instanceof RegExp?(e.global,t=>t.match(e)||[]):e=>e.match(Q)||[]}(s);return{tokenize:s=>(e||(s=s.toLowerCase()),t&&(s=D(s)),i(s))}}class J{static condition(e,t){return t.useTokenSearch}constructor(e,t){this.options=t,this.analyzer=U({isCaseSensitive:t.isCaseSensitive,ignoreDiacritics:t.ignoreDiacritics,tokenize:t.tokenize});const s=this.analyzer.tokenize(e),i=t._invertedIndex,{df:n,fieldCount:r}=i;this.termSearchers=[],this.idfWeights=[];for(const e of s){this.termSearchers.push(new _(e,{location:t.location,threshold:t.threshold,distance:t.distance,includeMatches:t.includeMatches,findAllMatches:t.findAllMatches,minMatchCharLength:t.minMatchCharLength,isCaseSensitive:t.isCaseSensitive,ignoreDiacritics:t.ignoreDiacritics,ignoreLocation:!0}));const s=n.get(e)||0,i=Math.log(1+(r-s+.5)/(s+.5));this.idfWeights.push(i)}this.combineAll="all"===t.tokenMatch,this.numTerms=this.termSearchers.length,this.useMask=this.numTerms<=31}searchIn(e){if(!this.termSearchers.length)return{isMatch:!1,score:1};const t=[];let s=0,i=0,n=0,r=0;const o=this.combineAll&&!this.useMask?new Set:null;for(let c=0;c0?1-s/i:0,u={isMatch:!0,score:Math.max(.001,c)};return this.options.includeMatches&&t.length&&(u.indices=x(t)),this.combineAll&&(this.useMask?u.matchedMask=r:u.matchedTerms=o,u.termCount=this.numTerms),u}}function V(e,t,s,i){const n=i.tokenize(t);if(!n.length)return;e.fieldCount++,e.docFieldCount.set(s,(e.docFieldCount.get(s)||0)+1);const r=new Set(n);let o=e.docTermFieldHits.get(s);o||(o=new Map,e.docTermFieldHits.set(s,o));for(const t of r)o.set(t,(o.get(t)||0)+1),e.df.set(t,(e.df.get(t)||0)+1)}function G(e,t,s,i){const{i:n,v:r,$:o}=t;if(void 0===r){if(o)for(let t=0;te-t);for(const t of s)X(e,t);const i=e=>{let t=0,i=s.length;for(;t>>1;s[n]n?i(t):t,s);e.docFieldCount=r;const o=new Map;for(const[t,s]of e.docTermFieldHits)o.set(t>n?i(t):t,s);e.docTermFieldHits=o}class Z{constructor(e,t,s){this.options={...p,...t},this.options.useExtendedSearch,this.options.useTokenSearch,this._keyStore=new l(this.options.keys),this._docs=e,this._myIndex=null,this._invertedIndex=null,this.setCollection(e,s),this._lastQuery=null,this._lastSearcher=null}_getSearcher(e){if(this._lastQuery===e)return this._lastSearcher;const t=$(e,this._invertedIndex?{...this.options,_invertedIndex:this._invertedIndex}:this.options);return this._lastQuery=e,this._lastSearcher=t,t}setCollection(e,t){if(this._docs=e,t&&!(t instanceof A))throw new Error("Incorrect 'index' type");if(this._myIndex=t||C(this.options.keys,this._docs,{getFn:this.options.getFn,fieldNormWeight:this.options.fieldNormWeight}),this.options.useTokenSearch){const e=U({isCaseSensitive:this.options.isCaseSensitive,ignoreDiacritics:this.options.ignoreDiacritics,tokenize:this.options.tokenize});this._invertedIndex=function(e,t,s){const i={fieldCount:0,df:new Map,docFieldCount:new Map,docTermFieldHits:new Map};for(const n of e)G(i,n,t,s);return i}(this._myIndex.records,this._myIndex.keys.length,e)}this._invalidateSearcherCache()}add(e){if(!o(e))return;this._docs.push(e);const t=this._myIndex.add(e,this._docs.length-1);if(this._invertedIndex&&t){const e=U({isCaseSensitive:this.options.isCaseSensitive,ignoreDiacritics:this.options.ignoreDiacritics,tokenize:this.options.tokenize});!function(e,t,s,i){G(e,t,s,i)}(this._invertedIndex,t,this._myIndex.keys.length,e)}this._invalidateSearcherCache()}remove(e=()=>!1){const t=[],s=[];for(let i=0,n=this._docs.length;i!e.has(s)),this._myIndex.removeAll(s),this._invalidateSearcherCache()}return t}removeAt(e){if(!Number.isInteger(e)||e<0||e>=this._docs.length)throw new Error(h);this._invertedIndex&&Y(this._invertedIndex,[e]);const t=this._docs.splice(e,1)[0];return this._myIndex.removeAt(e),this._invalidateSearcherCache(),t}_invalidateSearcherCache(){this._lastQuery=null,this._lastSearcher=null}getIndex(){return this._myIndex}search(e,t){const{limit:n=-1}=t||{},{includeMatches:r,includeScore:o,shouldSort:c,sortFn:u,ignoreFieldNorm:h}=this.options;if(s(e)&&!e.trim()){let e=this._docs.map((e,t)=>({item:e,refIndex:t}));return i(n)&&n>-1&&(e=e.slice(0,n)),e}let a;if(i(n)&&n>0&&s(e)){const t=new q(n);s(this._docs[0])?this._searchStringList(e,{heap:t,ignoreFieldNorm:h}):this._searchObjectList(e,{heap:t,ignoreFieldNorm:h}),a=t.extractSorted(u)}else a=s(e)?s(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e),function(e,{ignoreFieldNorm:t=p.ignoreFieldNorm}){e.forEach(e=>{e.score=K(e.matches,{ignoreFieldNorm:t})})}(a,{ignoreFieldNorm:h}),c&&a.sort(u),i(n)&&n>-1&&(a=a.slice(0,n));return P(a,this._docs,{includeMatches:r,includeScore:o})}_searchStringList(e,{heap:t,ignoreFieldNorm:s}={}){const i=this._getSearcher(e),n=this.options.useTokenSearch&&"all"===this.options.tokenMatch,{records:r}=this._myIndex,c=t?null:[];return r.forEach(({v:e,i:r,n:u})=>{if(!o(e))return;const h=i.searchIn(e);if(h.isMatch){const i={score:h.score,value:e,norm:u,indices:h.indices};n&&(i.matchedMask=h.matchedMask,i.matchedTerms=h.matchedTerms,i.termCount=h.termCount);const o=[i];if(!n||this._coversAllTokens(o)){const i={item:e,idx:r,matches:o};t?(i.score=K(i.matches,{ignoreFieldNorm:s}),t.shouldInsert(i.score)&&t.insert(i)):c.push(i)}}}),c}_searchLogical(e){const t=H(e,this.options),s=(e,t,i)=>{if(!("children"in e)){const{keyId:s,searcher:n}=e;let r;return null===s?(r=[],this._myIndex.keys.forEach((e,s)=>{r.push(...this._findMatches({key:e,value:t[s],searcher:n}))})):r=this._findMatches({key:this._keyStore.get(s),value:this._myIndex.getValueForItemAtKeyId(t,s),searcher:n}),r&&r.length?[{idx:i,item:t,matches:r}]:[]}const{children:n,operator:r}=e,o=[];for(let e=0,c=n.length;e{if(o(e)){const o=s(t,e,i);o.length&&(n.has(i)||(n.set(i,{idx:i,item:e,matches:[]}),r.push(n.get(i))),o.forEach(({matches:e})=>{n.get(i).matches.push(...e)}))}}),r}_searchObjectList(e,{heap:t,ignoreFieldNorm:s}={}){const i=this._getSearcher(e),n=this.options.useTokenSearch&&"all"===this.options.tokenMatch,{keys:r,records:c}=this._myIndex,u=t?null:[];return c.forEach(({$:e,i:c})=>{if(!o(e))return;const h=[];let a=!1,l=!1;if(r.forEach((t,s)=>{const n=this._findMatches({key:t,value:e[s],searcher:i});n.length?(h.push(...n),n[0].hasInverse&&(l=!0)):a=!0}),(!l||!a)&&h.length&&(!n||this._coversAllTokens(h))){const i={idx:c,item:e,matches:h};t?(i.score=K(i.matches,{ignoreFieldNorm:s}),t.shouldInsert(i.score)&&t.insert(i)):u.push(i)}}),u}_findMatches({key:t,value:s,searcher:i}){if(!o(s))return[];const n=[];if(e(s))s.forEach(({v:e,i:s,n:r})=>{if(!o(e))return;const c=i.searchIn(e);if(c.isMatch){const i={score:c.score,key:t,value:e,idx:s,norm:r,indices:c.indices,hasInverse:c.hasInverse};void 0!==c.termCount&&(i.matchedMask=c.matchedMask,i.matchedTerms=c.matchedTerms,i.termCount=c.termCount),n.push(i)}});else{const{v:e,n:r}=s,o=i.searchIn(e);if(o.isMatch){const s={score:o.score,key:t,value:e,norm:r,indices:o.indices,hasInverse:o.hasInverse};void 0!==o.termCount&&(s.matchedMask=o.matchedMask,s.matchedTerms=o.matchedTerms,s.termCount=o.termCount),n.push(s)}}return n}_coversAllTokens(e){const t=e.length?e[0].termCount:void 0;if(void 0===t)return!0;if(t<=31){let s=0;for(let t=0;tN(e))},module.exports=Z; \ No newline at end of file +function e(e){return Array.isArray?Array.isArray(e):u(e)===`[object Array]`}function t(e){if(typeof e==`string`)return e;if(typeof e==`bigint`)return e.toString();let t=e+``;return t==`0`&&1/e==-1/0?`-0`:t}function n(e){return e==null?``:t(e)}function r(e){return typeof e==`string`}function i(e){return typeof e==`number`}function a(e){return e===!0||e===!1||s(e)&&u(e)==`[object Boolean]`}function o(e){return typeof e==`object`}function s(e){return o(e)&&e!==null}function c(e){return e!=null}function l(e){return!e.trim().length}function u(e){return e==null?e===void 0?`[object Undefined]`:`[object Null]`:Object.prototype.toString.call(e)}const d=`Invalid doc index: must be a non-negative integer within the bounds of the docs array`,f=e=>`Invalid value for key ${e}`,p=e=>`Pattern length exceeds max of ${e}.`,m=e=>`Missing ${e} property in key`,h=e=>`Property 'weight' in key '${e}' must be a positive integer`,g=Object.prototype.hasOwnProperty;var _=class{constructor(e){this._keys=[],this._keyMap={};let t=0;e.forEach(e=>{let n=v(e);this._keys.push(n),this._keyMap[n.id]=n,t+=n.weight}),this._keys.forEach(e=>{e.weight/=t})}get(e){return this._keyMap[e]}keys(){return this._keys}toJSON(){return JSON.stringify(this._keys)}};function v(t){let n=null,i=null,a=null,o=1,s=null;if(r(t)||e(t))a=t,n=y(t),i=b(t);else{if(!g.call(t,`name`))throw Error(m(`name`));let e=t.name;if(a=e,g.call(t,`weight`)&&t.weight!==void 0&&(o=t.weight,o<=0))throw Error(h(b(e)));n=y(e),i=b(e),s=t.getFn??null}return{path:n,id:i,weight:o,src:a,getFn:s}}function y(t){return e(t)?t:t.split(`.`)}function b(t){return e(t)?t.join(`.`):t}function x(t,o){let s=[],l=!1,u=(t,o,d,f)=>{if(c(t))if(!o[d])s.push(f===void 0?t:{v:t,i:f});else{let p=t[o[d]];if(!c(p))return;if(d===o.length-1&&(r(p)||i(p)||a(p)||typeof p==`bigint`))s.push(f===void 0?n(p):{v:n(p),i:f});else if(e(p)){l=!0;for(let e=0,t=p.length;ee.score===t.score?e.idx{this._keysMap[e.id]=t})}create(){if(this.isCreated||!this.docs.length)return;this.isCreated=!0;let e=this.docs.length;this.records=Array(e);let t=0;if(r(this.docs[0]))for(let n=0;ne&&--this.records[t].i}removeAll(e){let t=new Set;for(let n of e)Number.isInteger(n)&&n>=0&&t.add(n);if(t.size===0)return;this.records=this.records.filter(e=>!t.has(e.i));let n=Array.from(t).sort((e,t)=>e-t);for(let e of this.records){let t=0,r=n.length;for(;t>>1;n[i]t),records:this.records}}};function O(e,t,{getFn:n=E.getFn,fieldNormWeight:r=E.fieldNormWeight}={}){let i=new D({getFn:n,fieldNormWeight:r});return i.setKeys(e.map(v)),i.setSources(t),i.create(),i}function te(e,{getFn:t=E.getFn,fieldNormWeight:n=E.fieldNormWeight}={}){let{keys:r,records:i}=e,a=new D({getFn:t,fieldNormWeight:n});return a.setKeys(r),a.setIndexRecords(i),a}function ne(e=[],t=E.minMatchCharLength){let n=[],r=-1,i=-1,a=0;for(let o=e.length;a=t&&n.push([r,i]),r=-1)}return e[a-1]&&a-r>=t&&n.push([r,a-1]),n}function re(e,t,n,{location:r=E.location,distance:i=E.distance,threshold:a=E.threshold,findAllMatches:o=E.findAllMatches,minMatchCharLength:s=E.minMatchCharLength,includeMatches:c=E.includeMatches,ignoreLocation:l=E.ignoreLocation}={}){if(t.length>32)throw Error(p(32));let u=t.length,d=e.length,f=Math.max(0,Math.min(r,d)),m=a,h=f,g=(e,t)=>{let n=e/u;if(l)return n;let r=Math.abs(f-t);return i?n+r/i:r?1:n},_=s>1||c,v=_?Array(d):[],y;for(;(y=e.indexOf(t,h))>-1;){let e=g(0,y);if(m=Math.min(e,m),h=y+u,_){let e=0;for(;e=a;--r){let i=r-1,o=n[e[i]];if(c[r]=(c[r+1]<<1|1)&o,t&&(c[r]|=(b[r+1]|b[r])<<1|1|b[r+1]),c[r]&w&&(x=g(t,i),x<=m)){if(m=x,h=i,S=t,h<=f)break;a=Math.max(1,2*f-h)}}if(g(t+1,f)>m)break;b=c}if(_&&h>=0){let t=Math.min(d-1,h+u-1+S);for(let r=h;r<=t;r+=1)n[e[r]]&&(v[r]=1)}let T={isMatch:h>=0,score:Math.max(.001,x)};if(_){let e=ne(v,s);e.length?c&&(T.indices=e):T.isMatch=!1}return T}function k(e){let t={};for(let n=0,r=e.length;ne[0]-t[0]||e[1]-t[1]);let t=[e[0]];for(let n=1,r=e.length;ne.normalize(`NFD`).replace(/[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]/g,``).replace(ie,e=>j[e]):e=>e;var N=class{constructor(e,{location:t=E.location,threshold:n=E.threshold,distance:r=E.distance,includeMatches:i=E.includeMatches,findAllMatches:a=E.findAllMatches,minMatchCharLength:o=E.minMatchCharLength,isCaseSensitive:s=E.isCaseSensitive,ignoreDiacritics:c=E.ignoreDiacritics,ignoreLocation:l=E.ignoreLocation}={}){if(this.options={location:t,threshold:n,distance:r,includeMatches:i,findAllMatches:a,minMatchCharLength:o,isCaseSensitive:s,ignoreDiacritics:c,ignoreLocation:l},e=s?e:e.toLowerCase(),e=c?M(e):e,this.pattern=e,this.chunks=[],!this.pattern.length)return;let u=(e,t)=>{this.chunks.push({pattern:e,alphabet:k(e),startIndex:t})},d=this.pattern.length;if(d>32){let e=0,t=d%32,n=d-t;for(;e{let{isMatch:m,score:h,indices:g}=re(e,t,n,{location:i+p,distance:a,threshold:o,findAllMatches:s,minMatchCharLength:c,includeMatches:r,ignoreLocation:l});m&&(f=!0),d+=h,m&&g&&u.push(...g)});let p={isMatch:f,score:f?d/this.chunks.length:1};return f&&r&&(p.indices=A(u)),p}};const P=new Set([`fuzzy`,`include`]);function F(e){return e.startsWith(`inverse`)}const I=[{type:`exact`,multiRegex:/^="(.*)"$/,singleRegex:/^=(.*)$/,create:e=>({type:`exact`,search(t){let n=t===e;return{isMatch:n,score:+!n,indices:[0,e.length-1]}}})},{type:`include`,multiRegex:/^'"(.*)"$/,singleRegex:/^'(.*)$/,create:e=>({type:`include`,search(t){let n=0,r,i=[],a=e.length;for(;(r=t.indexOf(e,n))>-1;)n=r+a,i.push([r,n-1]);let o=!!i.length;return{isMatch:o,score:+!o,indices:i}}})},{type:`prefix-exact`,multiRegex:/^\^"(.*)"$/,singleRegex:/^\^(.*)$/,create:e=>({type:`prefix-exact`,search(t){let n=t.startsWith(e);return{isMatch:n,score:+!n,indices:[0,e.length-1]}}})},{type:`inverse-prefix-exact`,multiRegex:/^!\^"(.*)"$/,singleRegex:/^!\^(.*)$/,create:e=>({type:`inverse-prefix-exact`,search(t){let n=!t.startsWith(e);return{isMatch:n,score:+!n,indices:[0,t.length-1]}}})},{type:`inverse-suffix-exact`,multiRegex:/^!"(.*)"\$$/,singleRegex:/^!(.*)\$$/,create:e=>({type:`inverse-suffix-exact`,search(t){let n=!t.endsWith(e);return{isMatch:n,score:+!n,indices:[0,t.length-1]}}})},{type:`suffix-exact`,multiRegex:/^"(.*)"\$$/,singleRegex:/^(.*)\$$/,create:e=>({type:`suffix-exact`,search(t){let n=t.endsWith(e);return{isMatch:n,score:+!n,indices:[t.length-e.length,t.length-1]}}})},{type:`inverse-exact`,multiRegex:/^!"(.*)"$/,singleRegex:/^!(.*)$/,create:e=>({type:`inverse-exact`,search(t){let n=t.indexOf(e)===-1;return{isMatch:n,score:+!n,indices:[0,t.length-1]}}})},{type:`fuzzy`,multiRegex:/^"(.*)"$/,singleRegex:/^(.*)$/,create:(e,t={})=>{let n=new N(e,{location:t.location??E.location,threshold:t.threshold??E.threshold,distance:t.distance??E.distance,includeMatches:t.includeMatches??E.includeMatches,findAllMatches:t.findAllMatches??E.findAllMatches,minMatchCharLength:t.minMatchCharLength??E.minMatchCharLength,isCaseSensitive:t.isCaseSensitive??E.isCaseSensitive,ignoreDiacritics:t.ignoreDiacritics??E.ignoreDiacritics,ignoreLocation:t.ignoreLocation??E.ignoreLocation});return{type:`fuzzy`,search(e){return n.searchIn(e)}}}}],L=I.length;function ae(e){let t=[],n=e.length,r=0;for(;r=n)break;let i=r;for(;i=n||e[t]===` `){i++;break}if(e[t]===`$`&&(t+1>=n||e[t+1]===` `)){i+=2;break}}i++}t.push(e.substring(r,i)),r=i}else{for(;i{let n=ae(e.replace(/\u0000/g,`|`).trim()).filter(e=>e&&!!e.trim()),r=[];for(let e=0,i=n.length;e!!(e[H.AND]||e[H.OR]),G=e=>!!e[U.PATH],ce=t=>!e(t)&&o(t)&&!W(t),K=e=>({[H.AND]:Object.keys(e).map(t=>({[t]:e[t]}))});function le(t,n,{auto:i=!0}={}){let a=t=>{if(r(t)){let e={keyId:null,pattern:t};return i&&(e.searcher=V(t,n)),e}let o=Object.keys(t),s=G(t);if(!s&&o.length>1&&!W(t))return a(K(t));if(ce(t)){let e=s?t[U.PATH]:o[0],a=s?t[U.PATTERN]:t[e];if(!r(a))throw Error(f(e));let c={keyId:b(e),pattern:a};return i&&(c.searcher=V(a,n)),c}let c={children:[],operator:o[0]};return o.forEach(n=>{let r=t[n];e(r)&&r.forEach(e=>{c.children.push(a(e))})}),c};return W(t)||(t=K(t)),a(t)}function q(e,{ignoreFieldNorm:t=E.ignoreFieldNorm}){let n=1;return e.forEach(({key:e,norm:r,score:i})=>{let a=e?e.weight:null;n*=(i===0&&a?2**-52:i)**+((a||1)*(t?1:r))}),n}function ue(e,{ignoreFieldNorm:t=E.ignoreFieldNorm}){e.forEach(e=>{e.score=q(e.matches,{ignoreFieldNorm:t})})}var de=class{constructor(e){this.limit=e,this.heap=[]}get size(){return this.heap.length}shouldInsert(e){return this.size0;){let n=e-1>>1;if(t[e].score<=t[n].score)break;let r=t[e];t[e]=t[n],t[n]=r,e=n}}_sinkDown(e){let t=this.heap,n=t.length,r=e;do{e=r;let i=2*e+1,a=2*e+2;if(it[r].score&&(r=i),at[r].score&&(r=a),r!==e){let n=t[e];t[e]=t[r],t[r]=n}}while(r!==e)}};function fe(e){let t=[];return e.matches.forEach(e=>{if(!c(e.indices)||!e.indices.length)return;let n={indices:e.indices,value:e.value};e.key&&(n.key=e.key.id),e.idx>-1&&(n.refIndex=e.idx),t.push(n)}),t}function pe(e,t,{includeMatches:n=E.includeMatches,includeScore:r=E.includeScore}={}){return e.map(e=>{let{idx:i}=e,a={item:t[i],refIndex:i};return n&&(a.matches=fe(e)),r&&(a.score=e.score),a})}const me=/[\p{L}\p{M}\p{N}_]+/gu;function he(e){return typeof e==`function`?t=>e(t):e instanceof RegExp?(e.global,t=>t.match(e)||[]):e=>e.match(me)||[]}function J({isCaseSensitive:e=!1,ignoreDiacritics:t=!1,tokenize:n}={}){let r=he(n);return{tokenize(n){return e||(n=n.toLowerCase()),t&&(n=M(n)),r(n)}}}var ge=class{static condition(e,t){return t.useTokenSearch}constructor(e,t){this.options=t,this.analyzer=J({isCaseSensitive:t.isCaseSensitive,ignoreDiacritics:t.ignoreDiacritics,tokenize:t.tokenize});let n=this.analyzer.tokenize(e),{df:r,fieldCount:i}=t._invertedIndex;this.termSearchers=[],this.idfWeights=[];for(let e of n){this.termSearchers.push(new N(e,{location:t.location,threshold:t.threshold,distance:t.distance,includeMatches:t.includeMatches,findAllMatches:t.findAllMatches,minMatchCharLength:t.minMatchCharLength,isCaseSensitive:t.isCaseSensitive,ignoreDiacritics:t.ignoreDiacritics,ignoreLocation:!0}));let n=r.get(e)||0,a=Math.log(1+(i-n+.5)/(n+.5));this.idfWeights.push(a)}this.combineAll=t.tokenMatch===`all`,this.numTerms=this.termSearchers.length,this.useMask=this.numTerms<=31}searchIn(e){if(!this.termSearchers.length)return{isMatch:!1,score:1};let t=[],n=0,r=0,i=0,a=0,o=this.combineAll&&!this.useMask?new Set:null;for(let s=0;s0?1-n/r:0,c={isMatch:!0,score:Math.max(.001,s)};return this.options.includeMatches&&t.length&&(c.indices=A(t)),this.combineAll&&(this.useMask?c.matchedMask=a:c.matchedTerms=o,c.termCount=this.numTerms),c}};function Y(e,t,n,r){let i=r.tokenize(t);if(!i.length)return;e.fieldCount++,e.docFieldCount.set(n,(e.docFieldCount.get(n)||0)+1);let a=new Set(i),o=e.docTermFieldHits.get(n);o||(o=new Map,e.docTermFieldHits.set(n,o));for(let t of a)o.set(t,(o.get(t)||0)+1),e.df.set(t,(e.df.get(t)||0)+1)}function X(e,t,n,r){let{i,v:a,$:o}=t;if(a!==void 0){Y(e,a,i,r);return}if(o)for(let t=0;te-t);for(let t of n)Z(e,t);let r=e=>{let t=0,r=n.length;for(;t>>1;n[i]i?r(t):t,n);e.docFieldCount=a;let o=new Map;for(let[t,n]of e.docTermFieldHits)o.set(t>i?r(t):t,n);e.docTermFieldHits=o}var $=class{constructor(e,t,n){this.options={...E,...t},this.options.useExtendedSearch,this.options.useTokenSearch,this._keyStore=new _(this.options.keys),this._docs=e,this._myIndex=null,this._invertedIndex=null,this.setCollection(e,n),this._lastQuery=null,this._lastSearcher=null}_getSearcher(e){if(this._lastQuery===e)return this._lastSearcher;let t=V(e,this._invertedIndex?{...this.options,_invertedIndex:this._invertedIndex}:this.options);return this._lastQuery=e,this._lastSearcher=t,t}setCollection(e,t){if(this._docs=e,t&&!(t instanceof D))throw Error(`Incorrect 'index' type`);if(this._myIndex=t||O(this.options.keys,this._docs,{getFn:this.options.getFn,fieldNormWeight:this.options.fieldNormWeight}),this.options.useTokenSearch){let e=J({isCaseSensitive:this.options.isCaseSensitive,ignoreDiacritics:this.options.ignoreDiacritics,tokenize:this.options.tokenize});this._invertedIndex=_e(this._myIndex.records,this._myIndex.keys.length,e)}this._invalidateSearcherCache()}add(e){if(!c(e))return;this._docs.push(e);let t=this._myIndex.add(e,this._docs.length-1);if(this._invertedIndex&&t){let e=J({isCaseSensitive:this.options.isCaseSensitive,ignoreDiacritics:this.options.ignoreDiacritics,tokenize:this.options.tokenize});ve(this._invertedIndex,t,this._myIndex.keys.length,e)}this._invalidateSearcherCache()}remove(e=()=>!1){let t=[],n=[];for(let r=0,i=this._docs.length;r!e.has(n)),this._myIndex.removeAll(n),this._invalidateSearcherCache()}return t}removeAt(e){if(!Number.isInteger(e)||e<0||e>=this._docs.length)throw Error(d);this._invertedIndex&&Q(this._invertedIndex,[e]);let t=this._docs.splice(e,1)[0];return this._myIndex.removeAt(e),this._invalidateSearcherCache(),t}_invalidateSearcherCache(){this._lastQuery=null,this._lastSearcher=null}getIndex(){return this._myIndex}search(e,t){let{limit:n=-1}=t||{},{includeMatches:a,includeScore:o,shouldSort:s,sortFn:c,ignoreFieldNorm:l}=this.options;if(r(e)&&!e.trim()){let e=this._docs.map((e,t)=>({item:e,refIndex:t}));return i(n)&&n>-1&&(e=e.slice(0,n)),e}let u=i(n)&&n>0&&r(e),d;if(u){let t=new de(n);r(this._docs[0])?this._searchStringList(e,{heap:t,ignoreFieldNorm:l}):this._searchObjectList(e,{heap:t,ignoreFieldNorm:l}),d=t.extractSorted(c)}else d=r(e)?r(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e),ue(d,{ignoreFieldNorm:l}),s&&d.sort(c),i(n)&&n>-1&&(d=d.slice(0,n));return pe(d,this._docs,{includeMatches:a,includeScore:o})}_searchStringList(e,{heap:t,ignoreFieldNorm:n}={}){let r=this._getSearcher(e),i=this.options.useTokenSearch&&this.options.tokenMatch===`all`,{records:a}=this._myIndex,o=t?null:[];return a.forEach(({v:e,i:a,n:s})=>{if(!c(e))return;let l=r.searchIn(e);if(l.isMatch){let r={score:l.score,value:e,norm:s,indices:l.indices};i&&(r.matchedMask=l.matchedMask,r.matchedTerms=l.matchedTerms,r.termCount=l.termCount);let c=[r];if(!i||this._coversAllTokens(c)){let r={item:e,idx:a,matches:c};t?(r.score=q(r.matches,{ignoreFieldNorm:n}),t.shouldInsert(r.score)&&t.insert(r)):o.push(r)}}}),o}_searchLogical(e){let t=le(e,this.options),n=(e,t,r)=>{if(!(`children`in e)){let{keyId:n,searcher:i}=e,a;return n===null?(a=[],this._myIndex.keys.forEach((e,n)=>{a.push(...this._findMatches({key:e,value:t[n],searcher:i}))})):a=this._findMatches({key:this._keyStore.get(n),value:this._myIndex.getValueForItemAtKeyId(t,n),searcher:i}),a&&a.length?[{idx:r,item:t,matches:a}]:[]}let{children:i,operator:a}=e,o=[];for(let e=0,s=i.length;e{if(c(e)){let o=n(t,e,r);o.length&&(i.has(r)||(i.set(r,{idx:r,item:e,matches:[]}),a.push(i.get(r))),o.forEach(({matches:e})=>{i.get(r).matches.push(...e)}))}}),a}_searchObjectList(e,{heap:t,ignoreFieldNorm:n}={}){let r=this._getSearcher(e),i=this.options.useTokenSearch&&this.options.tokenMatch===`all`,{keys:a,records:o}=this._myIndex,s=t?null:[];return o.forEach(({$:e,i:o})=>{if(!c(e))return;let l=[],u=!1,d=!1;if(a.forEach((t,n)=>{let i=this._findMatches({key:t,value:e[n],searcher:r});i.length?(l.push(...i),i[0].hasInverse&&(d=!0)):u=!0}),!(d&&u)&&l.length&&(!i||this._coversAllTokens(l))){let r={idx:o,item:e,matches:l};t?(r.score=q(r.matches,{ignoreFieldNorm:n}),t.shouldInsert(r.score)&&t.insert(r)):s.push(r)}}),s}_findMatches({key:t,value:n,searcher:r}){if(!c(n))return[];let i=[];if(e(n))n.forEach(({v:e,i:n,n:a})=>{if(!c(e))return;let o=r.searchIn(e);if(o.isMatch){let r={score:o.score,key:t,value:e,idx:n,norm:a,indices:o.indices,hasInverse:o.hasInverse};o.termCount!==void 0&&(r.matchedMask=o.matchedMask,r.matchedTerms=o.matchedTerms,r.termCount=o.termCount),i.push(r)}});else{let{v:e,n:a}=n,o=r.searchIn(e);if(o.isMatch){let n={score:o.score,key:t,value:e,norm:a,indices:o.indices,hasInverse:o.hasInverse};o.termCount!==void 0&&(n.matchedMask=o.matchedMask,n.matchedTerms=o.matchedTerms,n.termCount=o.termCount),i.push(n)}}return i}_coversAllTokens(e){let t=e.length?e[0].termCount:void 0;if(t===void 0)return!0;if(t<=31){let n=0;for(let t=0;tB(e))};var ye=$;module.exports=ye; \ No newline at end of file diff --git a/dist/fuse.min.mjs b/dist/fuse.min.mjs index 0cc0483b1..21c9d8323 100644 --- a/dist/fuse.min.mjs +++ b/dist/fuse.min.mjs @@ -6,4 +6,4 @@ * * http://www.apache.org/licenses/LICENSE-2.0 */ -function e(e){return Array.isArray?Array.isArray(e):"[object Array]"===u(e)}function t(e){return null==e?"":function(e){if("string"==typeof e)return e;if("bigint"==typeof e)return e.toString();const t=e+"";return"0"==t&&1/e==-1/0?"-0":t}(e)}function s(e){return"string"==typeof e}function i(e){return"number"==typeof e}function n(e){return!0===e||!1===e||function(e){return r(e)&&null!==e}(e)&&"[object Boolean]"==u(e)}function r(e){return"object"==typeof e}function o(e){return null!=e}function c(e){return!e.trim().length}function u(e){return null==e?void 0===e?"[object Undefined]":"[object Null]":Object.prototype.toString.call(e)}const h="Invalid doc index: must be a non-negative integer within the bounds of the docs array",a=Object.prototype.hasOwnProperty;class l{constructor(e){this._keys=[],this._keyMap={};let t=0;e.forEach(e=>{const s=d(e);this._keys.push(s),this._keyMap[s.id]=s,t+=s.weight}),this._keys.forEach(e=>{e.weight/=t})}get(e){return this._keyMap[e]}keys(){return this._keys}toJSON(){return JSON.stringify(this._keys)}}function d(t){let i=null,n=null,r=null,o=1,c=null;if(s(t)||e(t))r=t,i=f(t),n=g(t);else{if(!a.call(t,"name"))throw new Error((e=>`Missing ${e} property in key`)("name"));const e=t.name;if(r=e,a.call(t,"weight")&&void 0!==t.weight&&(o=t.weight,o<=0))throw new Error((e=>`Property 'weight' in key '${e}' must be a positive integer`)(g(e)));i=f(e),n=g(e),c=t.getFn??null}return{path:i,id:n,weight:o,src:r,getFn:c}}function f(t){return e(t)?t:t.split(".")}function g(t){return e(t)?t.join("."):t}const m={useExtendedSearch:!1,useTokenSearch:!1,tokenize:void 0,tokenMatch:"any",getFn:function(r,c){const u=[];let h=!1;const a=(r,c,l,d)=>{if(o(r))if(c[l]){const f=r[c[l]];if(!o(f))return;if(l===c.length-1&&(s(f)||i(f)||n(f)||"bigint"==typeof f))u.push(void 0!==d?{v:t(f),i:d}:t(f));else if(e(f)){h=!0;for(let e=0,t=f.length;ee.score===t.score?e.idx{this._keysMap[e.id]=t})}create(){if(this.isCreated||!this.docs.length)return;this.isCreated=!0;const e=this.docs.length;this.records=new Array(e);let t=0;if(s(this.docs[0]))for(let s=0;se&&(this.records[t].i-=1)}removeAll(e){const t=new Set;for(const s of e)Number.isInteger(s)&&s>=0&&t.add(s);if(0===t.size)return;this.records=this.records.filter(e=>!t.has(e.i));const s=Array.from(t).sort((e,t)=>e-t);for(const e of this.records){let t=0,i=s.length;for(;t>>1;s[n]t),records:this.records}}}function C(e,t,{getFn:s=p.getFn,fieldNormWeight:i=p.fieldNormWeight}={}){const n=new A({getFn:s,fieldNormWeight:i});return n.setKeys(e.map(d)),n.setSources(t),n.create(),n}const M=32;function y(e,t,s,{location:i=p.location,distance:n=p.distance,threshold:r=p.threshold,findAllMatches:o=p.findAllMatches,minMatchCharLength:c=p.minMatchCharLength,includeMatches:u=p.includeMatches,ignoreLocation:h=p.ignoreLocation}={}){if(t.length>M)throw new Error(`Pattern length exceeds max of ${M}.`);const a=t.length,l=e.length,d=Math.max(0,Math.min(i,l));let f=r,g=d;const m=(e,t)=>{const s=e/a;if(h)return s;const i=Math.abs(d-t);return n?s+i/n:i?1:s},A=c>1||u,C=A?Array(l):[];let y;for(;(y=e.indexOf(t,g))>-1;){const e=m(0,y);if(f=Math.min(e,f),g=y+a,A){let e=0;for(;e=r;i-=1){const n=i-1,o=s[e[n]];if(u[i]=(u[i+1]<<1|1)&o,t&&(u[i]|=(F[i+1]|F[i])<<1|1|F[i+1]),u[i]&D&&(x=m(t,n),x<=f)){if(f=x,g=n,v=t,g<=d)break;r=Math.max(1,2*d-g)}}if(m(t+1,d)>f)break;F=u}if(A&&g>=0){const t=Math.min(l-1,g+a-1+v);for(let i=g;i<=t;i+=1)s[e[i]]&&(C[i]=1)}const _={isMatch:g>=0,score:Math.max(.001,x)};if(A){const e=function(e=[],t=p.minMatchCharLength){const s=[];let i=-1,n=-1,r=0;for(let o=e.length;r=t&&s.push([i,n]),i=-1)}return e[r-1]&&r-i>=t&&s.push([i,r-1]),s}(C,c);e.length?u&&(_.indices=e):_.isMatch=!1}return _}function F(e){const t={};for(let s=0,i=e.length;se[0]-t[0]||e[1]-t[1]);const t=[e[0]];for(let s=1,i=e.length;se.normalize("NFD").replace(/[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]/g,"").replace(k,e=>v[e]):e=>e;class _{constructor(e,{location:t=p.location,threshold:s=p.threshold,distance:i=p.distance,includeMatches:n=p.includeMatches,findAllMatches:r=p.findAllMatches,minMatchCharLength:o=p.minMatchCharLength,isCaseSensitive:c=p.isCaseSensitive,ignoreDiacritics:u=p.ignoreDiacritics,ignoreLocation:h=p.ignoreLocation}={}){if(this.options={location:t,threshold:s,distance:i,includeMatches:n,findAllMatches:r,minMatchCharLength:o,isCaseSensitive:c,ignoreDiacritics:u,ignoreLocation:h},e=c?e:e.toLowerCase(),e=u?D(e):e,this.pattern=e,this.chunks=[],!this.pattern.length)return;const a=(e,t)=>{this.chunks.push({pattern:e,alphabet:F(e),startIndex:t})},l=this.pattern.length;if(l>M){let e=0;const t=l%M,s=l-t;for(;e{const{isMatch:g,score:m,indices:p}=y(e,t,s,{location:n+f,distance:r,threshold:o,findAllMatches:c,minMatchCharLength:u,includeMatches:i,ignoreLocation:h});g&&(d=!0),l+=m,g&&p&&a.push(...p)});const f={isMatch:d,score:d?l/this.chunks.length:1};return d&&i&&(f.indices=x(a)),f}}const E=new Set(["fuzzy","include"]);function B(e){return e.startsWith("inverse")}const S=[{type:"exact",multiRegex:/^="(.*)"$/,singleRegex:/^=(.*)$/,create:e=>({type:"exact",search(t){const s=t===e;return{isMatch:s,score:s?0:1,indices:[0,e.length-1]}}})},{type:"include",multiRegex:/^'"(.*)"$/,singleRegex:/^'(.*)$/,create:e=>({type:"include",search(t){let s,i=0;const n=[],r=e.length;for(;(s=t.indexOf(e,i))>-1;)i=s+r,n.push([s,i-1]);const o=!!n.length;return{isMatch:o,score:o?0:1,indices:n}}})},{type:"prefix-exact",multiRegex:/^\^"(.*)"$/,singleRegex:/^\^(.*)$/,create:e=>({type:"prefix-exact",search(t){const s=t.startsWith(e);return{isMatch:s,score:s?0:1,indices:[0,e.length-1]}}})},{type:"inverse-prefix-exact",multiRegex:/^!\^"(.*)"$/,singleRegex:/^!\^(.*)$/,create:e=>({type:"inverse-prefix-exact",search(t){const s=!t.startsWith(e);return{isMatch:s,score:s?0:1,indices:[0,t.length-1]}}})},{type:"inverse-suffix-exact",multiRegex:/^!"(.*)"\$$/,singleRegex:/^!(.*)\$$/,create:e=>({type:"inverse-suffix-exact",search(t){const s=!t.endsWith(e);return{isMatch:s,score:s?0:1,indices:[0,t.length-1]}}})},{type:"suffix-exact",multiRegex:/^"(.*)"\$$/,singleRegex:/^(.*)\$$/,create:e=>({type:"suffix-exact",search(t){const s=t.endsWith(e);return{isMatch:s,score:s?0:1,indices:[t.length-e.length,t.length-1]}}})},{type:"inverse-exact",multiRegex:/^!"(.*)"$/,singleRegex:/^!(.*)$/,create:e=>({type:"inverse-exact",search(t){const s=-1===t.indexOf(e);return{isMatch:s,score:s?0:1,indices:[0,t.length-1]}}})},{type:"fuzzy",multiRegex:/^"(.*)"$/,singleRegex:/^(.*)$/,create:(e,t={})=>{const s=new _(e,{location:t.location??p.location,threshold:t.threshold??p.threshold,distance:t.distance??p.distance,includeMatches:t.includeMatches??p.includeMatches,findAllMatches:t.findAllMatches??p.findAllMatches,minMatchCharLength:t.minMatchCharLength??p.minMatchCharLength,isCaseSensitive:t.isCaseSensitive??p.isCaseSensitive,ignoreDiacritics:t.ignoreDiacritics??p.ignoreDiacritics,ignoreLocation:t.ignoreLocation??p.ignoreLocation});return{type:"fuzzy",search:e=>s.searchIn(e)}}}],I=S.length;function w(e,t){const s=e.match(t);return s?s[1]:null}class b{constructor(e,{isCaseSensitive:t=p.isCaseSensitive,ignoreDiacritics:s=p.ignoreDiacritics,includeMatches:i=p.includeMatches,minMatchCharLength:n=p.minMatchCharLength,ignoreLocation:r=p.ignoreLocation,findAllMatches:o=p.findAllMatches,location:c=p.location,threshold:u=p.threshold,distance:h=p.distance}={}){this.query=null,this.options={isCaseSensitive:t,ignoreDiacritics:s,includeMatches:i,minMatchCharLength:n,findAllMatches:o,ignoreLocation:r,location:c,threshold:u,distance:h},e=t?e:e.toLowerCase(),e=s?D(e):e,this.pattern=e,this.query=function(e,t={}){return e.replace(/\\\|/g,"\0").split("|").map(e=>{const s=function(e){const t=[],s=e.length;let i=0;for(;i=s)break;let n=i;for(;n=s||" "===e[t]){n++;break}if("$"===e[t]&&(t+1>=s||" "===e[t+1])){n+=2;break}}n++}t.push(e.substring(i,n)),i=n}else{for(;ne&&!!e.trim()),i=[];for(let e=0,n=s.length;e!(!e[T]&&!e[z]),W=e=>({[T]:Object.keys(e).map(t=>({[t]:e[t]}))});function H(t,i,{auto:n=!0}={}){const o=t=>{if(s(t)){const e={keyId:null,pattern:t};return n&&(e.searcher=$(t,i)),e}const c=Object.keys(t),u=(e=>!!e[R])(t);if(!u&&c.length>1&&!j(t))return o(W(t));if((t=>!e(t)&&r(t)&&!j(t))(t)){const e=u?t[R]:c[0],r=u?t[O]:t[e];if(!s(r))throw new Error((e=>`Invalid value for key ${e}`)(e));const o={keyId:g(e),pattern:r};return n&&(o.searcher=$(r,i)),o}const h={children:[],operator:c[0]};return c.forEach(s=>{const i=t[s];e(i)&&i.forEach(e=>{h.children.push(o(e))})}),h};return j(t)||(t=W(t)),o(t)}function K(e,{ignoreFieldNorm:t=p.ignoreFieldNorm}){let s=1;return e.forEach(({key:e,norm:i,score:n})=>{const r=e?e.weight:null;s*=Math.pow(0===n&&r?Number.EPSILON:n,(r||1)*(t?1:i))}),s}class q{constructor(e){this.limit=e,this.heap=[]}get size(){return this.heap.length}shouldInsert(e){return this.size0;){const s=e-1>>1;if(t[e].score<=t[s].score)break;const i=t[e];t[e]=t[s],t[s]=i,e=s}}_sinkDown(e){const t=this.heap,s=t.length;let i=e;do{const n=2*(e=i)+1,r=2*e+2;if(nt[i].score&&(i=n),rt[i].score&&(i=r),i!==e){const s=t[e];t[e]=t[i],t[i]=s}}while(i!==e)}}function P(e,t,{includeMatches:s=p.includeMatches,includeScore:i=p.includeScore}={}){return e.map(e=>{const{idx:n}=e,r={item:t[n],refIndex:n};return s&&(r.matches=function(e){const t=[];return e.matches.forEach(e=>{if(!o(e.indices)||!e.indices.length)return;const s={indices:e.indices,value:e.value};e.key&&(s.key=e.key.id),e.idx>-1&&(s.refIndex=e.idx),t.push(s)}),t}(e)),i&&(r.score=e.score),r})}const Q=/[\p{L}\p{M}\p{N}_]+/gu;function U({isCaseSensitive:e=!1,ignoreDiacritics:t=!1,tokenize:s}={}){const i=function(e){return"function"==typeof e?t=>e(t):e instanceof RegExp?(e.global,t=>t.match(e)||[]):e=>e.match(Q)||[]}(s);return{tokenize:s=>(e||(s=s.toLowerCase()),t&&(s=D(s)),i(s))}}class J{static condition(e,t){return t.useTokenSearch}constructor(e,t){this.options=t,this.analyzer=U({isCaseSensitive:t.isCaseSensitive,ignoreDiacritics:t.ignoreDiacritics,tokenize:t.tokenize});const s=this.analyzer.tokenize(e),i=t._invertedIndex,{df:n,fieldCount:r}=i;this.termSearchers=[],this.idfWeights=[];for(const e of s){this.termSearchers.push(new _(e,{location:t.location,threshold:t.threshold,distance:t.distance,includeMatches:t.includeMatches,findAllMatches:t.findAllMatches,minMatchCharLength:t.minMatchCharLength,isCaseSensitive:t.isCaseSensitive,ignoreDiacritics:t.ignoreDiacritics,ignoreLocation:!0}));const s=n.get(e)||0,i=Math.log(1+(r-s+.5)/(s+.5));this.idfWeights.push(i)}this.combineAll="all"===t.tokenMatch,this.numTerms=this.termSearchers.length,this.useMask=this.numTerms<=31}searchIn(e){if(!this.termSearchers.length)return{isMatch:!1,score:1};const t=[];let s=0,i=0,n=0,r=0;const o=this.combineAll&&!this.useMask?new Set:null;for(let c=0;c0?1-s/i:0,u={isMatch:!0,score:Math.max(.001,c)};return this.options.includeMatches&&t.length&&(u.indices=x(t)),this.combineAll&&(this.useMask?u.matchedMask=r:u.matchedTerms=o,u.termCount=this.numTerms),u}}function V(e,t,s,i){const n=i.tokenize(t);if(!n.length)return;e.fieldCount++,e.docFieldCount.set(s,(e.docFieldCount.get(s)||0)+1);const r=new Set(n);let o=e.docTermFieldHits.get(s);o||(o=new Map,e.docTermFieldHits.set(s,o));for(const t of r)o.set(t,(o.get(t)||0)+1),e.df.set(t,(e.df.get(t)||0)+1)}function G(e,t,s,i){const{i:n,v:r,$:o}=t;if(void 0===r){if(o)for(let t=0;te-t);for(const t of s)X(e,t);const i=e=>{let t=0,i=s.length;for(;t>>1;s[n]n?i(t):t,s);e.docFieldCount=r;const o=new Map;for(const[t,s]of e.docTermFieldHits)o.set(t>n?i(t):t,s);e.docTermFieldHits=o}class Z{constructor(e,t,s){this.options={...p,...t},this.options.useExtendedSearch,this.options.useTokenSearch,this._keyStore=new l(this.options.keys),this._docs=e,this._myIndex=null,this._invertedIndex=null,this.setCollection(e,s),this._lastQuery=null,this._lastSearcher=null}_getSearcher(e){if(this._lastQuery===e)return this._lastSearcher;const t=$(e,this._invertedIndex?{...this.options,_invertedIndex:this._invertedIndex}:this.options);return this._lastQuery=e,this._lastSearcher=t,t}setCollection(e,t){if(this._docs=e,t&&!(t instanceof A))throw new Error("Incorrect 'index' type");if(this._myIndex=t||C(this.options.keys,this._docs,{getFn:this.options.getFn,fieldNormWeight:this.options.fieldNormWeight}),this.options.useTokenSearch){const e=U({isCaseSensitive:this.options.isCaseSensitive,ignoreDiacritics:this.options.ignoreDiacritics,tokenize:this.options.tokenize});this._invertedIndex=function(e,t,s){const i={fieldCount:0,df:new Map,docFieldCount:new Map,docTermFieldHits:new Map};for(const n of e)G(i,n,t,s);return i}(this._myIndex.records,this._myIndex.keys.length,e)}this._invalidateSearcherCache()}add(e){if(!o(e))return;this._docs.push(e);const t=this._myIndex.add(e,this._docs.length-1);if(this._invertedIndex&&t){const e=U({isCaseSensitive:this.options.isCaseSensitive,ignoreDiacritics:this.options.ignoreDiacritics,tokenize:this.options.tokenize});!function(e,t,s,i){G(e,t,s,i)}(this._invertedIndex,t,this._myIndex.keys.length,e)}this._invalidateSearcherCache()}remove(e=()=>!1){const t=[],s=[];for(let i=0,n=this._docs.length;i!e.has(s)),this._myIndex.removeAll(s),this._invalidateSearcherCache()}return t}removeAt(e){if(!Number.isInteger(e)||e<0||e>=this._docs.length)throw new Error(h);this._invertedIndex&&Y(this._invertedIndex,[e]);const t=this._docs.splice(e,1)[0];return this._myIndex.removeAt(e),this._invalidateSearcherCache(),t}_invalidateSearcherCache(){this._lastQuery=null,this._lastSearcher=null}getIndex(){return this._myIndex}search(e,t){const{limit:n=-1}=t||{},{includeMatches:r,includeScore:o,shouldSort:c,sortFn:u,ignoreFieldNorm:h}=this.options;if(s(e)&&!e.trim()){let e=this._docs.map((e,t)=>({item:e,refIndex:t}));return i(n)&&n>-1&&(e=e.slice(0,n)),e}let a;if(i(n)&&n>0&&s(e)){const t=new q(n);s(this._docs[0])?this._searchStringList(e,{heap:t,ignoreFieldNorm:h}):this._searchObjectList(e,{heap:t,ignoreFieldNorm:h}),a=t.extractSorted(u)}else a=s(e)?s(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e),function(e,{ignoreFieldNorm:t=p.ignoreFieldNorm}){e.forEach(e=>{e.score=K(e.matches,{ignoreFieldNorm:t})})}(a,{ignoreFieldNorm:h}),c&&a.sort(u),i(n)&&n>-1&&(a=a.slice(0,n));return P(a,this._docs,{includeMatches:r,includeScore:o})}_searchStringList(e,{heap:t,ignoreFieldNorm:s}={}){const i=this._getSearcher(e),n=this.options.useTokenSearch&&"all"===this.options.tokenMatch,{records:r}=this._myIndex,c=t?null:[];return r.forEach(({v:e,i:r,n:u})=>{if(!o(e))return;const h=i.searchIn(e);if(h.isMatch){const i={score:h.score,value:e,norm:u,indices:h.indices};n&&(i.matchedMask=h.matchedMask,i.matchedTerms=h.matchedTerms,i.termCount=h.termCount);const o=[i];if(!n||this._coversAllTokens(o)){const i={item:e,idx:r,matches:o};t?(i.score=K(i.matches,{ignoreFieldNorm:s}),t.shouldInsert(i.score)&&t.insert(i)):c.push(i)}}}),c}_searchLogical(e){const t=H(e,this.options),s=(e,t,i)=>{if(!("children"in e)){const{keyId:s,searcher:n}=e;let r;return null===s?(r=[],this._myIndex.keys.forEach((e,s)=>{r.push(...this._findMatches({key:e,value:t[s],searcher:n}))})):r=this._findMatches({key:this._keyStore.get(s),value:this._myIndex.getValueForItemAtKeyId(t,s),searcher:n}),r&&r.length?[{idx:i,item:t,matches:r}]:[]}const{children:n,operator:r}=e,o=[];for(let e=0,c=n.length;e{if(o(e)){const o=s(t,e,i);o.length&&(n.has(i)||(n.set(i,{idx:i,item:e,matches:[]}),r.push(n.get(i))),o.forEach(({matches:e})=>{n.get(i).matches.push(...e)}))}}),r}_searchObjectList(e,{heap:t,ignoreFieldNorm:s}={}){const i=this._getSearcher(e),n=this.options.useTokenSearch&&"all"===this.options.tokenMatch,{keys:r,records:c}=this._myIndex,u=t?null:[];return c.forEach(({$:e,i:c})=>{if(!o(e))return;const h=[];let a=!1,l=!1;if(r.forEach((t,s)=>{const n=this._findMatches({key:t,value:e[s],searcher:i});n.length?(h.push(...n),n[0].hasInverse&&(l=!0)):a=!0}),(!l||!a)&&h.length&&(!n||this._coversAllTokens(h))){const i={idx:c,item:e,matches:h};t?(i.score=K(i.matches,{ignoreFieldNorm:s}),t.shouldInsert(i.score)&&t.insert(i)):u.push(i)}}),u}_findMatches({key:t,value:s,searcher:i}){if(!o(s))return[];const n=[];if(e(s))s.forEach(({v:e,i:s,n:r})=>{if(!o(e))return;const c=i.searchIn(e);if(c.isMatch){const i={score:c.score,key:t,value:e,idx:s,norm:r,indices:c.indices,hasInverse:c.hasInverse};void 0!==c.termCount&&(i.matchedMask=c.matchedMask,i.matchedTerms=c.matchedTerms,i.termCount=c.termCount),n.push(i)}});else{const{v:e,n:r}=s,o=i.searchIn(e);if(o.isMatch){const s={score:o.score,key:t,value:e,norm:r,indices:o.indices,hasInverse:o.hasInverse};void 0!==o.termCount&&(s.matchedMask=o.matchedMask,s.matchedTerms=o.matchedTerms,s.termCount=o.termCount),n.push(s)}}return n}_coversAllTokens(e){const t=e.length?e[0].termCount:void 0;if(void 0===t)return!0;if(t<=31){let s=0;for(let t=0;tN(e))};export{Z as default}; \ No newline at end of file +function e(e){return Array.isArray?Array.isArray(e):u(e)===`[object Array]`}function t(e){if(typeof e==`string`)return e;if(typeof e==`bigint`)return e.toString();let t=e+``;return t==`0`&&1/e==-1/0?`-0`:t}function n(e){return e==null?``:t(e)}function r(e){return typeof e==`string`}function i(e){return typeof e==`number`}function a(e){return e===!0||e===!1||s(e)&&u(e)==`[object Boolean]`}function o(e){return typeof e==`object`}function s(e){return o(e)&&e!==null}function c(e){return e!=null}function l(e){return!e.trim().length}function u(e){return e==null?e===void 0?`[object Undefined]`:`[object Null]`:Object.prototype.toString.call(e)}const d=`Invalid doc index: must be a non-negative integer within the bounds of the docs array`,f=e=>`Invalid value for key ${e}`,p=e=>`Pattern length exceeds max of ${e}.`,m=e=>`Missing ${e} property in key`,h=e=>`Property 'weight' in key '${e}' must be a positive integer`,g=Object.prototype.hasOwnProperty;var _=class{constructor(e){this._keys=[],this._keyMap={};let t=0;e.forEach(e=>{let n=v(e);this._keys.push(n),this._keyMap[n.id]=n,t+=n.weight}),this._keys.forEach(e=>{e.weight/=t})}get(e){return this._keyMap[e]}keys(){return this._keys}toJSON(){return JSON.stringify(this._keys)}};function v(t){let n=null,i=null,a=null,o=1,s=null;if(r(t)||e(t))a=t,n=y(t),i=b(t);else{if(!g.call(t,`name`))throw Error(m(`name`));let e=t.name;if(a=e,g.call(t,`weight`)&&t.weight!==void 0&&(o=t.weight,o<=0))throw Error(h(b(e)));n=y(e),i=b(e),s=t.getFn??null}return{path:n,id:i,weight:o,src:a,getFn:s}}function y(t){return e(t)?t:t.split(`.`)}function b(t){return e(t)?t.join(`.`):t}function x(t,o){let s=[],l=!1,u=(t,o,d,f)=>{if(c(t))if(!o[d])s.push(f===void 0?t:{v:t,i:f});else{let p=t[o[d]];if(!c(p))return;if(d===o.length-1&&(r(p)||i(p)||a(p)||typeof p==`bigint`))s.push(f===void 0?n(p):{v:n(p),i:f});else if(e(p)){l=!0;for(let e=0,t=p.length;ee.score===t.score?e.idx{this._keysMap[e.id]=t})}create(){if(this.isCreated||!this.docs.length)return;this.isCreated=!0;let e=this.docs.length;this.records=Array(e);let t=0;if(r(this.docs[0]))for(let n=0;ne&&--this.records[t].i}removeAll(e){let t=new Set;for(let n of e)Number.isInteger(n)&&n>=0&&t.add(n);if(t.size===0)return;this.records=this.records.filter(e=>!t.has(e.i));let n=Array.from(t).sort((e,t)=>e-t);for(let e of this.records){let t=0,r=n.length;for(;t>>1;n[i]t),records:this.records}}};function O(e,t,{getFn:n=E.getFn,fieldNormWeight:r=E.fieldNormWeight}={}){let i=new D({getFn:n,fieldNormWeight:r});return i.setKeys(e.map(v)),i.setSources(t),i.create(),i}function te(e,{getFn:t=E.getFn,fieldNormWeight:n=E.fieldNormWeight}={}){let{keys:r,records:i}=e,a=new D({getFn:t,fieldNormWeight:n});return a.setKeys(r),a.setIndexRecords(i),a}function ne(e=[],t=E.minMatchCharLength){let n=[],r=-1,i=-1,a=0;for(let o=e.length;a=t&&n.push([r,i]),r=-1)}return e[a-1]&&a-r>=t&&n.push([r,a-1]),n}function re(e,t,n,{location:r=E.location,distance:i=E.distance,threshold:a=E.threshold,findAllMatches:o=E.findAllMatches,minMatchCharLength:s=E.minMatchCharLength,includeMatches:c=E.includeMatches,ignoreLocation:l=E.ignoreLocation}={}){if(t.length>32)throw Error(p(32));let u=t.length,d=e.length,f=Math.max(0,Math.min(r,d)),m=a,h=f,g=(e,t)=>{let n=e/u;if(l)return n;let r=Math.abs(f-t);return i?n+r/i:r?1:n},_=s>1||c,v=_?Array(d):[],y;for(;(y=e.indexOf(t,h))>-1;){let e=g(0,y);if(m=Math.min(e,m),h=y+u,_){let e=0;for(;e=a;--r){let i=r-1,o=n[e[i]];if(c[r]=(c[r+1]<<1|1)&o,t&&(c[r]|=(b[r+1]|b[r])<<1|1|b[r+1]),c[r]&w&&(x=g(t,i),x<=m)){if(m=x,h=i,S=t,h<=f)break;a=Math.max(1,2*f-h)}}if(g(t+1,f)>m)break;b=c}if(_&&h>=0){let t=Math.min(d-1,h+u-1+S);for(let r=h;r<=t;r+=1)n[e[r]]&&(v[r]=1)}let T={isMatch:h>=0,score:Math.max(.001,x)};if(_){let e=ne(v,s);e.length?c&&(T.indices=e):T.isMatch=!1}return T}function k(e){let t={};for(let n=0,r=e.length;ne[0]-t[0]||e[1]-t[1]);let t=[e[0]];for(let n=1,r=e.length;ne.normalize(`NFD`).replace(/[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]/g,``).replace(ie,e=>j[e]):e=>e;var N=class{constructor(e,{location:t=E.location,threshold:n=E.threshold,distance:r=E.distance,includeMatches:i=E.includeMatches,findAllMatches:a=E.findAllMatches,minMatchCharLength:o=E.minMatchCharLength,isCaseSensitive:s=E.isCaseSensitive,ignoreDiacritics:c=E.ignoreDiacritics,ignoreLocation:l=E.ignoreLocation}={}){if(this.options={location:t,threshold:n,distance:r,includeMatches:i,findAllMatches:a,minMatchCharLength:o,isCaseSensitive:s,ignoreDiacritics:c,ignoreLocation:l},e=s?e:e.toLowerCase(),e=c?M(e):e,this.pattern=e,this.chunks=[],!this.pattern.length)return;let u=(e,t)=>{this.chunks.push({pattern:e,alphabet:k(e),startIndex:t})},d=this.pattern.length;if(d>32){let e=0,t=d%32,n=d-t;for(;e{let{isMatch:m,score:h,indices:g}=re(e,t,n,{location:i+p,distance:a,threshold:o,findAllMatches:s,minMatchCharLength:c,includeMatches:r,ignoreLocation:l});m&&(f=!0),d+=h,m&&g&&u.push(...g)});let p={isMatch:f,score:f?d/this.chunks.length:1};return f&&r&&(p.indices=A(u)),p}};const P=new Set([`fuzzy`,`include`]);function F(e){return e.startsWith(`inverse`)}const I=[{type:`exact`,multiRegex:/^="(.*)"$/,singleRegex:/^=(.*)$/,create:e=>({type:`exact`,search(t){let n=t===e;return{isMatch:n,score:+!n,indices:[0,e.length-1]}}})},{type:`include`,multiRegex:/^'"(.*)"$/,singleRegex:/^'(.*)$/,create:e=>({type:`include`,search(t){let n=0,r,i=[],a=e.length;for(;(r=t.indexOf(e,n))>-1;)n=r+a,i.push([r,n-1]);let o=!!i.length;return{isMatch:o,score:+!o,indices:i}}})},{type:`prefix-exact`,multiRegex:/^\^"(.*)"$/,singleRegex:/^\^(.*)$/,create:e=>({type:`prefix-exact`,search(t){let n=t.startsWith(e);return{isMatch:n,score:+!n,indices:[0,e.length-1]}}})},{type:`inverse-prefix-exact`,multiRegex:/^!\^"(.*)"$/,singleRegex:/^!\^(.*)$/,create:e=>({type:`inverse-prefix-exact`,search(t){let n=!t.startsWith(e);return{isMatch:n,score:+!n,indices:[0,t.length-1]}}})},{type:`inverse-suffix-exact`,multiRegex:/^!"(.*)"\$$/,singleRegex:/^!(.*)\$$/,create:e=>({type:`inverse-suffix-exact`,search(t){let n=!t.endsWith(e);return{isMatch:n,score:+!n,indices:[0,t.length-1]}}})},{type:`suffix-exact`,multiRegex:/^"(.*)"\$$/,singleRegex:/^(.*)\$$/,create:e=>({type:`suffix-exact`,search(t){let n=t.endsWith(e);return{isMatch:n,score:+!n,indices:[t.length-e.length,t.length-1]}}})},{type:`inverse-exact`,multiRegex:/^!"(.*)"$/,singleRegex:/^!(.*)$/,create:e=>({type:`inverse-exact`,search(t){let n=t.indexOf(e)===-1;return{isMatch:n,score:+!n,indices:[0,t.length-1]}}})},{type:`fuzzy`,multiRegex:/^"(.*)"$/,singleRegex:/^(.*)$/,create:(e,t={})=>{let n=new N(e,{location:t.location??E.location,threshold:t.threshold??E.threshold,distance:t.distance??E.distance,includeMatches:t.includeMatches??E.includeMatches,findAllMatches:t.findAllMatches??E.findAllMatches,minMatchCharLength:t.minMatchCharLength??E.minMatchCharLength,isCaseSensitive:t.isCaseSensitive??E.isCaseSensitive,ignoreDiacritics:t.ignoreDiacritics??E.ignoreDiacritics,ignoreLocation:t.ignoreLocation??E.ignoreLocation});return{type:`fuzzy`,search(e){return n.searchIn(e)}}}}],L=I.length;function ae(e){let t=[],n=e.length,r=0;for(;r=n)break;let i=r;for(;i=n||e[t]===` `){i++;break}if(e[t]===`$`&&(t+1>=n||e[t+1]===` `)){i+=2;break}}i++}t.push(e.substring(r,i)),r=i}else{for(;i{let n=ae(e.replace(/\u0000/g,`|`).trim()).filter(e=>e&&!!e.trim()),r=[];for(let e=0,i=n.length;e!!(e[H.AND]||e[H.OR]),G=e=>!!e[U.PATH],ce=t=>!e(t)&&o(t)&&!W(t),K=e=>({[H.AND]:Object.keys(e).map(t=>({[t]:e[t]}))});function le(t,n,{auto:i=!0}={}){let a=t=>{if(r(t)){let e={keyId:null,pattern:t};return i&&(e.searcher=V(t,n)),e}let o=Object.keys(t),s=G(t);if(!s&&o.length>1&&!W(t))return a(K(t));if(ce(t)){let e=s?t[U.PATH]:o[0],a=s?t[U.PATTERN]:t[e];if(!r(a))throw Error(f(e));let c={keyId:b(e),pattern:a};return i&&(c.searcher=V(a,n)),c}let c={children:[],operator:o[0]};return o.forEach(n=>{let r=t[n];e(r)&&r.forEach(e=>{c.children.push(a(e))})}),c};return W(t)||(t=K(t)),a(t)}function q(e,{ignoreFieldNorm:t=E.ignoreFieldNorm}){let n=1;return e.forEach(({key:e,norm:r,score:i})=>{let a=e?e.weight:null;n*=(i===0&&a?2**-52:i)**+((a||1)*(t?1:r))}),n}function ue(e,{ignoreFieldNorm:t=E.ignoreFieldNorm}){e.forEach(e=>{e.score=q(e.matches,{ignoreFieldNorm:t})})}var de=class{constructor(e){this.limit=e,this.heap=[]}get size(){return this.heap.length}shouldInsert(e){return this.size0;){let n=e-1>>1;if(t[e].score<=t[n].score)break;let r=t[e];t[e]=t[n],t[n]=r,e=n}}_sinkDown(e){let t=this.heap,n=t.length,r=e;do{e=r;let i=2*e+1,a=2*e+2;if(it[r].score&&(r=i),at[r].score&&(r=a),r!==e){let n=t[e];t[e]=t[r],t[r]=n}}while(r!==e)}};function fe(e){let t=[];return e.matches.forEach(e=>{if(!c(e.indices)||!e.indices.length)return;let n={indices:e.indices,value:e.value};e.key&&(n.key=e.key.id),e.idx>-1&&(n.refIndex=e.idx),t.push(n)}),t}function pe(e,t,{includeMatches:n=E.includeMatches,includeScore:r=E.includeScore}={}){return e.map(e=>{let{idx:i}=e,a={item:t[i],refIndex:i};return n&&(a.matches=fe(e)),r&&(a.score=e.score),a})}const me=/[\p{L}\p{M}\p{N}_]+/gu;function he(e){return typeof e==`function`?t=>e(t):e instanceof RegExp?(e.global,t=>t.match(e)||[]):e=>e.match(me)||[]}function J({isCaseSensitive:e=!1,ignoreDiacritics:t=!1,tokenize:n}={}){let r=he(n);return{tokenize(n){return e||(n=n.toLowerCase()),t&&(n=M(n)),r(n)}}}var ge=class{static condition(e,t){return t.useTokenSearch}constructor(e,t){this.options=t,this.analyzer=J({isCaseSensitive:t.isCaseSensitive,ignoreDiacritics:t.ignoreDiacritics,tokenize:t.tokenize});let n=this.analyzer.tokenize(e),{df:r,fieldCount:i}=t._invertedIndex;this.termSearchers=[],this.idfWeights=[];for(let e of n){this.termSearchers.push(new N(e,{location:t.location,threshold:t.threshold,distance:t.distance,includeMatches:t.includeMatches,findAllMatches:t.findAllMatches,minMatchCharLength:t.minMatchCharLength,isCaseSensitive:t.isCaseSensitive,ignoreDiacritics:t.ignoreDiacritics,ignoreLocation:!0}));let n=r.get(e)||0,a=Math.log(1+(i-n+.5)/(n+.5));this.idfWeights.push(a)}this.combineAll=t.tokenMatch===`all`,this.numTerms=this.termSearchers.length,this.useMask=this.numTerms<=31}searchIn(e){if(!this.termSearchers.length)return{isMatch:!1,score:1};let t=[],n=0,r=0,i=0,a=0,o=this.combineAll&&!this.useMask?new Set:null;for(let s=0;s0?1-n/r:0,c={isMatch:!0,score:Math.max(.001,s)};return this.options.includeMatches&&t.length&&(c.indices=A(t)),this.combineAll&&(this.useMask?c.matchedMask=a:c.matchedTerms=o,c.termCount=this.numTerms),c}};function Y(e,t,n,r){let i=r.tokenize(t);if(!i.length)return;e.fieldCount++,e.docFieldCount.set(n,(e.docFieldCount.get(n)||0)+1);let a=new Set(i),o=e.docTermFieldHits.get(n);o||(o=new Map,e.docTermFieldHits.set(n,o));for(let t of a)o.set(t,(o.get(t)||0)+1),e.df.set(t,(e.df.get(t)||0)+1)}function X(e,t,n,r){let{i,v:a,$:o}=t;if(a!==void 0){Y(e,a,i,r);return}if(o)for(let t=0;te-t);for(let t of n)Z(e,t);let r=e=>{let t=0,r=n.length;for(;t>>1;n[i]i?r(t):t,n);e.docFieldCount=a;let o=new Map;for(let[t,n]of e.docTermFieldHits)o.set(t>i?r(t):t,n);e.docTermFieldHits=o}var $=class{constructor(e,t,n){this.options={...E,...t},this.options.useExtendedSearch,this.options.useTokenSearch,this._keyStore=new _(this.options.keys),this._docs=e,this._myIndex=null,this._invertedIndex=null,this.setCollection(e,n),this._lastQuery=null,this._lastSearcher=null}_getSearcher(e){if(this._lastQuery===e)return this._lastSearcher;let t=V(e,this._invertedIndex?{...this.options,_invertedIndex:this._invertedIndex}:this.options);return this._lastQuery=e,this._lastSearcher=t,t}setCollection(e,t){if(this._docs=e,t&&!(t instanceof D))throw Error(`Incorrect 'index' type`);if(this._myIndex=t||O(this.options.keys,this._docs,{getFn:this.options.getFn,fieldNormWeight:this.options.fieldNormWeight}),this.options.useTokenSearch){let e=J({isCaseSensitive:this.options.isCaseSensitive,ignoreDiacritics:this.options.ignoreDiacritics,tokenize:this.options.tokenize});this._invertedIndex=_e(this._myIndex.records,this._myIndex.keys.length,e)}this._invalidateSearcherCache()}add(e){if(!c(e))return;this._docs.push(e);let t=this._myIndex.add(e,this._docs.length-1);if(this._invertedIndex&&t){let e=J({isCaseSensitive:this.options.isCaseSensitive,ignoreDiacritics:this.options.ignoreDiacritics,tokenize:this.options.tokenize});ve(this._invertedIndex,t,this._myIndex.keys.length,e)}this._invalidateSearcherCache()}remove(e=()=>!1){let t=[],n=[];for(let r=0,i=this._docs.length;r!e.has(n)),this._myIndex.removeAll(n),this._invalidateSearcherCache()}return t}removeAt(e){if(!Number.isInteger(e)||e<0||e>=this._docs.length)throw Error(d);this._invertedIndex&&Q(this._invertedIndex,[e]);let t=this._docs.splice(e,1)[0];return this._myIndex.removeAt(e),this._invalidateSearcherCache(),t}_invalidateSearcherCache(){this._lastQuery=null,this._lastSearcher=null}getIndex(){return this._myIndex}search(e,t){let{limit:n=-1}=t||{},{includeMatches:a,includeScore:o,shouldSort:s,sortFn:c,ignoreFieldNorm:l}=this.options;if(r(e)&&!e.trim()){let e=this._docs.map((e,t)=>({item:e,refIndex:t}));return i(n)&&n>-1&&(e=e.slice(0,n)),e}let u=i(n)&&n>0&&r(e),d;if(u){let t=new de(n);r(this._docs[0])?this._searchStringList(e,{heap:t,ignoreFieldNorm:l}):this._searchObjectList(e,{heap:t,ignoreFieldNorm:l}),d=t.extractSorted(c)}else d=r(e)?r(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e),ue(d,{ignoreFieldNorm:l}),s&&d.sort(c),i(n)&&n>-1&&(d=d.slice(0,n));return pe(d,this._docs,{includeMatches:a,includeScore:o})}_searchStringList(e,{heap:t,ignoreFieldNorm:n}={}){let r=this._getSearcher(e),i=this.options.useTokenSearch&&this.options.tokenMatch===`all`,{records:a}=this._myIndex,o=t?null:[];return a.forEach(({v:e,i:a,n:s})=>{if(!c(e))return;let l=r.searchIn(e);if(l.isMatch){let r={score:l.score,value:e,norm:s,indices:l.indices};i&&(r.matchedMask=l.matchedMask,r.matchedTerms=l.matchedTerms,r.termCount=l.termCount);let c=[r];if(!i||this._coversAllTokens(c)){let r={item:e,idx:a,matches:c};t?(r.score=q(r.matches,{ignoreFieldNorm:n}),t.shouldInsert(r.score)&&t.insert(r)):o.push(r)}}}),o}_searchLogical(e){let t=le(e,this.options),n=(e,t,r)=>{if(!(`children`in e)){let{keyId:n,searcher:i}=e,a;return n===null?(a=[],this._myIndex.keys.forEach((e,n)=>{a.push(...this._findMatches({key:e,value:t[n],searcher:i}))})):a=this._findMatches({key:this._keyStore.get(n),value:this._myIndex.getValueForItemAtKeyId(t,n),searcher:i}),a&&a.length?[{idx:r,item:t,matches:a}]:[]}let{children:i,operator:a}=e,o=[];for(let e=0,s=i.length;e{if(c(e)){let o=n(t,e,r);o.length&&(i.has(r)||(i.set(r,{idx:r,item:e,matches:[]}),a.push(i.get(r))),o.forEach(({matches:e})=>{i.get(r).matches.push(...e)}))}}),a}_searchObjectList(e,{heap:t,ignoreFieldNorm:n}={}){let r=this._getSearcher(e),i=this.options.useTokenSearch&&this.options.tokenMatch===`all`,{keys:a,records:o}=this._myIndex,s=t?null:[];return o.forEach(({$:e,i:o})=>{if(!c(e))return;let l=[],u=!1,d=!1;if(a.forEach((t,n)=>{let i=this._findMatches({key:t,value:e[n],searcher:r});i.length?(l.push(...i),i[0].hasInverse&&(d=!0)):u=!0}),!(d&&u)&&l.length&&(!i||this._coversAllTokens(l))){let r={idx:o,item:e,matches:l};t?(r.score=q(r.matches,{ignoreFieldNorm:n}),t.shouldInsert(r.score)&&t.insert(r)):s.push(r)}}),s}_findMatches({key:t,value:n,searcher:r}){if(!c(n))return[];let i=[];if(e(n))n.forEach(({v:e,i:n,n:a})=>{if(!c(e))return;let o=r.searchIn(e);if(o.isMatch){let r={score:o.score,key:t,value:e,idx:n,norm:a,indices:o.indices,hasInverse:o.hasInverse};o.termCount!==void 0&&(r.matchedMask=o.matchedMask,r.matchedTerms=o.matchedTerms,r.termCount=o.termCount),i.push(r)}});else{let{v:e,n:a}=n,o=r.searchIn(e);if(o.isMatch){let n={score:o.score,key:t,value:e,norm:a,indices:o.indices,hasInverse:o.hasInverse};o.termCount!==void 0&&(n.matchedMask=o.matchedMask,n.matchedTerms=o.matchedTerms,n.termCount=o.termCount),i.push(n)}}return i}_coversAllTokens(e){let t=e.length?e[0].termCount:void 0;if(t===void 0)return!0;if(t<=31){let n=0;for(let t=0;tB(e))};var ye=$;export{ye as default}; \ No newline at end of file diff --git a/dist/fuse.mjs b/dist/fuse.mjs index 9298b5744..5c4ed8e77 100644 --- a/dist/fuse.mjs +++ b/dist/fuse.mjs @@ -6,2254 +6,1618 @@ * * http://www.apache.org/licenses/LICENSE-2.0 */ - +//#region src/helpers/typeGuards.ts function isArray(value) { - return !Array.isArray ? getTag(value) === '[object Array]' : Array.isArray(value); + return !Array.isArray ? getTag(value) === "[object Array]" : Array.isArray(value); } function baseToString(value) { - // Exit early for strings to avoid a performance hit in some environments. - if (typeof value == 'string') { - return value; - } - if (typeof value === 'bigint') { - return value.toString(); - } - const result = value + ''; - return result == '0' && 1 / value == -Infinity ? '-0' : result; + if (typeof value == "string") return value; + if (typeof value === "bigint") return value.toString(); + const result = value + ""; + return result == "0" && 1 / value == -Infinity ? "-0" : result; } function toString(value) { - return value == null ? '' : baseToString(value); + return value == null ? "" : baseToString(value); } function isString(value) { - return typeof value === 'string'; + return typeof value === "string"; } function isNumber(value) { - return typeof value === 'number'; + return typeof value === "number"; } - -// Adapted from: https://github.com/lodash/lodash/blob/master/isBoolean.js function isBoolean(value) { - return value === true || value === false || isObjectLike(value) && getTag(value) == '[object Boolean]'; + return value === true || value === false || isObjectLike(value) && getTag(value) == "[object Boolean]"; } function isObject(value) { - return typeof value === 'object'; + return typeof value === "object"; } - -// Checks if `value` is object-like. function isObjectLike(value) { - return isObject(value) && value !== null; + return isObject(value) && value !== null; } function isDefined(value) { - return value !== undefined && value !== null; + return value !== void 0 && value !== null; } function isBlank(value) { - return !value.trim().length; + return !value.trim().length; } - -// Gets the `toStringTag` of `value`. -// Adapted from: https://github.com/lodash/lodash/blob/master/.internal/getTag.js function getTag(value) { - return value == null ? value === undefined ? '[object Undefined]' : '[object Null]' : Object.prototype.toString.call(value); + return value == null ? value === void 0 ? "[object Undefined]" : "[object Null]" : Object.prototype.toString.call(value); } +//#endregion +//#region src/core/errorMessages.ts const INCORRECT_INDEX_TYPE = "Incorrect 'index' type"; -const INVALID_DOC_INDEX = 'Invalid doc index: must be a non-negative integer within the bounds of the docs array'; -const LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY = key => `Invalid value for key ${key}`; -const PATTERN_LENGTH_TOO_LARGE = max => `Pattern length exceeds max of ${max}.`; -const MISSING_KEY_PROPERTY = name => `Missing ${name} property in key`; -const INVALID_KEY_WEIGHT_VALUE = key => `Property 'weight' in key '${key}' must be a positive integer`; -const FUSE_MATCH_TOKEN_SEARCH_UNSUPPORTED = `Fuse.match does not support useTokenSearch: token search requires ` + `corpus-level statistics (df, fieldCount) that a one-off string ` + `comparison does not have. Use new Fuse(...).search(...) instead.`; - +const INVALID_DOC_INDEX = "Invalid doc index: must be a non-negative integer within the bounds of the docs array"; +const LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY = (key) => `Invalid value for key ${key}`; +const PATTERN_LENGTH_TOO_LARGE = (max) => `Pattern length exceeds max of ${max}.`; +const MISSING_KEY_PROPERTY = (name) => `Missing ${name} property in key`; +const INVALID_KEY_WEIGHT_VALUE = (key) => `Property 'weight' in key '${key}' must be a positive integer`; +const FUSE_MATCH_TOKEN_SEARCH_UNSUPPORTED = "Fuse.match does not support useTokenSearch: token search requires corpus-level statistics (df, fieldCount) that a one-off string comparison does not have. Use new Fuse(...).search(...) instead."; + +//#endregion +//#region src/tools/KeyStore.ts const hasOwn = Object.prototype.hasOwnProperty; -class KeyStore { - constructor(keys) { - this._keys = []; - this._keyMap = {}; - let totalWeight = 0; - keys.forEach(key => { - const obj = createKey(key); - this._keys.push(obj); - this._keyMap[obj.id] = obj; - totalWeight += obj.weight; - }); - - // Normalize weights so that their sum is equal to 1 - this._keys.forEach(key => { - key.weight /= totalWeight; - }); - } - get(keyId) { - return this._keyMap[keyId]; - } - keys() { - return this._keys; - } - toJSON() { - return JSON.stringify(this._keys); - } -} +var KeyStore = class { + constructor(keys) { + this._keys = []; + this._keyMap = {}; + let totalWeight = 0; + keys.forEach((key) => { + const obj = createKey(key); + this._keys.push(obj); + this._keyMap[obj.id] = obj; + totalWeight += obj.weight; + }); + this._keys.forEach((key) => { + key.weight /= totalWeight; + }); + } + get(keyId) { + return this._keyMap[keyId]; + } + keys() { + return this._keys; + } + toJSON() { + return JSON.stringify(this._keys); + } +}; function createKey(key) { - let path = null; - let id = null; - let src = null; - let weight = 1; - let getFn = null; - if (isString(key) || isArray(key)) { - src = key; - path = createKeyPath(key); - id = createKeyId(key); - } else { - if (!hasOwn.call(key, 'name')) { - throw new Error(MISSING_KEY_PROPERTY('name')); - } - const name = key.name; - src = name; - if (hasOwn.call(key, 'weight') && key.weight !== undefined) { - weight = key.weight; - if (weight <= 0) { - throw new Error(INVALID_KEY_WEIGHT_VALUE(createKeyId(name))); - } - } - path = createKeyPath(name); - id = createKeyId(name); - getFn = key.getFn ?? null; - } - return { - path: path, - id: id, - weight, - src: src, - getFn - }; + let path = null; + let id = null; + let src = null; + let weight = 1; + let getFn = null; + if (isString(key) || isArray(key)) { + src = key; + path = createKeyPath(key); + id = createKeyId(key); + } else { + if (!hasOwn.call(key, "name")) throw new Error(MISSING_KEY_PROPERTY("name")); + const name = key.name; + src = name; + if (hasOwn.call(key, "weight") && key.weight !== void 0) { + weight = key.weight; + if (weight <= 0) throw new Error(INVALID_KEY_WEIGHT_VALUE(createKeyId(name))); + } + path = createKeyPath(name); + id = createKeyId(name); + getFn = key.getFn ?? null; + } + return { + path, + id, + weight, + src, + getFn + }; } function createKeyPath(key) { - return isArray(key) ? key : key.split('.'); + return isArray(key) ? key : key.split("."); } function createKeyId(key) { - return isArray(key) ? key.join('.') : key; + return isArray(key) ? key.join(".") : key; } +//#endregion +//#region src/helpers/get.ts function get(obj, path) { - const list = []; - let arr = false; - const deepGet = (obj, path, index, arrayIndex) => { - if (!isDefined(obj)) { - return; - } - if (!path[index]) { - // If there's no path left, we've arrived at the object we care about. - list.push(arrayIndex !== undefined ? { - v: obj, - i: arrayIndex - } : obj); - } else { - const key = path[index]; - const value = obj[key]; - if (!isDefined(value)) { - return; - } - - // If we're at the last value in the path, and if it's a string/number/bool, - // add it to the list - if (index === path.length - 1 && (isString(value) || isNumber(value) || isBoolean(value) || typeof value === 'bigint')) { - list.push(arrayIndex !== undefined ? { - v: toString(value), - i: arrayIndex - } : toString(value)); - } else if (isArray(value)) { - arr = true; - // Search each item in the array. - for (let i = 0, len = value.length; i < len; i += 1) { - deepGet(value[i], path, index + 1, i); - } - } else if (path.length) { - // An object. Recurse further. - deepGet(value, path, index + 1, arrayIndex); - } - } - }; - - // Backwards compatibility (since path used to be a string) - deepGet(obj, isString(path) ? path.split('.') : path, 0); - return arr ? list : list[0]; + const list = []; + let arr = false; + const deepGet = (obj, path, index, arrayIndex) => { + if (!isDefined(obj)) return; + if (!path[index]) list.push(arrayIndex !== void 0 ? { + v: obj, + i: arrayIndex + } : obj); + else { + const value = obj[path[index]]; + if (!isDefined(value)) return; + if (index === path.length - 1 && (isString(value) || isNumber(value) || isBoolean(value) || typeof value === "bigint")) list.push(arrayIndex !== void 0 ? { + v: toString(value), + i: arrayIndex + } : toString(value)); + else if (isArray(value)) { + arr = true; + for (let i = 0, len = value.length; i < len; i += 1) deepGet(value[i], path, index + 1, i); + } else if (path.length) deepGet(value, path, index + 1, arrayIndex); + } + }; + deepGet(obj, isString(path) ? path.split(".") : path, 0); + return arr ? list : list[0]; } +//#endregion +//#region src/core/config.ts const MatchOptions = { - includeMatches: false, - findAllMatches: false, - minMatchCharLength: 1 + includeMatches: false, + findAllMatches: false, + minMatchCharLength: 1 }; const BasicOptions = { - isCaseSensitive: false, - ignoreDiacritics: false, - includeScore: false, - keys: [], - shouldSort: true, - sortFn: (a, b) => a.score === b.score ? a.idx < b.idx ? -1 : 1 : a.score < b.score ? -1 : 1 + isCaseSensitive: false, + ignoreDiacritics: false, + includeScore: false, + keys: [], + shouldSort: true, + sortFn: (a, b) => a.score === b.score ? a.idx < b.idx ? -1 : 1 : a.score < b.score ? -1 : 1 }; const FuzzyOptions = { - location: 0, - threshold: 0.6, - distance: 100 + location: 0, + threshold: .6, + distance: 100 }; const AdvancedOptions = { - useExtendedSearch: false, - useTokenSearch: false, - tokenize: undefined, - tokenMatch: 'any', - getFn: get, - ignoreLocation: false, - ignoreFieldNorm: false, - fieldNormWeight: 1 + useExtendedSearch: false, + useTokenSearch: false, + tokenize: void 0, + tokenMatch: "any", + getFn: get, + ignoreLocation: false, + ignoreFieldNorm: false, + fieldNormWeight: 1 }; const Config = Object.freeze({ - ...BasicOptions, - ...MatchOptions, - ...FuzzyOptions, - ...AdvancedOptions + ...BasicOptions, + ...MatchOptions, + ...FuzzyOptions, + ...AdvancedOptions }); -// Field-length norm: the shorter the field, the higher the weight. -// Set to 3 decimals to reduce index size. +//#endregion +//#region src/tools/fieldNorm.ts function norm(weight = 1, mantissa = 3) { - const cache = new Map(); - const m = Math.pow(10, mantissa); - return { - get(value) { - // Count words by counting space transitions — avoids allocating a regex match array - let numTokens = 1; - let inSpace = false; - for (let i = 0; i < value.length; i++) { - if (value.charCodeAt(i) === 32) { - if (!inSpace) { - numTokens++; - inSpace = true; - } - } else { - inSpace = false; - } - } - if (cache.has(numTokens)) { - return cache.get(numTokens); - } - - // Default function is 1/sqrt(x), weight makes that variable - const n = Math.round(m / Math.pow(numTokens, 0.5 * weight)) / m; - cache.set(numTokens, n); - return n; - }, - clear() { - cache.clear(); - } - }; + const cache = /* @__PURE__ */ new Map(); + const m = Math.pow(10, mantissa); + return { + get(value) { + let numTokens = 1; + let inSpace = false; + for (let i = 0; i < value.length; i++) if (value.charCodeAt(i) === 32) { + if (!inSpace) { + numTokens++; + inSpace = true; + } + } else inSpace = false; + if (cache.has(numTokens)) return cache.get(numTokens); + const n = Math.round(m / Math.pow(numTokens, .5 * weight)) / m; + cache.set(numTokens, n); + return n; + }, + clear() { + cache.clear(); + } + }; } -class FuseIndex { - constructor({ - getFn = Config.getFn, - fieldNormWeight = Config.fieldNormWeight - } = {}) { - this.norm = norm(fieldNormWeight, 3); - this.getFn = getFn; - this.isCreated = false; - this.docs = []; - this.keys = []; - this._keysMap = {}; - this.setIndexRecords(); - } - setSources(docs = []) { - this.docs = docs; - } - setIndexRecords(records = []) { - this.records = records; - } - setKeys(keys = []) { - this.keys = keys; - this._keysMap = {}; - keys.forEach((key, idx) => { - this._keysMap[key.id] = idx; - }); - } - create() { - if (this.isCreated || !this.docs.length) { - return; - } - this.isCreated = true; - const len = this.docs.length; - this.records = new Array(len); - let recordCount = 0; - - // List is Array - if (isString(this.docs[0])) { - for (let i = 0; i < len; i++) { - const record = this._createStringRecord(this.docs[i], i); - if (record) { - this.records[recordCount++] = record; - } - } - } else { - // List is Array - for (let i = 0; i < len; i++) { - this.records[recordCount++] = this._createObjectRecord(this.docs[i], i); - } - } - this.records.length = recordCount; - this.norm.clear(); - } - // Appends a record for `doc` at `docIndex` (the doc's position in the source - // array). Returns the appended record, or null when `doc` is a blank string - // (those are skipped at record creation; see `_createStringRecord`). Callers - // use the return value to gate downstream bookkeeping like the inverted - // index, which must not be touched when no record was produced. - add(doc, docIndex) { - if (!Number.isInteger(docIndex) || docIndex < 0) { - throw new Error(INVALID_DOC_INDEX); - } - if (isString(doc)) { - const record = this._createStringRecord(doc, docIndex); - if (record) { - this.records.push(record); - } - return record; - } - const record = this._createObjectRecord(doc, docIndex); - this.records.push(record); - return record; - } - // Removes the record for the doc at the specified source-array (docs) index. - // Blank string docs have no record; callers may pass such an index and the - // splice is a no-op, but subsequent records still need their .i decremented - // to track the docs array that the caller is splicing in parallel. - removeAt(idx) { - if (!Number.isInteger(idx) || idx < 0) { - throw new Error(INVALID_DOC_INDEX); - } - - // Find and remove the record at this doc-index, if one exists. Records are - // typically sorted by .i but the algorithm doesn't depend on it — parsed - // indexes via setIndexRecords may arrive in arbitrary order. - for (let i = 0, len = this.records.length; i < len; i += 1) { - if (this.records[i].i === idx) { - this.records.splice(i, 1); - break; - } - } - - // Decrement every record whose source-array index is now stale. - for (let i = 0, len = this.records.length; i < len; i += 1) { - if (this.records[i].i > idx) { - this.records[i].i -= 1; - } - } - } - // Removes records for the docs at the specified source-array indices, then - // shifts every surviving record's .i down by the count of removed indices - // strictly less than it (mirrors removeAndShiftInvertedIndex's shift math). - // Invalid entries (non-integer, negative) in `indices` are dropped silently - // — removeAll's natural use case is "caller passed a list of matched doc - // indices"; asymmetric throw-vs-no-op would be more surprising than a clean - // filter. - removeAll(indices) { - const toRemove = new Set(); - for (const v of indices) { - if (Number.isInteger(v) && v >= 0) { - toRemove.add(v); - } - } - if (toRemove.size === 0) { - return; - } - this.records = this.records.filter(r => !toRemove.has(r.i)); - const sorted = Array.from(toRemove).sort((a, b) => a - b); - for (const record of this.records) { - // shift = count of removed indices strictly less than record.i - let lo = 0; - let hi = sorted.length; - while (lo < hi) { - const mid = lo + hi >>> 1; - if (sorted[mid] < record.i) lo = mid + 1;else hi = mid; - } - record.i -= lo; - } - } - getValueForItemAtKeyId(item, keyId) { - return item[this._keysMap[keyId]]; - } - size() { - return this.records.length; - } - _createStringRecord(doc, docIndex) { - if (!isDefined(doc) || isBlank(doc)) { - return null; - } - return { - v: doc, - i: docIndex, - n: this.norm.get(doc) - }; - } - _createObjectRecord(doc, docIndex) { - const record = { - i: docIndex, - $: {} - }; - - // Iterate over every key (i.e, path), and fetch the value at that key - for (let keyIndex = 0, keyLen = this.keys.length; keyIndex < keyLen; keyIndex++) { - const key = this.keys[keyIndex]; - const value = key.getFn ? key.getFn(doc) : this.getFn(doc, key.path); - if (!isDefined(value)) { - continue; - } - if (isArray(value)) { - const subRecords = []; - for (let i = 0, len = value.length; i < len; i += 1) { - const item = value[i]; - if (!isDefined(item)) { - continue; - } - if (isString(item)) { - // Custom getFn returning plain string array (backward compat) - if (!isBlank(item)) { - const subRecord = { - v: item, - i: i, - n: this.norm.get(item) - }; - subRecords.push(subRecord); - } - } else if (isDefined(item.v)) { - // Default get() returns {v, i} objects with original array indices - const text = isString(item.v) ? item.v : toString(item.v); - if (!isBlank(text)) { - const subRecord = { - v: text, - i: item.i, - n: this.norm.get(text) - }; - subRecords.push(subRecord); - } - } - } - record.$[keyIndex] = subRecords; - } else if (isString(value) && !isBlank(value)) { - const subRecord = { - v: value, - n: this.norm.get(value) - }; - record.$[keyIndex] = subRecord; - } - } - return record; - } - toJSON() { - return { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - keys: this.keys.map(({ - getFn, - ...key - }) => key), - records: this.records - }; - } -} -function createIndex(keys, docs, { - getFn = Config.getFn, - fieldNormWeight = Config.fieldNormWeight -} = {}) { - const myIndex = new FuseIndex({ - getFn, - fieldNormWeight - }); - myIndex.setKeys(keys.map(createKey)); - myIndex.setSources(docs); - myIndex.create(); - return myIndex; +//#endregion +//#region src/tools/FuseIndex.ts +var FuseIndex = class { + constructor({ getFn = Config.getFn, fieldNormWeight = Config.fieldNormWeight } = {}) { + this.norm = norm(fieldNormWeight, 3); + this.getFn = getFn; + this.isCreated = false; + this.docs = []; + this.keys = []; + this._keysMap = {}; + this.setIndexRecords(); + } + setSources(docs = []) { + this.docs = docs; + } + setIndexRecords(records = []) { + this.records = records; + } + setKeys(keys = []) { + this.keys = keys; + this._keysMap = {}; + keys.forEach((key, idx) => { + this._keysMap[key.id] = idx; + }); + } + create() { + if (this.isCreated || !this.docs.length) return; + this.isCreated = true; + const len = this.docs.length; + this.records = new Array(len); + let recordCount = 0; + if (isString(this.docs[0])) for (let i = 0; i < len; i++) { + const record = this._createStringRecord(this.docs[i], i); + if (record) this.records[recordCount++] = record; + } + else for (let i = 0; i < len; i++) this.records[recordCount++] = this._createObjectRecord(this.docs[i], i); + this.records.length = recordCount; + this.norm.clear(); + } + add(doc, docIndex) { + if (!Number.isInteger(docIndex) || docIndex < 0) throw new Error(INVALID_DOC_INDEX); + if (isString(doc)) { + const record = this._createStringRecord(doc, docIndex); + if (record) this.records.push(record); + return record; + } + const record = this._createObjectRecord(doc, docIndex); + this.records.push(record); + return record; + } + removeAt(idx) { + if (!Number.isInteger(idx) || idx < 0) throw new Error(INVALID_DOC_INDEX); + for (let i = 0, len = this.records.length; i < len; i += 1) if (this.records[i].i === idx) { + this.records.splice(i, 1); + break; + } + for (let i = 0, len = this.records.length; i < len; i += 1) if (this.records[i].i > idx) this.records[i].i -= 1; + } + removeAll(indices) { + const toRemove = /* @__PURE__ */ new Set(); + for (const v of indices) if (Number.isInteger(v) && v >= 0) toRemove.add(v); + if (toRemove.size === 0) return; + this.records = this.records.filter((r) => !toRemove.has(r.i)); + const sorted = Array.from(toRemove).sort((a, b) => a - b); + for (const record of this.records) { + let lo = 0; + let hi = sorted.length; + while (lo < hi) { + const mid = lo + hi >>> 1; + if (sorted[mid] < record.i) lo = mid + 1; + else hi = mid; + } + record.i -= lo; + } + } + getValueForItemAtKeyId(item, keyId) { + return item[this._keysMap[keyId]]; + } + size() { + return this.records.length; + } + _createStringRecord(doc, docIndex) { + if (!isDefined(doc) || isBlank(doc)) return null; + return { + v: doc, + i: docIndex, + n: this.norm.get(doc) + }; + } + _createObjectRecord(doc, docIndex) { + const record = { + i: docIndex, + $: {} + }; + for (let keyIndex = 0, keyLen = this.keys.length; keyIndex < keyLen; keyIndex++) { + const key = this.keys[keyIndex]; + const value = key.getFn ? key.getFn(doc) : this.getFn(doc, key.path); + if (!isDefined(value)) continue; + if (isArray(value)) { + const subRecords = []; + for (let i = 0, len = value.length; i < len; i += 1) { + const item = value[i]; + if (!isDefined(item)) continue; + if (isString(item)) { + if (!isBlank(item)) { + const subRecord = { + v: item, + i, + n: this.norm.get(item) + }; + subRecords.push(subRecord); + } + } else if (isDefined(item.v)) { + const text = isString(item.v) ? item.v : toString(item.v); + if (!isBlank(text)) { + const subRecord = { + v: text, + i: item.i, + n: this.norm.get(text) + }; + subRecords.push(subRecord); + } + } + } + record.$[keyIndex] = subRecords; + } else if (isString(value) && !isBlank(value)) { + const subRecord = { + v: value, + n: this.norm.get(value) + }; + record.$[keyIndex] = subRecord; + } + } + return record; + } + toJSON() { + return { + keys: this.keys.map(({ getFn, ...key }) => key), + records: this.records + }; + } +}; +function createIndex(keys, docs, { getFn = Config.getFn, fieldNormWeight = Config.fieldNormWeight } = {}) { + const myIndex = new FuseIndex({ + getFn, + fieldNormWeight + }); + myIndex.setKeys(keys.map(createKey)); + myIndex.setSources(docs); + myIndex.create(); + return myIndex; } -function parseIndex(data, { - getFn = Config.getFn, - fieldNormWeight = Config.fieldNormWeight -} = {}) { - const { - keys, - records - } = data; - const myIndex = new FuseIndex({ - getFn, - fieldNormWeight - }); - myIndex.setKeys(keys); - myIndex.setIndexRecords(records); - return myIndex; +function parseIndex(data, { getFn = Config.getFn, fieldNormWeight = Config.fieldNormWeight } = {}) { + const { keys, records } = data; + const myIndex = new FuseIndex({ + getFn, + fieldNormWeight + }); + myIndex.setKeys(keys); + myIndex.setIndexRecords(records); + return myIndex; } +//#endregion +//#region src/search/bitap/convertMaskToIndices.ts function convertMaskToIndices(matchmask = [], minMatchCharLength = Config.minMatchCharLength) { - const indices = []; - let start = -1; - let end = -1; - let i = 0; - for (let len = matchmask.length; i < len; i += 1) { - const match = matchmask[i]; - if (match && start === -1) { - start = i; - } else if (!match && start !== -1) { - end = i - 1; - if (end - start + 1 >= minMatchCharLength) { - indices.push([start, end]); - } - start = -1; - } - } - - // (i-1 - start) + 1 => i - start - if (matchmask[i - 1] && i - start >= minMatchCharLength) { - indices.push([start, i - 1]); - } - return indices; + const indices = []; + let start = -1; + let end = -1; + let i = 0; + for (let len = matchmask.length; i < len; i += 1) { + const match = matchmask[i]; + if (match && start === -1) start = i; + else if (!match && start !== -1) { + end = i - 1; + if (end - start + 1 >= minMatchCharLength) indices.push([start, end]); + start = -1; + } + } + if (matchmask[i - 1] && i - start >= minMatchCharLength) indices.push([start, i - 1]); + return indices; } -// Machine word size -const MAX_BITS = 32; - -function search(text, pattern, patternAlphabet, { - location = Config.location, - distance = Config.distance, - threshold = Config.threshold, - findAllMatches = Config.findAllMatches, - minMatchCharLength = Config.minMatchCharLength, - includeMatches = Config.includeMatches, - ignoreLocation = Config.ignoreLocation -} = {}) { - if (pattern.length > MAX_BITS) { - throw new Error(PATTERN_LENGTH_TOO_LARGE(MAX_BITS)); - } - const patternLen = pattern.length; - // Set starting location at beginning text and initialize the alphabet. - const textLen = text.length; - // Handle the case when location > text.length - const expectedLocation = Math.max(0, Math.min(location, textLen)); - // Highest score beyond which we give up. - let currentThreshold = threshold; - // Is there a nearby exact match? (speedup) - let bestLocation = expectedLocation; - - // Inlined score computation — avoids object allocation per call in hot loops. - // See ./computeScore.ts for the documented version of this formula. - const calcScore = (errors, currentLocation) => { - const accuracy = errors / patternLen; - if (ignoreLocation) return accuracy; - const proximity = Math.abs(expectedLocation - currentLocation); - if (!distance) return proximity ? 1.0 : accuracy; - return accuracy + proximity / distance; - }; - - // Performance: only computer matches when the minMatchCharLength > 1 - // OR if `includeMatches` is true. - const computeMatches = minMatchCharLength > 1 || includeMatches; - // A mask of the matches, used for building the indices - const matchMask = computeMatches ? Array(textLen) : []; - let index; - - // Get all exact matches, here for speed up - while ((index = text.indexOf(pattern, bestLocation)) > -1) { - const score = calcScore(0, index); - currentThreshold = Math.min(score, currentThreshold); - bestLocation = index + patternLen; - if (computeMatches) { - let i = 0; - while (i < patternLen) { - matchMask[index + i] = 1; - i += 1; - } - } - } - - // Reset the best location - bestLocation = -1; - let lastBitArr = []; - let finalScore = 1; - let bestErrors = 0; - let binMax = patternLen + textLen; - const mask = 1 << patternLen - 1; - for (let i = 0; i < patternLen; i += 1) { - // Scan for the best match; each iteration allows for one more error. - // Run a binary search to determine how far from the match location we can stray - // at this error level. - let binMin = 0; - let binMid = binMax; - while (binMin < binMid) { - const score = calcScore(i, expectedLocation + binMid); - if (score <= currentThreshold) { - binMin = binMid; - } else { - binMax = binMid; - } - binMid = Math.floor((binMax - binMin) / 2 + binMin); - } - - // Use the result from this iteration as the maximum for the next. - binMax = binMid; - let start = Math.max(1, expectedLocation - binMid + 1); - const finish = findAllMatches ? textLen : Math.min(expectedLocation + binMid, textLen) + patternLen; - - // Initialize the bit array - const bitArr = Array(finish + 2); - bitArr[finish + 1] = (1 << i) - 1; - for (let j = finish; j >= start; j -= 1) { - const currentLocation = j - 1; - const charMatch = patternAlphabet[text[currentLocation]]; - - // First pass: exact match - bitArr[j] = (bitArr[j + 1] << 1 | 1) & charMatch; - - // Subsequent passes: fuzzy match - if (i) { - bitArr[j] |= (lastBitArr[j + 1] | lastBitArr[j]) << 1 | 1 | lastBitArr[j + 1]; - } - if (bitArr[j] & mask) { - finalScore = calcScore(i, currentLocation); - - // This match will almost certainly be better than any existing match. - // But check anyway. - if (finalScore <= currentThreshold) { - // Indeed it is - currentThreshold = finalScore; - bestLocation = currentLocation; - bestErrors = i; - - // Already passed `loc`, downhill from here on in. - if (bestLocation <= expectedLocation) { - break; - } - - // When passing `bestLocation`, don't exceed our current distance from `expectedLocation`. - start = Math.max(1, 2 * expectedLocation - bestLocation); - } - } - } - - // No hope for a (better) match at greater error levels. - const score = calcScore(i + 1, expectedLocation); - if (score > currentThreshold) { - break; - } - lastBitArr = bitArr; - } - - // Fill matchMask across the matched window only. Bitap anchors a match at - // bestLocation (the start), spanning patternLen characters plus up to - // bestErrors extra characters when errors are text-side insertions. Marking - // alphabet positions in that window keeps the highlight indices honest about - // what actually matched, instead of every pattern-alphabet character the - // scan happened to visit. - if (computeMatches && bestLocation >= 0) { - const matchEnd = Math.min(textLen - 1, bestLocation + patternLen - 1 + bestErrors); - for (let k = bestLocation; k <= matchEnd; k += 1) { - if (patternAlphabet[text[k]]) { - matchMask[k] = 1; - } - } - } - const result = { - isMatch: bestLocation >= 0, - // Count exact matches (those with a score of 0) to be "almost" exact - score: Math.max(0.001, finalScore) - }; - if (computeMatches) { - const indices = convertMaskToIndices(matchMask, minMatchCharLength); - if (!indices.length) { - result.isMatch = false; - } else if (includeMatches) { - result.indices = indices; - } - } - return result; +//#endregion +//#region src/search/bitap/search.ts +function search(text, pattern, patternAlphabet, { location = Config.location, distance = Config.distance, threshold = Config.threshold, findAllMatches = Config.findAllMatches, minMatchCharLength = Config.minMatchCharLength, includeMatches = Config.includeMatches, ignoreLocation = Config.ignoreLocation } = {}) { + if (pattern.length > 32) throw new Error(PATTERN_LENGTH_TOO_LARGE(32)); + const patternLen = pattern.length; + const textLen = text.length; + const expectedLocation = Math.max(0, Math.min(location, textLen)); + let currentThreshold = threshold; + let bestLocation = expectedLocation; + const calcScore = (errors, currentLocation) => { + const accuracy = errors / patternLen; + if (ignoreLocation) return accuracy; + const proximity = Math.abs(expectedLocation - currentLocation); + if (!distance) return proximity ? 1 : accuracy; + return accuracy + proximity / distance; + }; + const computeMatches = minMatchCharLength > 1 || includeMatches; + const matchMask = computeMatches ? Array(textLen) : []; + let index; + while ((index = text.indexOf(pattern, bestLocation)) > -1) { + const score = calcScore(0, index); + currentThreshold = Math.min(score, currentThreshold); + bestLocation = index + patternLen; + if (computeMatches) { + let i = 0; + while (i < patternLen) { + matchMask[index + i] = 1; + i += 1; + } + } + } + bestLocation = -1; + let lastBitArr = []; + let finalScore = 1; + let bestErrors = 0; + let binMax = patternLen + textLen; + const mask = 1 << patternLen - 1; + for (let i = 0; i < patternLen; i += 1) { + let binMin = 0; + let binMid = binMax; + while (binMin < binMid) { + if (calcScore(i, expectedLocation + binMid) <= currentThreshold) binMin = binMid; + else binMax = binMid; + binMid = Math.floor((binMax - binMin) / 2 + binMin); + } + binMax = binMid; + let start = Math.max(1, expectedLocation - binMid + 1); + const finish = findAllMatches ? textLen : Math.min(expectedLocation + binMid, textLen) + patternLen; + const bitArr = Array(finish + 2); + bitArr[finish + 1] = (1 << i) - 1; + for (let j = finish; j >= start; j -= 1) { + const currentLocation = j - 1; + const charMatch = patternAlphabet[text[currentLocation]]; + bitArr[j] = (bitArr[j + 1] << 1 | 1) & charMatch; + if (i) bitArr[j] |= (lastBitArr[j + 1] | lastBitArr[j]) << 1 | 1 | lastBitArr[j + 1]; + if (bitArr[j] & mask) { + finalScore = calcScore(i, currentLocation); + if (finalScore <= currentThreshold) { + currentThreshold = finalScore; + bestLocation = currentLocation; + bestErrors = i; + if (bestLocation <= expectedLocation) break; + start = Math.max(1, 2 * expectedLocation - bestLocation); + } + } + } + if (calcScore(i + 1, expectedLocation) > currentThreshold) break; + lastBitArr = bitArr; + } + if (computeMatches && bestLocation >= 0) { + const matchEnd = Math.min(textLen - 1, bestLocation + patternLen - 1 + bestErrors); + for (let k = bestLocation; k <= matchEnd; k += 1) if (patternAlphabet[text[k]]) matchMask[k] = 1; + } + const result = { + isMatch: bestLocation >= 0, + score: Math.max(.001, finalScore) + }; + if (computeMatches) { + const indices = convertMaskToIndices(matchMask, minMatchCharLength); + if (!indices.length) result.isMatch = false; + else if (includeMatches) result.indices = indices; + } + return result; } +//#endregion +//#region src/search/bitap/createPatternAlphabet.ts function createPatternAlphabet(pattern) { - const mask = {}; - for (let i = 0, len = pattern.length; i < len; i += 1) { - const char = pattern.charAt(i); - mask[char] = (mask[char] || 0) | 1 << len - i - 1; - } - return mask; + const mask = {}; + for (let i = 0, len = pattern.length; i < len; i += 1) { + const char = pattern.charAt(i); + mask[char] = (mask[char] || 0) | 1 << len - i - 1; + } + return mask; } +//#endregion +//#region src/helpers/mergeIndices.ts function mergeIndices(indices) { - if (indices.length <= 1) return indices; - indices.sort((a, b) => a[0] - b[0] || a[1] - b[1]); - const merged = [indices[0]]; - for (let i = 1, len = indices.length; i < len; i += 1) { - const last = merged[merged.length - 1]; - const curr = indices[i]; - if (curr[0] <= last[1] + 1) { - last[1] = Math.max(last[1], curr[1]); - } else { - merged.push(curr); - } - } - return merged; + if (indices.length <= 1) return indices; + indices.sort((a, b) => a[0] - b[0] || a[1] - b[1]); + const merged = [indices[0]]; + for (let i = 1, len = indices.length; i < len; i += 1) { + const last = merged[merged.length - 1]; + const curr = indices[i]; + if (curr[0] <= last[1] + 1) last[1] = Math.max(last[1], curr[1]); + else merged.push(curr); + } + return merged; } -// Characters that survive NFD normalization unchanged and need explicit mapping +//#endregion +//#region src/helpers/diacritics.ts const NON_DECOMPOSABLE_MAP = { - '\u0142': 'l', - // ł - '\u0141': 'L', - // Ł - '\u0111': 'd', - // đ - '\u0110': 'D', - // Đ - '\u00F8': 'o', - // ø - '\u00D8': 'O', - // Ø - '\u0127': 'h', - // ħ - '\u0126': 'H', - // Ħ - '\u0167': 't', - // ŧ - '\u0166': 'T', - // Ŧ - '\u0131': 'i', - // ı - '\u00DF': 'ss' // ß + "ł": "l", + "Ł": "L", + "đ": "d", + "Đ": "D", + "ø": "o", + "Ø": "O", + "ħ": "h", + "Ħ": "H", + "ŧ": "t", + "Ŧ": "T", + "ı": "i", + "ß": "ss" +}; +const NON_DECOMPOSABLE_RE = new RegExp("[" + Object.keys(NON_DECOMPOSABLE_MAP).join("") + "]", "g"); +const stripDiacritics = typeof String.prototype.normalize === "function" ? (str) => str.normalize("NFD").replace(/[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]/g, "").replace(NON_DECOMPOSABLE_RE, (ch) => NON_DECOMPOSABLE_MAP[ch]) : (str) => str; + +//#endregion +//#region src/search/bitap/index.ts +var BitapSearch = class { + constructor(pattern, { location = Config.location, threshold = Config.threshold, distance = Config.distance, includeMatches = Config.includeMatches, findAllMatches = Config.findAllMatches, minMatchCharLength = Config.minMatchCharLength, isCaseSensitive = Config.isCaseSensitive, ignoreDiacritics = Config.ignoreDiacritics, ignoreLocation = Config.ignoreLocation } = {}) { + this.options = { + location, + threshold, + distance, + includeMatches, + findAllMatches, + minMatchCharLength, + isCaseSensitive, + ignoreDiacritics, + ignoreLocation + }; + pattern = isCaseSensitive ? pattern : pattern.toLowerCase(); + pattern = ignoreDiacritics ? stripDiacritics(pattern) : pattern; + this.pattern = pattern; + this.chunks = []; + if (!this.pattern.length) return; + const addChunk = (pattern, startIndex) => { + this.chunks.push({ + pattern, + alphabet: createPatternAlphabet(pattern), + startIndex + }); + }; + const len = this.pattern.length; + if (len > 32) { + let i = 0; + const remainder = len % 32; + const end = len - remainder; + while (i < end) { + addChunk(this.pattern.substr(i, 32), i); + i += 32; + } + if (remainder) { + const startIndex = len - 32; + addChunk(this.pattern.substr(startIndex), startIndex); + } + } else addChunk(this.pattern, 0); + } + searchIn(text) { + const { isCaseSensitive, ignoreDiacritics, includeMatches } = this.options; + text = isCaseSensitive ? text : text.toLowerCase(); + text = ignoreDiacritics ? stripDiacritics(text) : text; + if (this.pattern === text) { + const result = { + isMatch: true, + score: 0 + }; + if (includeMatches) result.indices = [[0, text.length - 1]]; + return result; + } + const { location, distance, threshold, findAllMatches, minMatchCharLength, ignoreLocation } = this.options; + const allIndices = []; + let totalScore = 0; + let hasMatches = false; + this.chunks.forEach(({ pattern, alphabet, startIndex }) => { + const { isMatch, score, indices } = search(text, pattern, alphabet, { + location: location + startIndex, + distance, + threshold, + findAllMatches, + minMatchCharLength, + includeMatches, + ignoreLocation + }); + if (isMatch) hasMatches = true; + totalScore += score; + if (isMatch && indices) allIndices.push(...indices); + }); + const result = { + isMatch: hasMatches, + score: hasMatches ? totalScore / this.chunks.length : 1 + }; + if (hasMatches && includeMatches) result.indices = mergeIndices(allIndices); + return result; + } }; -const NON_DECOMPOSABLE_RE = new RegExp('[' + Object.keys(NON_DECOMPOSABLE_MAP).join('') + ']', 'g'); -const stripDiacritics = typeof String.prototype.normalize === 'function' ? str => str.normalize('NFD').replace(/[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]/g, '').replace(NON_DECOMPOSABLE_RE, ch => NON_DECOMPOSABLE_MAP[ch]) : str => str; - -class BitapSearch { - constructor(pattern, { - location = Config.location, - threshold = Config.threshold, - distance = Config.distance, - includeMatches = Config.includeMatches, - findAllMatches = Config.findAllMatches, - minMatchCharLength = Config.minMatchCharLength, - isCaseSensitive = Config.isCaseSensitive, - ignoreDiacritics = Config.ignoreDiacritics, - ignoreLocation = Config.ignoreLocation - } = {}) { - this.options = { - location, - threshold, - distance, - includeMatches, - findAllMatches, - minMatchCharLength, - isCaseSensitive, - ignoreDiacritics, - ignoreLocation - }; - pattern = isCaseSensitive ? pattern : pattern.toLowerCase(); - pattern = ignoreDiacritics ? stripDiacritics(pattern) : pattern; - this.pattern = pattern; - this.chunks = []; - if (!this.pattern.length) { - return; - } - const addChunk = (pattern, startIndex) => { - this.chunks.push({ - pattern, - alphabet: createPatternAlphabet(pattern), - startIndex - }); - }; - const len = this.pattern.length; - if (len > MAX_BITS) { - let i = 0; - const remainder = len % MAX_BITS; - const end = len - remainder; - while (i < end) { - addChunk(this.pattern.substr(i, MAX_BITS), i); - i += MAX_BITS; - } - if (remainder) { - const startIndex = len - MAX_BITS; - addChunk(this.pattern.substr(startIndex), startIndex); - } - } else { - addChunk(this.pattern, 0); - } - } - searchIn(text) { - const { - isCaseSensitive, - ignoreDiacritics, - includeMatches - } = this.options; - text = isCaseSensitive ? text : text.toLowerCase(); - text = ignoreDiacritics ? stripDiacritics(text) : text; - - // Exact match - if (this.pattern === text) { - const result = { - isMatch: true, - score: 0 - }; - if (includeMatches) { - result.indices = [[0, text.length - 1]]; - } - return result; - } - - // Otherwise, use Bitap algorithm - const { - location, - distance, - threshold, - findAllMatches, - minMatchCharLength, - ignoreLocation - } = this.options; - const allIndices = []; - let totalScore = 0; - let hasMatches = false; - this.chunks.forEach(({ - pattern, - alphabet, - startIndex - }) => { - const { - isMatch, - score, - indices - } = search(text, pattern, alphabet, { - location: location + startIndex, - distance, - threshold, - findAllMatches, - minMatchCharLength, - includeMatches, - ignoreLocation - }); - if (isMatch) { - hasMatches = true; - } - totalScore += score; - if (isMatch && indices) { - allIndices.push(...indices); - } - }); - const result = { - isMatch: hasMatches, - score: hasMatches ? totalScore / this.chunks.length : 1 - }; - if (hasMatches && includeMatches) { - result.indices = mergeIndices(allIndices); - } - return result; - } -} - -// ── Matcher interface ───────────────────────────────────────────── -// -// Each matcher is a lightweight object with a type tag and a search -// function. No class hierarchy needed — the search logic for most -// matchers is a one-liner. - -// ── Matcher definition ──────────────────────────────────────────── -// -// A definition pairs the detection regexes (used by parseQuery to -// recognize string-syntax operators like ^, =, !) with a factory -// that creates a Matcher instance. - -// Whether a matcher type can return multiple index ranges -const MULTI_MATCH_TYPES = new Set(['fuzzy', 'include']); -// Whether a matcher type is an inverse match +//#endregion +//#region src/search/extended/matchers.ts +const MULTI_MATCH_TYPES = new Set(["fuzzy", "include"]); function isInverse(type) { - return type.startsWith('inverse'); + return type.startsWith("inverse"); } - -// ── Matcher definitions ─────────────────────────────────────────── -// -// Order matters — parseQuery tries each in sequence and uses the -// first match. FuzzyMatch (catch-all) must be last. - -// prettier-ignore const matchers = [ -// =term — exact match -{ - type: 'exact', - multiRegex: /^="(.*)"$/, - singleRegex: /^=(.*)$/, - create: pattern => ({ - type: 'exact', - search(text) { - const isMatch = text === pattern; - return { - isMatch, - score: isMatch ? 0 : 1, - indices: [0, pattern.length - 1] - }; - } - }) -}, -// 'term — include (substring) match -{ - type: 'include', - multiRegex: /^'"(.*)"$/, - singleRegex: /^'(.*)$/, - create: pattern => ({ - type: 'include', - search(text) { - let location = 0; - let index; - const indices = []; - const patternLen = pattern.length; - while ((index = text.indexOf(pattern, location)) > -1) { - location = index + patternLen; - indices.push([index, location - 1]); - } - const isMatch = !!indices.length; - return { - isMatch, - score: isMatch ? 0 : 1, - indices - }; - } - }) -}, -// ^term — prefix match -{ - type: 'prefix-exact', - multiRegex: /^\^"(.*)"$/, - singleRegex: /^\^(.*)$/, - create: pattern => ({ - type: 'prefix-exact', - search(text) { - const isMatch = text.startsWith(pattern); - return { - isMatch, - score: isMatch ? 0 : 1, - indices: [0, pattern.length - 1] - }; - } - }) -}, -// !^term — inverse prefix match -{ - type: 'inverse-prefix-exact', - multiRegex: /^!\^"(.*)"$/, - singleRegex: /^!\^(.*)$/, - create: pattern => ({ - type: 'inverse-prefix-exact', - search(text) { - const isMatch = !text.startsWith(pattern); - return { - isMatch, - score: isMatch ? 0 : 1, - indices: [0, text.length - 1] - }; - } - }) -}, -// !term$ — inverse suffix match -{ - type: 'inverse-suffix-exact', - multiRegex: /^!"(.*)"\$$/, - singleRegex: /^!(.*)\$$/, - create: pattern => ({ - type: 'inverse-suffix-exact', - search(text) { - const isMatch = !text.endsWith(pattern); - return { - isMatch, - score: isMatch ? 0 : 1, - indices: [0, text.length - 1] - }; - } - }) -}, -// term$ — suffix match -{ - type: 'suffix-exact', - multiRegex: /^"(.*)"\$$/, - singleRegex: /^(.*)\$$/, - create: pattern => ({ - type: 'suffix-exact', - search(text) { - const isMatch = text.endsWith(pattern); - return { - isMatch, - score: isMatch ? 0 : 1, - indices: [text.length - pattern.length, text.length - 1] - }; - } - }) -}, -// !term — inverse exact (does not contain) -{ - type: 'inverse-exact', - multiRegex: /^!"(.*)"$/, - singleRegex: /^!(.*)$/, - create: pattern => ({ - type: 'inverse-exact', - search(text) { - const isMatch = text.indexOf(pattern) === -1; - return { - isMatch, - score: isMatch ? 0 : 1, - indices: [0, text.length - 1] - }; - } - }) -}, -// term — fuzzy match (catch-all, must be last) -{ - type: 'fuzzy', - multiRegex: /^"(.*)"$/, - singleRegex: /^(.*)$/, - create: (pattern, options = {}) => { - const bitap = new BitapSearch(pattern, { - location: options.location ?? Config.location, - threshold: options.threshold ?? Config.threshold, - distance: options.distance ?? Config.distance, - includeMatches: options.includeMatches ?? Config.includeMatches, - findAllMatches: options.findAllMatches ?? Config.findAllMatches, - minMatchCharLength: options.minMatchCharLength ?? Config.minMatchCharLength, - isCaseSensitive: options.isCaseSensitive ?? Config.isCaseSensitive, - ignoreDiacritics: options.ignoreDiacritics ?? Config.ignoreDiacritics, - ignoreLocation: options.ignoreLocation ?? Config.ignoreLocation - }); - return { - type: 'fuzzy', - search(text) { - return bitap.searchIn(text); - } - }; - } -}]; - + { + type: "exact", + multiRegex: /^="(.*)"$/, + singleRegex: /^=(.*)$/, + create: (pattern) => ({ + type: "exact", + search(text) { + const isMatch = text === pattern; + return { + isMatch, + score: isMatch ? 0 : 1, + indices: [0, pattern.length - 1] + }; + } + }) + }, + { + type: "include", + multiRegex: /^'"(.*)"$/, + singleRegex: /^'(.*)$/, + create: (pattern) => ({ + type: "include", + search(text) { + let location = 0; + let index; + const indices = []; + const patternLen = pattern.length; + while ((index = text.indexOf(pattern, location)) > -1) { + location = index + patternLen; + indices.push([index, location - 1]); + } + const isMatch = !!indices.length; + return { + isMatch, + score: isMatch ? 0 : 1, + indices + }; + } + }) + }, + { + type: "prefix-exact", + multiRegex: /^\^"(.*)"$/, + singleRegex: /^\^(.*)$/, + create: (pattern) => ({ + type: "prefix-exact", + search(text) { + const isMatch = text.startsWith(pattern); + return { + isMatch, + score: isMatch ? 0 : 1, + indices: [0, pattern.length - 1] + }; + } + }) + }, + { + type: "inverse-prefix-exact", + multiRegex: /^!\^"(.*)"$/, + singleRegex: /^!\^(.*)$/, + create: (pattern) => ({ + type: "inverse-prefix-exact", + search(text) { + const isMatch = !text.startsWith(pattern); + return { + isMatch, + score: isMatch ? 0 : 1, + indices: [0, text.length - 1] + }; + } + }) + }, + { + type: "inverse-suffix-exact", + multiRegex: /^!"(.*)"\$$/, + singleRegex: /^!(.*)\$$/, + create: (pattern) => ({ + type: "inverse-suffix-exact", + search(text) { + const isMatch = !text.endsWith(pattern); + return { + isMatch, + score: isMatch ? 0 : 1, + indices: [0, text.length - 1] + }; + } + }) + }, + { + type: "suffix-exact", + multiRegex: /^"(.*)"\$$/, + singleRegex: /^(.*)\$$/, + create: (pattern) => ({ + type: "suffix-exact", + search(text) { + const isMatch = text.endsWith(pattern); + return { + isMatch, + score: isMatch ? 0 : 1, + indices: [text.length - pattern.length, text.length - 1] + }; + } + }) + }, + { + type: "inverse-exact", + multiRegex: /^!"(.*)"$/, + singleRegex: /^!(.*)$/, + create: (pattern) => ({ + type: "inverse-exact", + search(text) { + const isMatch = text.indexOf(pattern) === -1; + return { + isMatch, + score: isMatch ? 0 : 1, + indices: [0, text.length - 1] + }; + } + }) + }, + { + type: "fuzzy", + multiRegex: /^"(.*)"$/, + singleRegex: /^(.*)$/, + create: (pattern, options = {}) => { + const bitap = new BitapSearch(pattern, { + location: options.location ?? Config.location, + threshold: options.threshold ?? Config.threshold, + distance: options.distance ?? Config.distance, + includeMatches: options.includeMatches ?? Config.includeMatches, + findAllMatches: options.findAllMatches ?? Config.findAllMatches, + minMatchCharLength: options.minMatchCharLength ?? Config.minMatchCharLength, + isCaseSensitive: options.isCaseSensitive ?? Config.isCaseSensitive, + ignoreDiacritics: options.ignoreDiacritics ?? Config.ignoreDiacritics, + ignoreLocation: options.ignoreLocation ?? Config.ignoreLocation + }); + return { + type: "fuzzy", + search(text) { + return bitap.searchIn(text); + } + }; + } + } +]; + +//#endregion +//#region src/search/extended/parseQuery.ts const matchersLen = matchers.length; -const ESCAPED_PIPE = '\u0000'; // placeholder for escaped \| -const OR_TOKEN = '|'; - -// Tokenize a query string into individual search terms. -// Respects multi-match quoted tokens like ="said "test"" or ^"hello world"$ -// where inner spaces and quotes are part of the token. +const ESCAPED_PIPE = "\0"; +const OR_TOKEN = "|"; function tokenize(pattern) { - const tokens = []; - const len = pattern.length; - let i = 0; - while (i < len) { - // Skip spaces - while (i < len && pattern[i] === ' ') i++; - if (i >= len) break; - - // Scan past prefix characters (=, !, ^, ') to see if a quote follows - let j = i; - while (j < len && pattern[j] !== ' ' && pattern[j] !== '"') j++; - if (j < len && pattern[j] === '"') { - // Multi-match token: prefix + "content" (possibly with inner quotes) - // Find the closing " that ends this token: - // it must be followed by optional $, then space or end-of-string - j++; // skip opening quote - while (j < len) { - if (pattern[j] === '"') { - // Check if this is the closing quote - const next = j + 1; - if (next >= len || pattern[next] === ' ') { - j++; // include closing quote - break; - } - if (pattern[next] === '$' && (next + 1 >= len || pattern[next + 1] === ' ')) { - j += 2; // include "$ - break; - } - } - j++; - } - tokens.push(pattern.substring(i, j)); - i = j; - } else { - // Regular (unquoted) token: read until space or end - while (j < len && pattern[j] !== ' ') j++; - tokens.push(pattern.substring(i, j)); - i = j; - } - } - return tokens; + const tokens = []; + const len = pattern.length; + let i = 0; + while (i < len) { + while (i < len && pattern[i] === " ") i++; + if (i >= len) break; + let j = i; + while (j < len && pattern[j] !== " " && pattern[j] !== "\"") j++; + if (j < len && pattern[j] === "\"") { + j++; + while (j < len) { + if (pattern[j] === "\"") { + const next = j + 1; + if (next >= len || pattern[next] === " ") { + j++; + break; + } + if (pattern[next] === "$" && (next + 1 >= len || pattern[next + 1] === " ")) { + j += 2; + break; + } + } + j++; + } + tokens.push(pattern.substring(i, j)); + i = j; + } else { + while (j < len && pattern[j] !== " ") j++; + tokens.push(pattern.substring(i, j)); + i = j; + } + } + return tokens; } function getMatch(pattern, exp) { - const matches = pattern.match(exp); - return matches ? matches[1] : null; + const matches = pattern.match(exp); + return matches ? matches[1] : null; } - -// Return a 2D array representation of the query, for simpler parsing. -// Example: -// "^core go$ | rb$ | py$ xy$" => [["^core", "go$"], ["rb$"], ["py$", "xy$"]] function parseQuery(pattern, options = {}) { - // Replace escaped \| with placeholder before splitting on | - const escaped = pattern.replace(/\\\|/g, ESCAPED_PIPE); - return escaped.split(OR_TOKEN).map(item => { - // Restore escaped pipes in each OR group - const restored = item.replace(/\u0000/g, '|'); - const query = tokenize(restored.trim()).filter(item => item && !!item.trim()); - const results = []; - for (let i = 0, len = query.length; i < len; i += 1) { - const queryItem = query[i]; - - // 1. Handle multiple query match (i.e, ones that are quoted, like `"hello world"`) - let found = false; - let idx = -1; - while (!found && ++idx < matchersLen) { - const def = matchers[idx]; - const token = getMatch(queryItem, def.multiRegex); - if (token) { - results.push(def.create(token, options)); - found = true; - } - } - if (found) { - continue; - } - - // 2. Handle single query matches (i.e, ones that are *not* quoted) - idx = -1; - while (++idx < matchersLen) { - const def = matchers[idx]; - const token = getMatch(queryItem, def.singleRegex); - if (token) { - results.push(def.create(token, options)); - break; - } - } - } - return results; - }); + return pattern.replace(/\\\|/g, ESCAPED_PIPE).split(OR_TOKEN).map((item) => { + const query = tokenize(item.replace(/\u0000/g, "|").trim()).filter((item) => item && !!item.trim()); + const results = []; + for (let i = 0, len = query.length; i < len; i += 1) { + const queryItem = query[i]; + let found = false; + let idx = -1; + while (!found && ++idx < matchersLen) { + const def = matchers[idx]; + const token = getMatch(queryItem, def.multiRegex); + if (token) { + results.push(def.create(token, options)); + found = true; + } + } + if (found) continue; + idx = -1; + while (++idx < matchersLen) { + const def = matchers[idx]; + const token = getMatch(queryItem, def.singleRegex); + if (token) { + results.push(def.create(token, options)); + break; + } + } + } + return results; + }); } -class ExtendedSearch { - constructor(pattern, { - isCaseSensitive = Config.isCaseSensitive, - ignoreDiacritics = Config.ignoreDiacritics, - includeMatches = Config.includeMatches, - minMatchCharLength = Config.minMatchCharLength, - ignoreLocation = Config.ignoreLocation, - findAllMatches = Config.findAllMatches, - location = Config.location, - threshold = Config.threshold, - distance = Config.distance - } = {}) { - this.query = null; - this.options = { - isCaseSensitive, - ignoreDiacritics, - includeMatches, - minMatchCharLength, - findAllMatches, - ignoreLocation, - location, - threshold, - distance - }; - pattern = isCaseSensitive ? pattern : pattern.toLowerCase(); - pattern = ignoreDiacritics ? stripDiacritics(pattern) : pattern; - this.pattern = pattern; - this.query = parseQuery(this.pattern, this.options); - } - static condition(_, options) { - return options.useExtendedSearch; - } - - // Note: searchIn operates on a single text value and sets hasInverse on the - // result when inverse patterns are involved. _searchObjectList uses this to - // switch from "ANY key" to "ALL keys" aggregation. See #712. - searchIn(text) { - const query = this.query; - if (!query) { - return { - isMatch: false, - score: 1 - }; - } - const { - includeMatches, - isCaseSensitive, - ignoreDiacritics - } = this.options; - text = isCaseSensitive ? text : text.toLowerCase(); - text = ignoreDiacritics ? stripDiacritics(text) : text; - let numMatches = 0; - const allIndices = []; - let totalScore = 0; - let hasInverse = false; - - // ORs - for (let i = 0, qLen = query.length; i < qLen; i += 1) { - const searchers = query[i]; - - // Reset indices - allIndices.length = 0; - numMatches = 0; - hasInverse = false; - - // ANDs - for (let j = 0, pLen = searchers.length; j < pLen; j += 1) { - const matcher = searchers[j]; - const { - isMatch, - indices, - score - } = matcher.search(text); - if (isMatch) { - numMatches += 1; - totalScore += score; - if (isInverse(matcher.type)) { - hasInverse = true; - } - if (includeMatches) { - if (MULTI_MATCH_TYPES.has(matcher.type)) { - allIndices.push(...indices); - } else { - allIndices.push(indices); - } - } - } else { - totalScore = 0; - numMatches = 0; - allIndices.length = 0; - hasInverse = false; - break; - } - } - - // OR condition, so if TRUE, return - if (numMatches) { - const result = { - isMatch: true, - score: totalScore / numMatches - }; - if (hasInverse) { - result.hasInverse = true; - } - if (includeMatches) { - result.indices = mergeIndices(allIndices); - } - return result; - } - } - - // Nothing was matched - return { - isMatch: false, - score: 1 - }; - } -} +//#endregion +//#region src/search/extended/index.ts +var ExtendedSearch = class { + constructor(pattern, { isCaseSensitive = Config.isCaseSensitive, ignoreDiacritics = Config.ignoreDiacritics, includeMatches = Config.includeMatches, minMatchCharLength = Config.minMatchCharLength, ignoreLocation = Config.ignoreLocation, findAllMatches = Config.findAllMatches, location = Config.location, threshold = Config.threshold, distance = Config.distance } = {}) { + this.query = null; + this.options = { + isCaseSensitive, + ignoreDiacritics, + includeMatches, + minMatchCharLength, + findAllMatches, + ignoreLocation, + location, + threshold, + distance + }; + pattern = isCaseSensitive ? pattern : pattern.toLowerCase(); + pattern = ignoreDiacritics ? stripDiacritics(pattern) : pattern; + this.pattern = pattern; + this.query = parseQuery(this.pattern, this.options); + } + static condition(_, options) { + return options.useExtendedSearch; + } + searchIn(text) { + const query = this.query; + if (!query) return { + isMatch: false, + score: 1 + }; + const { includeMatches, isCaseSensitive, ignoreDiacritics } = this.options; + text = isCaseSensitive ? text : text.toLowerCase(); + text = ignoreDiacritics ? stripDiacritics(text) : text; + let numMatches = 0; + const allIndices = []; + let totalScore = 0; + let hasInverse = false; + for (let i = 0, qLen = query.length; i < qLen; i += 1) { + const searchers = query[i]; + allIndices.length = 0; + numMatches = 0; + hasInverse = false; + for (let j = 0, pLen = searchers.length; j < pLen; j += 1) { + const matcher = searchers[j]; + const { isMatch, indices, score } = matcher.search(text); + if (isMatch) { + numMatches += 1; + totalScore += score; + if (isInverse(matcher.type)) hasInverse = true; + if (includeMatches) if (MULTI_MATCH_TYPES.has(matcher.type)) allIndices.push(...indices); + else allIndices.push(indices); + } else { + totalScore = 0; + numMatches = 0; + allIndices.length = 0; + hasInverse = false; + break; + } + } + if (numMatches) { + const result = { + isMatch: true, + score: totalScore / numMatches + }; + if (hasInverse) result.hasInverse = true; + if (includeMatches) result.indices = mergeIndices(allIndices); + return result; + } + } + return { + isMatch: false, + score: 1 + }; + } +}; +//#endregion +//#region src/core/register.ts const registeredSearchers = []; function register(...args) { - registeredSearchers.push(...args); + registeredSearchers.push(...args); } function createSearcher(pattern, options) { - for (let i = 0, len = registeredSearchers.length; i < len; i += 1) { - const searcherClass = registeredSearchers[i]; - if (searcherClass.condition(pattern, options)) { - return new searcherClass(pattern, options); - } - } - return new BitapSearch(pattern, options); + for (let i = 0, len = registeredSearchers.length; i < len; i += 1) { + const searcherClass = registeredSearchers[i]; + if (searcherClass.condition(pattern, options)) return new searcherClass(pattern, options); + } + return new BitapSearch(pattern, options); } +//#endregion +//#region src/core/queryParser.ts const LogicalOperator = { - AND: '$and', - OR: '$or' + AND: "$and", + OR: "$or" }; const KeyType = { - PATH: '$path', - PATTERN: '$val' + PATH: "$path", + PATTERN: "$val" }; -const isExpression = query => !!(query[LogicalOperator.AND] || query[LogicalOperator.OR]); -const isPath = query => !!query[KeyType.PATH]; -const isLeaf = query => !isArray(query) && isObject(query) && !isExpression(query); -const convertToExplicit = query => ({ - [LogicalOperator.AND]: Object.keys(query).map(key => ({ - [key]: query[key] - })) -}); - -// When `auto` is `true`, the parse function will infer and initialize and add -// the appropriate `Searcher` instance -function parse(query, options, { - auto = true -} = {}) { - const next = query => { - // Keyless string entry: search across all keys - if (isString(query)) { - const obj = { - keyId: null, - pattern: query - }; - if (auto) { - obj.searcher = createSearcher(query, options); - } - return obj; - } - const keys = Object.keys(query); - const isQueryPath = isPath(query); - if (!isQueryPath && keys.length > 1 && !isExpression(query)) { - return next(convertToExplicit(query)); - } - if (isLeaf(query)) { - const key = isQueryPath ? query[KeyType.PATH] : keys[0]; - const pattern = isQueryPath ? query[KeyType.PATTERN] : query[key]; - if (!isString(pattern)) { - throw new Error(LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY(key)); - } - const obj = { - keyId: createKeyId(key), - pattern - }; - if (auto) { - obj.searcher = createSearcher(pattern, options); - } - return obj; - } - const node = { - children: [], - operator: keys[0] - }; - keys.forEach(key => { - const value = query[key]; - if (isArray(value)) { - value.forEach(item => { - node.children.push(next(item)); - }); - } - }); - return node; - }; - if (!isExpression(query)) { - query = convertToExplicit(query); - } - return next(query); +const isExpression = (query) => !!(query[LogicalOperator.AND] || query[LogicalOperator.OR]); +const isPath = (query) => !!query[KeyType.PATH]; +const isLeaf = (query) => !isArray(query) && isObject(query) && !isExpression(query); +const convertToExplicit = (query) => ({ [LogicalOperator.AND]: Object.keys(query).map((key) => ({ [key]: query[key] })) }); +function parse(query, options, { auto = true } = {}) { + const next = (query) => { + if (isString(query)) { + const obj = { + keyId: null, + pattern: query + }; + if (auto) obj.searcher = createSearcher(query, options); + return obj; + } + const keys = Object.keys(query); + const isQueryPath = isPath(query); + if (!isQueryPath && keys.length > 1 && !isExpression(query)) return next(convertToExplicit(query)); + if (isLeaf(query)) { + const key = isQueryPath ? query[KeyType.PATH] : keys[0]; + const pattern = isQueryPath ? query[KeyType.PATTERN] : query[key]; + if (!isString(pattern)) throw new Error(LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY(key)); + const obj = { + keyId: createKeyId(key), + pattern + }; + if (auto) obj.searcher = createSearcher(pattern, options); + return obj; + } + const node = { + children: [], + operator: keys[0] + }; + keys.forEach((key) => { + const value = query[key]; + if (isArray(value)) value.forEach((item) => { + node.children.push(next(item)); + }); + }); + return node; + }; + if (!isExpression(query)) query = convertToExplicit(query); + return next(query); } -function computeScoreSingle(matches, { - ignoreFieldNorm = Config.ignoreFieldNorm -}) { - let totalScore = 1; - matches.forEach(({ - key, - norm, - score - }) => { - const weight = key ? key.weight : null; - totalScore *= Math.pow(score === 0 && weight ? Number.EPSILON : score, (weight || 1) * (ignoreFieldNorm ? 1 : norm)); - }); - return totalScore; +//#endregion +//#region src/core/computeScore.ts +function computeScoreSingle(matches, { ignoreFieldNorm = Config.ignoreFieldNorm }) { + let totalScore = 1; + matches.forEach(({ key, norm, score }) => { + const weight = key ? key.weight : null; + totalScore *= Math.pow(score === 0 && weight ? Number.EPSILON : score, (weight || 1) * (ignoreFieldNorm ? 1 : norm)); + }); + return totalScore; } -function computeScore(results, { - ignoreFieldNorm = Config.ignoreFieldNorm -}) { - results.forEach(result => { - result.score = computeScoreSingle(result.matches, { - ignoreFieldNorm - }); - }); +function computeScore(results, { ignoreFieldNorm = Config.ignoreFieldNorm }) { + results.forEach((result) => { + result.score = computeScoreSingle(result.matches, { ignoreFieldNorm }); + }); } -// Max-heap by score: keeps the worst (highest) score at the top -// so we can efficiently evict it when a better result arrives. -class MaxHeap { - constructor(limit) { - this.limit = limit; - this.heap = []; - } - get size() { - return this.heap.length; - } - shouldInsert(score) { - return this.size < this.limit || score < this.heap[0].score; - } - insert(item) { - if (this.size < this.limit) { - this.heap.push(item); - this._bubbleUp(this.size - 1); - } else if (item.score < this.heap[0].score) { - this.heap[0] = item; - this._sinkDown(0); - } - } - extractSorted(sortFn) { - return this.heap.sort(sortFn); - } - _bubbleUp(i) { - const heap = this.heap; - while (i > 0) { - const parent = i - 1 >> 1; - if (heap[i].score <= heap[parent].score) break; - const tmp = heap[i]; - heap[i] = heap[parent]; - heap[parent] = tmp; - i = parent; - } - } - _sinkDown(i) { - const heap = this.heap; - const len = heap.length; - let largest = i; - do { - i = largest; - const left = 2 * i + 1; - const right = 2 * i + 2; - if (left < len && heap[left].score > heap[largest].score) { - largest = left; - } - if (right < len && heap[right].score > heap[largest].score) { - largest = right; - } - if (largest !== i) { - const tmp = heap[i]; - heap[i] = heap[largest]; - heap[largest] = tmp; - } - } while (largest !== i); - } -} +//#endregion +//#region src/tools/MaxHeap.ts +var MaxHeap = class { + constructor(limit) { + this.limit = limit; + this.heap = []; + } + get size() { + return this.heap.length; + } + shouldInsert(score) { + return this.size < this.limit || score < this.heap[0].score; + } + insert(item) { + if (this.size < this.limit) { + this.heap.push(item); + this._bubbleUp(this.size - 1); + } else if (item.score < this.heap[0].score) { + this.heap[0] = item; + this._sinkDown(0); + } + } + extractSorted(sortFn) { + return this.heap.sort(sortFn); + } + _bubbleUp(i) { + const heap = this.heap; + while (i > 0) { + const parent = i - 1 >> 1; + if (heap[i].score <= heap[parent].score) break; + const tmp = heap[i]; + heap[i] = heap[parent]; + heap[parent] = tmp; + i = parent; + } + } + _sinkDown(i) { + const heap = this.heap; + const len = heap.length; + let largest = i; + do { + i = largest; + const left = 2 * i + 1; + const right = 2 * i + 2; + if (left < len && heap[left].score > heap[largest].score) largest = left; + if (right < len && heap[right].score > heap[largest].score) largest = right; + if (largest !== i) { + const tmp = heap[i]; + heap[i] = heap[largest]; + heap[largest] = tmp; + } + } while (largest !== i); + } +}; +//#endregion +//#region src/core/formatMatches.ts function formatMatches(result) { - const matches = []; - result.matches.forEach(match => { - if (!isDefined(match.indices) || !match.indices.length) { - return; - } - const obj = { - indices: match.indices, - value: match.value - }; - if (match.key) { - // `key.id` is the canonical dotted-string identity (array paths joined - // with '.'); `key.src` is the raw user input and can be a string[]. - obj.key = match.key.id; - } - if (match.idx > -1) { - obj.refIndex = match.idx; - } - matches.push(obj); - }); - return matches; + const matches = []; + result.matches.forEach((match) => { + if (!isDefined(match.indices) || !match.indices.length) return; + const obj = { + indices: match.indices, + value: match.value + }; + if (match.key) obj.key = match.key.id; + if (match.idx > -1) obj.refIndex = match.idx; + matches.push(obj); + }); + return matches; } -function format(results, docs, { - includeMatches = Config.includeMatches, - includeScore = Config.includeScore -} = {}) { - return results.map(result => { - const { - idx - } = result; - const data = { - item: docs[idx], - refIndex: idx - }; - if (includeMatches) data.matches = formatMatches(result); - if (includeScore) data.score = result.score; - return data; - }); +//#endregion +//#region src/core/format.ts +function format(results, docs, { includeMatches = Config.includeMatches, includeScore = Config.includeScore } = {}) { + return results.map((result) => { + const { idx } = result; + const data = { + item: docs[idx], + refIndex: idx + }; + if (includeMatches) data.matches = formatMatches(result); + if (includeScore) data.score = result.score; + return data; + }); } -// Includes \p{M} (Mark) so combining marks stay attached to their base -// letter — without it, scripts like Devanagari and NFD-normalized Latin -// shatter (e.g. 'हिन्दी' → ['ह','न','द'], 'café'.normalize('NFD') → ['cafe']). +//#endregion +//#region src/search/token/analyzer.ts const DEFAULT_TOKEN = /[\p{L}\p{M}\p{N}_]+/gu; -const warned = new WeakSet(); +const warned = /* @__PURE__ */ new WeakSet(); function warnNonGlobal(regex) { - if (!warned.has(regex)) { - warned.add(regex); - console.warn(`[Fuse] tokenize regex ${regex} lacks the global flag; only the ` + `first match per text will be returned. Add the 'g' flag.`); - } + if (!warned.has(regex)) { + warned.add(regex); + console.warn(`[Fuse] tokenize regex ${regex} lacks the global flag; only the first match per text will be returned. Add the 'g' flag.`); + } } function resolveTokenize(tokenize) { - if (typeof tokenize === 'function') { - let validated = false; - return text => { - const result = tokenize(text); - if (!validated) { - validated = true; - if (!Array.isArray(result) || result.some(t => typeof t !== 'string')) { - throw new Error(`[Fuse] tokenize function must return string[]; received ${Array.isArray(result) ? 'array containing non-strings' : typeof result}.`); - } - } - return result; - }; - } - if (tokenize instanceof RegExp) { - if (!tokenize.global) warnNonGlobal(tokenize); - return text => text.match(tokenize) || []; - } - return text => text.match(DEFAULT_TOKEN) || []; + if (typeof tokenize === "function") { + let validated = false; + return (text) => { + const result = tokenize(text); + if (!validated) { + validated = true; + if (!Array.isArray(result) || result.some((t) => typeof t !== "string")) throw new Error(`[Fuse] tokenize function must return string[]; received ${Array.isArray(result) ? "array containing non-strings" : typeof result}.`); + } + return result; + }; + } + if (tokenize instanceof RegExp) { + if (!tokenize.global) warnNonGlobal(tokenize); + return (text) => text.match(tokenize) || []; + } + return (text) => text.match(DEFAULT_TOKEN) || []; } -function createAnalyzer({ - isCaseSensitive = false, - ignoreDiacritics = false, - tokenize -} = {}) { - const tokenizeFn = resolveTokenize(tokenize); - return { - tokenize(text) { - if (!isCaseSensitive) { - text = text.toLowerCase(); - } - if (ignoreDiacritics) { - text = stripDiacritics(text); - } - return tokenizeFn(text); - } - }; +function createAnalyzer({ isCaseSensitive = false, ignoreDiacritics = false, tokenize } = {}) { + const tokenizeFn = resolveTokenize(tokenize); + return { tokenize(text) { + if (!isCaseSensitive) text = text.toLowerCase(); + if (ignoreDiacritics) text = stripDiacritics(text); + return tokenizeFn(text); + } }; } -// `tokenMatch: 'all'` packs per-term coverage into a bitmask. JS bitwise ops -// are 32-bit *signed*, so bit 31 is the sign bit — only bits 0..30 are safe. -// Queries with more than this many terms fall back to a Set (no bit limit). +//#endregion +//#region src/search/token/index.ts const MAX_MASK_TERMS = 31; -class TokenSearch { - // `tokenMatch: 'all'` (AND) coverage. When true, searchIn reports which - // query terms matched each text so the core loop can require record-level - // coverage of every term. Bitmask is the ≤31-term fast path; Set is the - // ≥32-term fallback (JS bitwise ops are 32-bit signed). - - static condition(_, options) { - return options.useTokenSearch; - } - constructor(pattern, options) { - this.options = options; - this.analyzer = createAnalyzer({ - isCaseSensitive: options.isCaseSensitive, - ignoreDiacritics: options.ignoreDiacritics, - tokenize: options.tokenize - }); - const queryTerms = this.analyzer.tokenize(pattern); - const invertedIndex = options._invertedIndex; - const { - df, - fieldCount - } = invertedIndex; - this.termSearchers = []; - this.idfWeights = []; - for (const term of queryTerms) { - this.termSearchers.push(new BitapSearch(term, { - location: options.location, - threshold: options.threshold, - distance: options.distance, - includeMatches: options.includeMatches, - findAllMatches: options.findAllMatches, - minMatchCharLength: options.minMatchCharLength, - isCaseSensitive: options.isCaseSensitive, - ignoreDiacritics: options.ignoreDiacritics, - ignoreLocation: true - })); - const docFreq = df.get(term) || 0; - const idf = Math.log(1 + (fieldCount - docFreq + 0.5) / (docFreq + 0.5)); - this.idfWeights.push(idf); - } - this.combineAll = options.tokenMatch === 'all'; - this.numTerms = this.termSearchers.length; - this.useMask = this.numTerms <= MAX_MASK_TERMS; - } - searchIn(text) { - if (!this.termSearchers.length) { - return { - isMatch: false, - score: 1 - }; - } - const allIndices = []; - let weightedScore = 0; - let maxPossibleScore = 0; - let matchedCount = 0; - - // `tokenMatch: 'all'` coverage for this text (untouched in the default - // 'any' path, so it allocates nothing there). - let matchedMask = 0; - const matchedTerms = this.combineAll && !this.useMask ? new Set() : null; - for (let i = 0; i < this.termSearchers.length; i++) { - const result = this.termSearchers[i].searchIn(text); - const idf = this.idfWeights[i]; - maxPossibleScore += idf; - if (result.isMatch) { - matchedCount++; - weightedScore += idf * (1 - result.score); - if (result.indices) { - allIndices.push(...result.indices); - } - if (this.combineAll) { - if (this.useMask) { - matchedMask |= 1 << i; - } else { - matchedTerms.add(i); - } - } - } - } - if (matchedCount === 0) { - return { - isMatch: false, - score: 1 - }; - } - const normalized = maxPossibleScore > 0 ? 1 - weightedScore / maxPossibleScore : 0; - const searchResult = { - isMatch: true, - score: Math.max(0.001, normalized) - }; - if (this.options.includeMatches && allIndices.length) { - searchResult.indices = mergeIndices(allIndices); - } - - // Report term coverage so the core loop can enforce record-level AND. - if (this.combineAll) { - if (this.useMask) { - searchResult.matchedMask = matchedMask; - } else { - searchResult.matchedTerms = matchedTerms; - } - searchResult.termCount = this.numTerms; - } - return searchResult; - } -} - -// Stats-only inverted index for token search (per Plan 008 Direction B). -// -// The query path consumes only `df` and `fieldCount` (IDF weighting). The -// per-doc maps exist solely to keep `df` and `fieldCount` correct under -// `add` / `remove` / `removeAt`: -// -// docFieldCount[doc] = # distinct fields the doc contributed; subtracted -// from `fieldCount` on remove. -// docTermFieldHits[doc] = Map; each entry decrements `df[term]` by -// that count on remove. -// -// `df` is incremented once per (doc, term, field) at index time. Removing a -// doc decrements `df` by the same count, mirroring the increment exactly. +var TokenSearch = class { + static condition(_, options) { + return options.useTokenSearch; + } + constructor(pattern, options) { + this.options = options; + this.analyzer = createAnalyzer({ + isCaseSensitive: options.isCaseSensitive, + ignoreDiacritics: options.ignoreDiacritics, + tokenize: options.tokenize + }); + const queryTerms = this.analyzer.tokenize(pattern); + const { df, fieldCount } = options._invertedIndex; + this.termSearchers = []; + this.idfWeights = []; + for (const term of queryTerms) { + this.termSearchers.push(new BitapSearch(term, { + location: options.location, + threshold: options.threshold, + distance: options.distance, + includeMatches: options.includeMatches, + findAllMatches: options.findAllMatches, + minMatchCharLength: options.minMatchCharLength, + isCaseSensitive: options.isCaseSensitive, + ignoreDiacritics: options.ignoreDiacritics, + ignoreLocation: true + })); + const docFreq = df.get(term) || 0; + const idf = Math.log(1 + (fieldCount - docFreq + .5) / (docFreq + .5)); + this.idfWeights.push(idf); + } + this.combineAll = options.tokenMatch === "all"; + this.numTerms = this.termSearchers.length; + this.useMask = this.numTerms <= 31; + } + searchIn(text) { + if (!this.termSearchers.length) return { + isMatch: false, + score: 1 + }; + const allIndices = []; + let weightedScore = 0; + let maxPossibleScore = 0; + let matchedCount = 0; + let matchedMask = 0; + const matchedTerms = this.combineAll && !this.useMask ? /* @__PURE__ */ new Set() : null; + for (let i = 0; i < this.termSearchers.length; i++) { + const result = this.termSearchers[i].searchIn(text); + const idf = this.idfWeights[i]; + maxPossibleScore += idf; + if (result.isMatch) { + matchedCount++; + weightedScore += idf * (1 - result.score); + if (result.indices) allIndices.push(...result.indices); + if (this.combineAll) if (this.useMask) matchedMask |= 1 << i; + else matchedTerms.add(i); + } + } + if (matchedCount === 0) return { + isMatch: false, + score: 1 + }; + const normalized = maxPossibleScore > 0 ? 1 - weightedScore / maxPossibleScore : 0; + const searchResult = { + isMatch: true, + score: Math.max(.001, normalized) + }; + if (this.options.includeMatches && allIndices.length) searchResult.indices = mergeIndices(allIndices); + if (this.combineAll) { + if (this.useMask) searchResult.matchedMask = matchedMask; + else searchResult.matchedTerms = matchedTerms; + searchResult.termCount = this.numTerms; + } + return searchResult; + } +}; +//#endregion +//#region src/search/token/InvertedIndex.ts function addField(index, text, docIdx, analyzer) { - const tokens = analyzer.tokenize(text); - if (!tokens.length) return; - index.fieldCount++; - index.docFieldCount.set(docIdx, (index.docFieldCount.get(docIdx) || 0) + 1); - - // We count each (doc, term, field) once — repeated occurrences within the - // same field don't multiply df. - const distinctTerms = new Set(tokens); - let perDocTerms = index.docTermFieldHits.get(docIdx); - if (!perDocTerms) { - perDocTerms = new Map(); - index.docTermFieldHits.set(docIdx, perDocTerms); - } - for (const term of distinctTerms) { - perDocTerms.set(term, (perDocTerms.get(term) || 0) + 1); - index.df.set(term, (index.df.get(term) || 0) + 1); - } + const tokens = analyzer.tokenize(text); + if (!tokens.length) return; + index.fieldCount++; + index.docFieldCount.set(docIdx, (index.docFieldCount.get(docIdx) || 0) + 1); + const distinctTerms = new Set(tokens); + let perDocTerms = index.docTermFieldHits.get(docIdx); + if (!perDocTerms) { + perDocTerms = /* @__PURE__ */ new Map(); + index.docTermFieldHits.set(docIdx, perDocTerms); + } + for (const term of distinctTerms) { + perDocTerms.set(term, (perDocTerms.get(term) || 0) + 1); + index.df.set(term, (index.df.get(term) || 0) + 1); + } } function ingestRecord(index, record, keyCount, analyzer) { - const { - i: docIdx, - v, - $: fields - } = record; - if (v !== undefined) { - addField(index, v, docIdx, analyzer); - return; - } - if (!fields) return; - for (let keyIdx = 0; keyIdx < keyCount; keyIdx++) { - const value = fields[keyIdx]; - if (!value) continue; - if (Array.isArray(value)) { - for (const sub of value) addField(index, sub.v, docIdx, analyzer); - } else { - addField(index, value.v, docIdx, analyzer); - } - } + const { i: docIdx, v, $: fields } = record; + if (v !== void 0) { + addField(index, v, docIdx, analyzer); + return; + } + if (!fields) return; + for (let keyIdx = 0; keyIdx < keyCount; keyIdx++) { + const value = fields[keyIdx]; + if (!value) continue; + if (Array.isArray(value)) for (const sub of value) addField(index, sub.v, docIdx, analyzer); + else addField(index, value.v, docIdx, analyzer); + } } function buildInvertedIndex(records, keyCount, analyzer) { - const index = { - fieldCount: 0, - df: new Map(), - docFieldCount: new Map(), - docTermFieldHits: new Map() - }; - for (const record of records) { - ingestRecord(index, record, keyCount, analyzer); - } - return index; + const index = { + fieldCount: 0, + df: /* @__PURE__ */ new Map(), + docFieldCount: /* @__PURE__ */ new Map(), + docTermFieldHits: /* @__PURE__ */ new Map() + }; + for (const record of records) ingestRecord(index, record, keyCount, analyzer); + return index; } function addToInvertedIndex(index, record, keyCount, analyzer) { - ingestRecord(index, record, keyCount, analyzer); + ingestRecord(index, record, keyCount, analyzer); } function removeFromInvertedIndex(index, docIdx) { - const fieldCount = index.docFieldCount.get(docIdx); - if (fieldCount === undefined) return; - index.fieldCount -= fieldCount; - index.docFieldCount.delete(docIdx); - const perDocTerms = index.docTermFieldHits.get(docIdx); - if (!perDocTerms) return; - for (const [term, hits] of perDocTerms) { - const next = (index.df.get(term) || 0) - hits; - if (next <= 0) { - index.df.delete(term); - } else { - index.df.set(term, next); - } - } - index.docTermFieldHits.delete(docIdx); + const fieldCount = index.docFieldCount.get(docIdx); + if (fieldCount === void 0) return; + index.fieldCount -= fieldCount; + index.docFieldCount.delete(docIdx); + const perDocTerms = index.docTermFieldHits.get(docIdx); + if (!perDocTerms) return; + for (const [term, hits] of perDocTerms) { + const next = (index.df.get(term) || 0) - hits; + if (next <= 0) index.df.delete(term); + else index.df.set(term, next); + } + index.docTermFieldHits.delete(docIdx); } - -// Removes the given docIdx entries and renumbers the remaining per-doc maps -// so they stay in sync with FuseIndex's contiguous renumbering on remove. function removeAndShiftInvertedIndex(index, removedIndices) { - if (removedIndices.length === 0) return; - - // De-dup and sort so the shift computation is O(log k) per lookup. - const sorted = Array.from(new Set(removedIndices)).sort((a, b) => a - b); - for (const idx of sorted) { - removeFromInvertedIndex(index, idx); - } - - // For any surviving oldIdx, its new idx is oldIdx minus the number of - // removed indices strictly less than oldIdx. - const shift = oldIdx => { - let lo = 0; - let hi = sorted.length; - while (lo < hi) { - const mid = lo + hi >>> 1; - if (sorted[mid] < oldIdx) lo = mid + 1;else hi = mid; - } - return oldIdx - lo; - }; - const firstRemoved = sorted[0]; - const shiftedDocFieldCount = new Map(); - for (const [oldKey, count] of index.docFieldCount) { - shiftedDocFieldCount.set(oldKey > firstRemoved ? shift(oldKey) : oldKey, count); - } - index.docFieldCount = shiftedDocFieldCount; - const shiftedDocTermFieldHits = new Map(); - for (const [oldKey, terms] of index.docTermFieldHits) { - shiftedDocTermFieldHits.set(oldKey > firstRemoved ? shift(oldKey) : oldKey, terms); - } - index.docTermFieldHits = shiftedDocTermFieldHits; + if (removedIndices.length === 0) return; + const sorted = Array.from(new Set(removedIndices)).sort((a, b) => a - b); + for (const idx of sorted) removeFromInvertedIndex(index, idx); + const shift = (oldIdx) => { + let lo = 0; + let hi = sorted.length; + while (lo < hi) { + const mid = lo + hi >>> 1; + if (sorted[mid] < oldIdx) lo = mid + 1; + else hi = mid; + } + return oldIdx - lo; + }; + const firstRemoved = sorted[0]; + const shiftedDocFieldCount = /* @__PURE__ */ new Map(); + for (const [oldKey, count] of index.docFieldCount) shiftedDocFieldCount.set(oldKey > firstRemoved ? shift(oldKey) : oldKey, count); + index.docFieldCount = shiftedDocFieldCount; + const shiftedDocTermFieldHits = /* @__PURE__ */ new Map(); + for (const [oldKey, terms] of index.docTermFieldHits) shiftedDocTermFieldHits.set(oldKey > firstRemoved ? shift(oldKey) : oldKey, terms); + index.docTermFieldHits = shiftedDocTermFieldHits; } -class Fuse { - // Statics are assigned in entry.ts - - constructor(docs, options, index) { - this.options = { - ...Config, - ...options - }; - if (this.options.useExtendedSearch && false) ; - if (this.options.useTokenSearch && false) ; - this._keyStore = new KeyStore(this.options.keys); - this._docs = docs; - this._myIndex = null; - this._invertedIndex = null; - this.setCollection(docs, index); - this._lastQuery = null; - this._lastSearcher = null; - } - _getSearcher(query) { - if (this._lastQuery === query) { - return this._lastSearcher; - } - const opts = this._invertedIndex ? { - ...this.options, - _invertedIndex: this._invertedIndex - } : this.options; - const searcher = createSearcher(query, opts); - this._lastQuery = query; - this._lastSearcher = searcher; - return searcher; - } - setCollection(docs, index) { - this._docs = docs; - if (index && !(index instanceof FuseIndex)) { - throw new Error(INCORRECT_INDEX_TYPE); - } - this._myIndex = index || createIndex(this.options.keys, this._docs, { - getFn: this.options.getFn, - fieldNormWeight: this.options.fieldNormWeight - }); - if (this.options.useTokenSearch) { - const analyzer = createAnalyzer({ - isCaseSensitive: this.options.isCaseSensitive, - ignoreDiacritics: this.options.ignoreDiacritics, - tokenize: this.options.tokenize - }); - this._invertedIndex = buildInvertedIndex(this._myIndex.records, this._myIndex.keys.length, analyzer); - } - this._invalidateSearcherCache(); - } - add(doc) { - if (!isDefined(doc)) { - return; - } - this._docs.push(doc); - const record = this._myIndex.add(doc, this._docs.length - 1); - - // Skip inverted-index bookkeeping when no record was appended (blank - // strings produce null). The previous code read `records[records.length-1]` - // unconditionally, which would re-ingest the previous doc on `add("")`. - if (this._invertedIndex && record) { - const analyzer = createAnalyzer({ - isCaseSensitive: this.options.isCaseSensitive, - ignoreDiacritics: this.options.ignoreDiacritics, - tokenize: this.options.tokenize - }); - addToInvertedIndex(this._invertedIndex, record, this._myIndex.keys.length, analyzer); - } - this._invalidateSearcherCache(); - } - remove(predicate = () => false) { - const results = []; - const indicesToRemove = []; - for (let i = 0, len = this._docs.length; i < len; i += 1) { - if (predicate(this._docs[i], i)) { - results.push(this._docs[i]); - indicesToRemove.push(i); - } - } - if (indicesToRemove.length) { - if (this._invertedIndex) { - removeAndShiftInvertedIndex(this._invertedIndex, indicesToRemove); - } - - // Filter docs in a single pass instead of reverse-splicing - const toRemove = new Set(indicesToRemove); - this._docs = this._docs.filter((_, i) => !toRemove.has(i)); - this._myIndex.removeAll(indicesToRemove); - this._invalidateSearcherCache(); - } - return results; - } - removeAt(idx) { - // Validate before any mutation. The previous code spliced `_docs` first - // and let FuseIndex.removeAt throw afterward — partial-state on invalid - // input. Atomic now. - if (!Number.isInteger(idx) || idx < 0 || idx >= this._docs.length) { - throw new Error(INVALID_DOC_INDEX); - } - if (this._invertedIndex) { - removeAndShiftInvertedIndex(this._invertedIndex, [idx]); - } - const doc = this._docs.splice(idx, 1)[0]; - this._myIndex.removeAt(idx); - this._invalidateSearcherCache(); - return doc; - } - _invalidateSearcherCache() { - this._lastQuery = null; - this._lastSearcher = null; - } - getIndex() { - return this._myIndex; - } - search(query, options) { - const { - limit = -1 - } = options || {}; - const { - includeMatches, - includeScore, - shouldSort, - sortFn, - ignoreFieldNorm - } = this.options; - - // Empty string query returns all docs (useful for search UIs) - if (isString(query) && !query.trim()) { - let docs = this._docs.map((item, idx) => ({ - item, - refIndex: idx - })); - if (isNumber(limit) && limit > -1) { - docs = docs.slice(0, limit); - } - return docs; - } - const useHeap = isNumber(limit) && limit > 0 && isString(query); - let results; - if (useHeap) { - const heap = new MaxHeap(limit); - if (isString(this._docs[0])) { - this._searchStringList(query, { - heap, - ignoreFieldNorm - }); - } else { - this._searchObjectList(query, { - heap, - ignoreFieldNorm - }); - } - results = heap.extractSorted(sortFn); - } else { - results = isString(query) ? isString(this._docs[0]) ? this._searchStringList(query) : this._searchObjectList(query) : this._searchLogical(query); - computeScore(results, { - ignoreFieldNorm - }); - if (shouldSort) { - results.sort(sortFn); - } - if (isNumber(limit) && limit > -1) { - results = results.slice(0, limit); - } - } - return format(results, this._docs, { - includeMatches, - includeScore - }); - } - _searchStringList(query, { - heap, - ignoreFieldNorm - } = {}) { - const searcher = this._getSearcher(query); - const requireAllTokens = this.options.useTokenSearch && this.options.tokenMatch === 'all'; - const { - records - } = this._myIndex; - const results = heap ? null : []; - - // Iterate over every string in the index - records.forEach(({ - v: text, - i: idx, - n: norm - }) => { - if (!isDefined(text)) { - return; - } - const searchResult = searcher.searchIn(text); - if (searchResult.isMatch) { - const match = { - score: searchResult.score, - value: text, - norm: norm, - indices: searchResult.indices - }; - if (requireAllTokens) { - match.matchedMask = searchResult.matchedMask; - match.matchedTerms = searchResult.matchedTerms; - match.termCount = searchResult.termCount; - } - const matches = [match]; - - // Record-level AND gate (token search `tokenMatch: 'all'`), applied - // before heap insertion so `limit` returns the same top-N as unlimited. - if (!requireAllTokens || this._coversAllTokens(matches)) { - const result = { - item: text, - idx, - matches - }; - if (heap) { - result.score = computeScoreSingle(result.matches, { - ignoreFieldNorm - }); - if (heap.shouldInsert(result.score)) { - heap.insert(result); - } - } else { - results.push(result); - } - } - } - }); - return results; - } - _searchLogical(query) { - const expression = parse(query, this.options); - const evaluate = (node, item, idx) => { - if (!('children' in node)) { - const { - keyId, - searcher - } = node; - let matches; - if (keyId === null) { - // Keyless entry: search across all keys - matches = []; - this._myIndex.keys.forEach((key, keyIndex) => { - matches.push(...this._findMatches({ - key, - value: item[keyIndex], - searcher: searcher - })); - }); - } else { - matches = this._findMatches({ - key: this._keyStore.get(keyId), - value: this._myIndex.getValueForItemAtKeyId(item, keyId), - searcher: searcher - }); - } - if (matches && matches.length) { - return [{ - idx, - item, - matches - }]; - } - return []; - } - const { - children, - operator - } = node; - const res = []; - for (let i = 0, len = children.length; i < len; i += 1) { - const child = children[i]; - const result = evaluate(child, item, idx); - if (result.length) { - res.push(...result); - } else if (operator === LogicalOperator.AND) { - return []; - } - } - return res; - }; - const records = this._myIndex.records; - const resultMap = new Map(); - const results = []; - records.forEach(({ - $: item, - i: idx - }) => { - if (isDefined(item)) { - const expResults = evaluate(expression, item, idx); - if (expResults.length) { - // Dedupe when adding - if (!resultMap.has(idx)) { - resultMap.set(idx, { - idx, - item, - matches: [] - }); - results.push(resultMap.get(idx)); - } - expResults.forEach(({ - matches - }) => { - resultMap.get(idx).matches.push(...matches); - }); - } - } - }); - return results; - } - - // When a search involves inverse patterns (e.g. !Syrup), the aggregation - // across keys switches from "ANY key matches" to "ALL keys must match." - // This is signaled by hasInverse on the SearchResult from ExtendedSearch. - // - // For mixed patterns like "^hello !Syrup", a key failure is ambiguous — - // it could be the positive or inverse term that failed. In that case we - // conservatively exclude the item, which is strictly better than the old - // behavior of including it. See: https://github.com/krisk/Fuse/issues/712 - _searchObjectList(query, { - heap, - ignoreFieldNorm - } = {}) { - const searcher = this._getSearcher(query); - const requireAllTokens = this.options.useTokenSearch && this.options.tokenMatch === 'all'; - const { - keys, - records - } = this._myIndex; - const results = heap ? null : []; - - // List is Array - records.forEach(({ - $: item, - i: idx - }) => { - if (!isDefined(item)) { - return; - } - const matches = []; - let anyKeyFailed = false; - let hasInverse = false; - - // Iterate over every key (i.e, path), and fetch the value at that key - keys.forEach((key, keyIndex) => { - const keyMatches = this._findMatches({ - key, - value: item[keyIndex], - searcher - }); - if (keyMatches.length) { - matches.push(...keyMatches); - if (keyMatches[0].hasInverse) { - hasInverse = true; - } - } else { - anyKeyFailed = true; - } - }); - - // If the search involves inverse patterns, ALL keys must match - if (hasInverse && anyKeyFailed) { - return; - } - - // Record-level AND gate (token search `tokenMatch: 'all'`): every query - // term must be covered across the record's field/array-element matches. - // Applied before heap insertion so `limit` returns the same top-N. - if (matches.length && (!requireAllTokens || this._coversAllTokens(matches))) { - const result = { - idx, - item, - matches - }; - if (heap) { - result.score = computeScoreSingle(result.matches, { - ignoreFieldNorm - }); - if (heap.shouldInsert(result.score)) { - heap.insert(result); - } - } else { - results.push(result); - } - } - }); - return results; - } - _findMatches({ - key, - value, - searcher - }) { - if (!isDefined(value)) { - return []; - } - const matches = []; - if (isArray(value)) { - value.forEach(({ - v: text, - i: idx, - n: norm - }) => { - if (!isDefined(text)) { - return; - } - const searchResult = searcher.searchIn(text); - if (searchResult.isMatch) { - const match = { - score: searchResult.score, - key, - value: text, - idx, - norm, - indices: searchResult.indices, - hasInverse: searchResult.hasInverse - }; - // Carry token-search AND coverage only when present, so the default - // (non-token / 'any') MatchScore keeps its original object shape. - if (searchResult.termCount !== undefined) { - match.matchedMask = searchResult.matchedMask; - match.matchedTerms = searchResult.matchedTerms; - match.termCount = searchResult.termCount; - } - matches.push(match); - } - }); - } else { - const { - v: text, - n: norm - } = value; - const searchResult = searcher.searchIn(text); - if (searchResult.isMatch) { - const match = { - score: searchResult.score, - key, - value: text, - norm, - indices: searchResult.indices, - hasInverse: searchResult.hasInverse - }; - if (searchResult.termCount !== undefined) { - match.matchedMask = searchResult.matchedMask; - match.matchedTerms = searchResult.matchedTerms; - match.termCount = searchResult.termCount; - } - matches.push(match); - } - } - return matches; - } - - // Record-level AND gate for token search (`tokenMatch: 'all'`). Returns true - // unless the matched terms across ALL of a record's field/array-element - // matches fail to cover every query term. `termCount` is only set by - // TokenSearch in 'all' mode, so non-token / 'any' searches always pass. - _coversAllTokens(matches) { - const termCount = matches.length ? matches[0].termCount : undefined; - if (termCount === undefined) { - return true; - } - if (termCount <= MAX_MASK_TERMS) { - let coverage = 0; - for (let i = 0; i < matches.length; i++) { - coverage |= matches[i].matchedMask || 0; - } - return coverage === 2 ** termCount - 1; - } - const coverage = new Set(); - for (let i = 0; i < matches.length; i++) { - const terms = matches[i].matchedTerms; - if (terms) { - for (const t of terms) { - coverage.add(t); - } - } - } - return coverage.size === termCount; - } -} +//#endregion +//#region src/core/index.ts +var Fuse = class { + constructor(docs, options, index) { + this.options = { + ...Config, + ...options + }; + if (this.options.useExtendedSearch && false); + if (this.options.useTokenSearch && false); + this._keyStore = new KeyStore(this.options.keys); + this._docs = docs; + this._myIndex = null; + this._invertedIndex = null; + this.setCollection(docs, index); + this._lastQuery = null; + this._lastSearcher = null; + } + _getSearcher(query) { + if (this._lastQuery === query) return this._lastSearcher; + const searcher = createSearcher(query, this._invertedIndex ? { + ...this.options, + _invertedIndex: this._invertedIndex + } : this.options); + this._lastQuery = query; + this._lastSearcher = searcher; + return searcher; + } + setCollection(docs, index) { + this._docs = docs; + if (index && !(index instanceof FuseIndex)) throw new Error(INCORRECT_INDEX_TYPE); + this._myIndex = index || createIndex(this.options.keys, this._docs, { + getFn: this.options.getFn, + fieldNormWeight: this.options.fieldNormWeight + }); + if (this.options.useTokenSearch) { + const analyzer = createAnalyzer({ + isCaseSensitive: this.options.isCaseSensitive, + ignoreDiacritics: this.options.ignoreDiacritics, + tokenize: this.options.tokenize + }); + this._invertedIndex = buildInvertedIndex(this._myIndex.records, this._myIndex.keys.length, analyzer); + } + this._invalidateSearcherCache(); + } + add(doc) { + if (!isDefined(doc)) return; + this._docs.push(doc); + const record = this._myIndex.add(doc, this._docs.length - 1); + if (this._invertedIndex && record) { + const analyzer = createAnalyzer({ + isCaseSensitive: this.options.isCaseSensitive, + ignoreDiacritics: this.options.ignoreDiacritics, + tokenize: this.options.tokenize + }); + addToInvertedIndex(this._invertedIndex, record, this._myIndex.keys.length, analyzer); + } + this._invalidateSearcherCache(); + } + remove(predicate = () => false) { + const results = []; + const indicesToRemove = []; + for (let i = 0, len = this._docs.length; i < len; i += 1) if (predicate(this._docs[i], i)) { + results.push(this._docs[i]); + indicesToRemove.push(i); + } + if (indicesToRemove.length) { + if (this._invertedIndex) removeAndShiftInvertedIndex(this._invertedIndex, indicesToRemove); + const toRemove = new Set(indicesToRemove); + this._docs = this._docs.filter((_, i) => !toRemove.has(i)); + this._myIndex.removeAll(indicesToRemove); + this._invalidateSearcherCache(); + } + return results; + } + removeAt(idx) { + if (!Number.isInteger(idx) || idx < 0 || idx >= this._docs.length) throw new Error(INVALID_DOC_INDEX); + if (this._invertedIndex) removeAndShiftInvertedIndex(this._invertedIndex, [idx]); + const doc = this._docs.splice(idx, 1)[0]; + this._myIndex.removeAt(idx); + this._invalidateSearcherCache(); + return doc; + } + _invalidateSearcherCache() { + this._lastQuery = null; + this._lastSearcher = null; + } + getIndex() { + return this._myIndex; + } + search(query, options) { + const { limit = -1 } = options || {}; + const { includeMatches, includeScore, shouldSort, sortFn, ignoreFieldNorm } = this.options; + if (isString(query) && !query.trim()) { + let docs = this._docs.map((item, idx) => ({ + item, + refIndex: idx + })); + if (isNumber(limit) && limit > -1) docs = docs.slice(0, limit); + return docs; + } + const useHeap = isNumber(limit) && limit > 0 && isString(query); + let results; + if (useHeap) { + const heap = new MaxHeap(limit); + if (isString(this._docs[0])) this._searchStringList(query, { + heap, + ignoreFieldNorm + }); + else this._searchObjectList(query, { + heap, + ignoreFieldNorm + }); + results = heap.extractSorted(sortFn); + } else { + results = isString(query) ? isString(this._docs[0]) ? this._searchStringList(query) : this._searchObjectList(query) : this._searchLogical(query); + computeScore(results, { ignoreFieldNorm }); + if (shouldSort) results.sort(sortFn); + if (isNumber(limit) && limit > -1) results = results.slice(0, limit); + } + return format(results, this._docs, { + includeMatches, + includeScore + }); + } + _searchStringList(query, { heap, ignoreFieldNorm } = {}) { + const searcher = this._getSearcher(query); + const requireAllTokens = this.options.useTokenSearch && this.options.tokenMatch === "all"; + const { records } = this._myIndex; + const results = heap ? null : []; + records.forEach(({ v: text, i: idx, n: norm }) => { + if (!isDefined(text)) return; + const searchResult = searcher.searchIn(text); + if (searchResult.isMatch) { + const match = { + score: searchResult.score, + value: text, + norm, + indices: searchResult.indices + }; + if (requireAllTokens) { + match.matchedMask = searchResult.matchedMask; + match.matchedTerms = searchResult.matchedTerms; + match.termCount = searchResult.termCount; + } + const matches = [match]; + if (!requireAllTokens || this._coversAllTokens(matches)) { + const result = { + item: text, + idx, + matches + }; + if (heap) { + result.score = computeScoreSingle(result.matches, { ignoreFieldNorm }); + if (heap.shouldInsert(result.score)) heap.insert(result); + } else results.push(result); + } + } + }); + return results; + } + _searchLogical(query) { + const expression = parse(query, this.options); + const evaluate = (node, item, idx) => { + if (!("children" in node)) { + const { keyId, searcher } = node; + let matches; + if (keyId === null) { + matches = []; + this._myIndex.keys.forEach((key, keyIndex) => { + matches.push(...this._findMatches({ + key, + value: item[keyIndex], + searcher + })); + }); + } else matches = this._findMatches({ + key: this._keyStore.get(keyId), + value: this._myIndex.getValueForItemAtKeyId(item, keyId), + searcher + }); + if (matches && matches.length) return [{ + idx, + item, + matches + }]; + return []; + } + const { children, operator } = node; + const res = []; + for (let i = 0, len = children.length; i < len; i += 1) { + const child = children[i]; + const result = evaluate(child, item, idx); + if (result.length) res.push(...result); + else if (operator === LogicalOperator.AND) return []; + } + return res; + }; + const records = this._myIndex.records; + const resultMap = /* @__PURE__ */ new Map(); + const results = []; + records.forEach(({ $: item, i: idx }) => { + if (isDefined(item)) { + const expResults = evaluate(expression, item, idx); + if (expResults.length) { + if (!resultMap.has(idx)) { + resultMap.set(idx, { + idx, + item, + matches: [] + }); + results.push(resultMap.get(idx)); + } + expResults.forEach(({ matches }) => { + resultMap.get(idx).matches.push(...matches); + }); + } + } + }); + return results; + } + _searchObjectList(query, { heap, ignoreFieldNorm } = {}) { + const searcher = this._getSearcher(query); + const requireAllTokens = this.options.useTokenSearch && this.options.tokenMatch === "all"; + const { keys, records } = this._myIndex; + const results = heap ? null : []; + records.forEach(({ $: item, i: idx }) => { + if (!isDefined(item)) return; + const matches = []; + let anyKeyFailed = false; + let hasInverse = false; + keys.forEach((key, keyIndex) => { + const keyMatches = this._findMatches({ + key, + value: item[keyIndex], + searcher + }); + if (keyMatches.length) { + matches.push(...keyMatches); + if (keyMatches[0].hasInverse) hasInverse = true; + } else anyKeyFailed = true; + }); + if (hasInverse && anyKeyFailed) return; + if (matches.length && (!requireAllTokens || this._coversAllTokens(matches))) { + const result = { + idx, + item, + matches + }; + if (heap) { + result.score = computeScoreSingle(result.matches, { ignoreFieldNorm }); + if (heap.shouldInsert(result.score)) heap.insert(result); + } else results.push(result); + } + }); + return results; + } + _findMatches({ key, value, searcher }) { + if (!isDefined(value)) return []; + const matches = []; + if (isArray(value)) value.forEach(({ v: text, i: idx, n: norm }) => { + if (!isDefined(text)) return; + const searchResult = searcher.searchIn(text); + if (searchResult.isMatch) { + const match = { + score: searchResult.score, + key, + value: text, + idx, + norm, + indices: searchResult.indices, + hasInverse: searchResult.hasInverse + }; + if (searchResult.termCount !== void 0) { + match.matchedMask = searchResult.matchedMask; + match.matchedTerms = searchResult.matchedTerms; + match.termCount = searchResult.termCount; + } + matches.push(match); + } + }); + else { + const { v: text, n: norm } = value; + const searchResult = searcher.searchIn(text); + if (searchResult.isMatch) { + const match = { + score: searchResult.score, + key, + value: text, + norm, + indices: searchResult.indices, + hasInverse: searchResult.hasInverse + }; + if (searchResult.termCount !== void 0) { + match.matchedMask = searchResult.matchedMask; + match.matchedTerms = searchResult.matchedTerms; + match.termCount = searchResult.termCount; + } + matches.push(match); + } + } + return matches; + } + _coversAllTokens(matches) { + const termCount = matches.length ? matches[0].termCount : void 0; + if (termCount === void 0) return true; + if (termCount <= 31) { + let coverage = 0; + for (let i = 0; i < matches.length; i++) coverage |= matches[i].matchedMask || 0; + return coverage === 2 ** termCount - 1; + } + const coverage = /* @__PURE__ */ new Set(); + for (let i = 0; i < matches.length; i++) { + const terms = matches[i].matchedTerms; + if (terms) for (const t of terms) coverage.add(t); + } + return coverage.size === termCount; + } +}; -Fuse.version = '7.4.1'; +//#endregion +//#region src/entry.ts +Fuse.version = "7.4.1"; Fuse.createIndex = createIndex; Fuse.parseIndex = parseIndex; Fuse.config = Config; -Fuse.match = function (pattern, text, options) { - // Token search needs corpus statistics (df, fieldCount) that a one-off - // string comparison can't provide. Reject it here so the contract is the - // same in the full and basic builds — without this guard, the full build - // crashes with an opaque TypeError and the basic build silently falls back - // to fuzzy matching. - if (options && options.useTokenSearch) { - throw new Error(FUSE_MATCH_TOKEN_SEARCH_UNSUPPORTED); - } - const searcher = createSearcher(pattern, { - ...Config, - ...options - }); - return searcher.searchIn(text); +Fuse.match = function(pattern, text, options) { + if (options && options.useTokenSearch) throw new Error(FUSE_MATCH_TOKEN_SEARCH_UNSUPPORTED); + return createSearcher(pattern, { + ...Config, + ...options + }).searchIn(text); }; -{ - Fuse.parseQuery = parse; -} -{ - register(ExtendedSearch); -} -{ - register(TokenSearch); -} -Fuse.use = function (...plugins) { - plugins.forEach(plugin => register(plugin)); +Fuse.parseQuery = parse; +register(ExtendedSearch); +register(TokenSearch); +Fuse.use = function(...plugins) { + plugins.forEach((plugin) => register(plugin)); }; +var entry_default = Fuse; -// Re-export public types - -export { Fuse as default }; +//#endregion +export { entry_default as default }; \ No newline at end of file diff --git a/dist/fuse.worker.min.mjs b/dist/fuse.worker.min.mjs index e203aa1c5..d876db139 100644 --- a/dist/fuse.worker.min.mjs +++ b/dist/fuse.worker.min.mjs @@ -6,4 +6,4 @@ * * http://www.apache.org/licenses/LICENSE-2.0 */ -function e(e){return Array.isArray?Array.isArray(e):"[object Array]"===u(e)}function t(e){return null==e?"":function(e){if("string"==typeof e)return e;if("bigint"==typeof e)return e.toString();const t=e+"";return"0"==t&&1/e==-1/0?"-0":t}(e)}function s(e){return"string"==typeof e}function i(e){return"number"==typeof e}function n(e){return!0===e||!1===e||function(e){return r(e)&&null!==e}(e)&&"[object Boolean]"==u(e)}function r(e){return"object"==typeof e}function o(e){return null!=e}function c(e){return!e.trim().length}function u(e){return null==e?void 0===e?"[object Undefined]":"[object Null]":Object.prototype.toString.call(e)}const h="Invalid doc index: must be a non-negative integer within the bounds of the docs array",a=Object.prototype.hasOwnProperty;class l{constructor(e){this._keys=[],this._keyMap={};let t=0;e.forEach(e=>{const s=d(e);this._keys.push(s),this._keyMap[s.id]=s,t+=s.weight}),this._keys.forEach(e=>{e.weight/=t})}get(e){return this._keyMap[e]}keys(){return this._keys}toJSON(){return JSON.stringify(this._keys)}}function d(t){let i=null,n=null,r=null,o=1,c=null;if(s(t)||e(t))r=t,i=f(t),n=g(t);else{if(!a.call(t,"name"))throw new Error((e=>`Missing ${e} property in key`)("name"));const e=t.name;if(r=e,a.call(t,"weight")&&void 0!==t.weight&&(o=t.weight,o<=0))throw new Error((e=>`Property 'weight' in key '${e}' must be a positive integer`)(g(e)));i=f(e),n=g(e),c=t.getFn??null}return{path:i,id:n,weight:o,src:r,getFn:c}}function f(t){return e(t)?t:t.split(".")}function g(t){return e(t)?t.join("."):t}const m={useExtendedSearch:!1,useTokenSearch:!1,tokenize:void 0,tokenMatch:"any",getFn:function(r,c){const u=[];let h=!1;const a=(r,c,l,d)=>{if(o(r))if(c[l]){const f=r[c[l]];if(!o(f))return;if(l===c.length-1&&(s(f)||i(f)||n(f)||"bigint"==typeof f))u.push(void 0!==d?{v:t(f),i:d}:t(f));else if(e(f)){h=!0;for(let e=0,t=f.length;ee.score===t.score?e.idx{this._keysMap[e.id]=t})}create(){if(this.isCreated||!this.docs.length)return;this.isCreated=!0;const e=this.docs.length;this.records=new Array(e);let t=0;if(s(this.docs[0]))for(let s=0;se&&(this.records[t].i-=1)}removeAll(e){const t=new Set;for(const s of e)Number.isInteger(s)&&s>=0&&t.add(s);if(0===t.size)return;this.records=this.records.filter(e=>!t.has(e.i));const s=Array.from(t).sort((e,t)=>e-t);for(const e of this.records){let t=0,i=s.length;for(;t>>1;s[n]t),records:this.records}}}function C(e,t,{getFn:s=p.getFn,fieldNormWeight:i=p.fieldNormWeight}={}){const n=new A({getFn:s,fieldNormWeight:i});return n.setKeys(e.map(d)),n.setSources(t),n.create(),n}const M=32;function y(e,t,s,{location:i=p.location,distance:n=p.distance,threshold:r=p.threshold,findAllMatches:o=p.findAllMatches,minMatchCharLength:c=p.minMatchCharLength,includeMatches:u=p.includeMatches,ignoreLocation:h=p.ignoreLocation}={}){if(t.length>M)throw new Error(`Pattern length exceeds max of ${M}.`);const a=t.length,l=e.length,d=Math.max(0,Math.min(i,l));let f=r,g=d;const m=(e,t)=>{const s=e/a;if(h)return s;const i=Math.abs(d-t);return n?s+i/n:i?1:s},A=c>1||u,C=A?Array(l):[];let y;for(;(y=e.indexOf(t,g))>-1;){const e=m(0,y);if(f=Math.min(e,f),g=y+a,A){let e=0;for(;e=r;i-=1){const n=i-1,o=s[e[n]];if(u[i]=(u[i+1]<<1|1)&o,t&&(u[i]|=(F[i+1]|F[i])<<1|1|F[i+1]),u[i]&D&&(x=m(t,n),x<=f)){if(f=x,g=n,v=t,g<=d)break;r=Math.max(1,2*d-g)}}if(m(t+1,d)>f)break;F=u}if(A&&g>=0){const t=Math.min(l-1,g+a-1+v);for(let i=g;i<=t;i+=1)s[e[i]]&&(C[i]=1)}const _={isMatch:g>=0,score:Math.max(.001,x)};if(A){const e=function(e=[],t=p.minMatchCharLength){const s=[];let i=-1,n=-1,r=0;for(let o=e.length;r=t&&s.push([i,n]),i=-1)}return e[r-1]&&r-i>=t&&s.push([i,r-1]),s}(C,c);e.length?u&&(_.indices=e):_.isMatch=!1}return _}function F(e){const t={};for(let s=0,i=e.length;se[0]-t[0]||e[1]-t[1]);const t=[e[0]];for(let s=1,i=e.length;se.normalize("NFD").replace(/[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]/g,"").replace(k,e=>v[e]):e=>e;class _{constructor(e,{location:t=p.location,threshold:s=p.threshold,distance:i=p.distance,includeMatches:n=p.includeMatches,findAllMatches:r=p.findAllMatches,minMatchCharLength:o=p.minMatchCharLength,isCaseSensitive:c=p.isCaseSensitive,ignoreDiacritics:u=p.ignoreDiacritics,ignoreLocation:h=p.ignoreLocation}={}){if(this.options={location:t,threshold:s,distance:i,includeMatches:n,findAllMatches:r,minMatchCharLength:o,isCaseSensitive:c,ignoreDiacritics:u,ignoreLocation:h},e=c?e:e.toLowerCase(),e=u?D(e):e,this.pattern=e,this.chunks=[],!this.pattern.length)return;const a=(e,t)=>{this.chunks.push({pattern:e,alphabet:F(e),startIndex:t})},l=this.pattern.length;if(l>M){let e=0;const t=l%M,s=l-t;for(;e{const{isMatch:g,score:m,indices:p}=y(e,t,s,{location:n+f,distance:r,threshold:o,findAllMatches:c,minMatchCharLength:u,includeMatches:i,ignoreLocation:h});g&&(d=!0),l+=m,g&&p&&a.push(...p)});const f={isMatch:d,score:d?l/this.chunks.length:1};return d&&i&&(f.indices=x(a)),f}}const E=new Set(["fuzzy","include"]);function B(e){return e.startsWith("inverse")}const S=[{type:"exact",multiRegex:/^="(.*)"$/,singleRegex:/^=(.*)$/,create:e=>({type:"exact",search(t){const s=t===e;return{isMatch:s,score:s?0:1,indices:[0,e.length-1]}}})},{type:"include",multiRegex:/^'"(.*)"$/,singleRegex:/^'(.*)$/,create:e=>({type:"include",search(t){let s,i=0;const n=[],r=e.length;for(;(s=t.indexOf(e,i))>-1;)i=s+r,n.push([s,i-1]);const o=!!n.length;return{isMatch:o,score:o?0:1,indices:n}}})},{type:"prefix-exact",multiRegex:/^\^"(.*)"$/,singleRegex:/^\^(.*)$/,create:e=>({type:"prefix-exact",search(t){const s=t.startsWith(e);return{isMatch:s,score:s?0:1,indices:[0,e.length-1]}}})},{type:"inverse-prefix-exact",multiRegex:/^!\^"(.*)"$/,singleRegex:/^!\^(.*)$/,create:e=>({type:"inverse-prefix-exact",search(t){const s=!t.startsWith(e);return{isMatch:s,score:s?0:1,indices:[0,t.length-1]}}})},{type:"inverse-suffix-exact",multiRegex:/^!"(.*)"\$$/,singleRegex:/^!(.*)\$$/,create:e=>({type:"inverse-suffix-exact",search(t){const s=!t.endsWith(e);return{isMatch:s,score:s?0:1,indices:[0,t.length-1]}}})},{type:"suffix-exact",multiRegex:/^"(.*)"\$$/,singleRegex:/^(.*)\$$/,create:e=>({type:"suffix-exact",search(t){const s=t.endsWith(e);return{isMatch:s,score:s?0:1,indices:[t.length-e.length,t.length-1]}}})},{type:"inverse-exact",multiRegex:/^!"(.*)"$/,singleRegex:/^!(.*)$/,create:e=>({type:"inverse-exact",search(t){const s=-1===t.indexOf(e);return{isMatch:s,score:s?0:1,indices:[0,t.length-1]}}})},{type:"fuzzy",multiRegex:/^"(.*)"$/,singleRegex:/^(.*)$/,create:(e,t={})=>{const s=new _(e,{location:t.location??p.location,threshold:t.threshold??p.threshold,distance:t.distance??p.distance,includeMatches:t.includeMatches??p.includeMatches,findAllMatches:t.findAllMatches??p.findAllMatches,minMatchCharLength:t.minMatchCharLength??p.minMatchCharLength,isCaseSensitive:t.isCaseSensitive??p.isCaseSensitive,ignoreDiacritics:t.ignoreDiacritics??p.ignoreDiacritics,ignoreLocation:t.ignoreLocation??p.ignoreLocation});return{type:"fuzzy",search:e=>s.searchIn(e)}}}],I=S.length;function w(e,t){const s=e.match(t);return s?s[1]:null}class b{constructor(e,{isCaseSensitive:t=p.isCaseSensitive,ignoreDiacritics:s=p.ignoreDiacritics,includeMatches:i=p.includeMatches,minMatchCharLength:n=p.minMatchCharLength,ignoreLocation:r=p.ignoreLocation,findAllMatches:o=p.findAllMatches,location:c=p.location,threshold:u=p.threshold,distance:h=p.distance}={}){this.query=null,this.options={isCaseSensitive:t,ignoreDiacritics:s,includeMatches:i,minMatchCharLength:n,findAllMatches:o,ignoreLocation:r,location:c,threshold:u,distance:h},e=t?e:e.toLowerCase(),e=s?D(e):e,this.pattern=e,this.query=function(e,t={}){return e.replace(/\\\|/g,"\0").split("|").map(e=>{const s=function(e){const t=[],s=e.length;let i=0;for(;i=s)break;let n=i;for(;n=s||" "===e[t]){n++;break}if("$"===e[t]&&(t+1>=s||" "===e[t+1])){n+=2;break}}n++}t.push(e.substring(i,n)),i=n}else{for(;ne&&!!e.trim()),i=[];for(let e=0,n=s.length;e!(!e[z]&&!e[R]),W=e=>({[z]:Object.keys(e).map(t=>({[t]:e[t]}))});function H(t,i,{auto:n=!0}={}){const o=t=>{if(s(t)){const e={keyId:null,pattern:t};return n&&(e.searcher=N(t,i)),e}const c=Object.keys(t),u=(e=>!!e[T])(t);if(!u&&c.length>1&&!j(t))return o(W(t));if((t=>!e(t)&&r(t)&&!j(t))(t)){const e=u?t[T]:c[0],r=u?t[O]:t[e];if(!s(r))throw new Error((e=>`Invalid value for key ${e}`)(e));const o={keyId:g(e),pattern:r};return n&&(o.searcher=N(r,i)),o}const h={children:[],operator:c[0]};return c.forEach(s=>{const i=t[s];e(i)&&i.forEach(e=>{h.children.push(o(e))})}),h};return j(t)||(t=W(t)),o(t)}function K(e,{ignoreFieldNorm:t=p.ignoreFieldNorm}){let s=1;return e.forEach(({key:e,norm:i,score:n})=>{const r=e?e.weight:null;s*=Math.pow(0===n&&r?Number.EPSILON:n,(r||1)*(t?1:i))}),s}class P{constructor(e){this.limit=e,this.heap=[]}get size(){return this.heap.length}shouldInsert(e){return this.size0;){const s=e-1>>1;if(t[e].score<=t[s].score)break;const i=t[e];t[e]=t[s],t[s]=i,e=s}}_sinkDown(e){const t=this.heap,s=t.length;let i=e;do{const n=2*(e=i)+1,r=2*e+2;if(nt[i].score&&(i=n),rt[i].score&&(i=r),i!==e){const s=t[e];t[e]=t[i],t[i]=s}}while(i!==e)}}function Q(e,t,{includeMatches:s=p.includeMatches,includeScore:i=p.includeScore}={}){return e.map(e=>{const{idx:n}=e,r={item:t[n],refIndex:n};return s&&(r.matches=function(e){const t=[];return e.matches.forEach(e=>{if(!o(e.indices)||!e.indices.length)return;const s={indices:e.indices,value:e.value};e.key&&(s.key=e.key.id),e.idx>-1&&(s.refIndex=e.idx),t.push(s)}),t}(e)),i&&(r.score=e.score),r})}const q=/[\p{L}\p{M}\p{N}_]+/gu;function J({isCaseSensitive:e=!1,ignoreDiacritics:t=!1,tokenize:s}={}){const i=function(e){return"function"==typeof e?t=>e(t):e instanceof RegExp?(e.global,t=>t.match(e)||[]):e=>e.match(q)||[]}(s);return{tokenize:s=>(e||(s=s.toLowerCase()),t&&(s=D(s)),i(s))}}class U{static condition(e,t){return t.useTokenSearch}constructor(e,t){this.options=t,this.analyzer=J({isCaseSensitive:t.isCaseSensitive,ignoreDiacritics:t.ignoreDiacritics,tokenize:t.tokenize});const s=this.analyzer.tokenize(e),i=t._invertedIndex,{df:n,fieldCount:r}=i;this.termSearchers=[],this.idfWeights=[];for(const e of s){this.termSearchers.push(new _(e,{location:t.location,threshold:t.threshold,distance:t.distance,includeMatches:t.includeMatches,findAllMatches:t.findAllMatches,minMatchCharLength:t.minMatchCharLength,isCaseSensitive:t.isCaseSensitive,ignoreDiacritics:t.ignoreDiacritics,ignoreLocation:!0}));const s=n.get(e)||0,i=Math.log(1+(r-s+.5)/(s+.5));this.idfWeights.push(i)}this.combineAll="all"===t.tokenMatch,this.numTerms=this.termSearchers.length,this.useMask=this.numTerms<=31}searchIn(e){if(!this.termSearchers.length)return{isMatch:!1,score:1};const t=[];let s=0,i=0,n=0,r=0;const o=this.combineAll&&!this.useMask?new Set:null;for(let c=0;c0?1-s/i:0,u={isMatch:!0,score:Math.max(.001,c)};return this.options.includeMatches&&t.length&&(u.indices=x(t)),this.combineAll&&(this.useMask?u.matchedMask=r:u.matchedTerms=o,u.termCount=this.numTerms),u}}function V(e,t,s,i){const n=i.tokenize(t);if(!n.length)return;e.fieldCount++,e.docFieldCount.set(s,(e.docFieldCount.get(s)||0)+1);const r=new Set(n);let o=e.docTermFieldHits.get(s);o||(o=new Map,e.docTermFieldHits.set(s,o));for(const t of r)o.set(t,(o.get(t)||0)+1),e.df.set(t,(e.df.get(t)||0)+1)}function G(e,t,s,i){const{i:n,v:r,$:o}=t;if(void 0===r){if(o)for(let t=0;te-t);for(const t of s)X(e,t);const i=e=>{let t=0,i=s.length;for(;t>>1;s[n]n?i(t):t,s);e.docFieldCount=r;const o=new Map;for(const[t,s]of e.docTermFieldHits)o.set(t>n?i(t):t,s);e.docTermFieldHits=o}class Z{constructor(e,t,s){this.options={...p,...t},this.options.useExtendedSearch,this.options.useTokenSearch,this._keyStore=new l(this.options.keys),this._docs=e,this._myIndex=null,this._invertedIndex=null,this.setCollection(e,s),this._lastQuery=null,this._lastSearcher=null}_getSearcher(e){if(this._lastQuery===e)return this._lastSearcher;const t=N(e,this._invertedIndex?{...this.options,_invertedIndex:this._invertedIndex}:this.options);return this._lastQuery=e,this._lastSearcher=t,t}setCollection(e,t){if(this._docs=e,t&&!(t instanceof A))throw new Error("Incorrect 'index' type");if(this._myIndex=t||C(this.options.keys,this._docs,{getFn:this.options.getFn,fieldNormWeight:this.options.fieldNormWeight}),this.options.useTokenSearch){const e=J({isCaseSensitive:this.options.isCaseSensitive,ignoreDiacritics:this.options.ignoreDiacritics,tokenize:this.options.tokenize});this._invertedIndex=function(e,t,s){const i={fieldCount:0,df:new Map,docFieldCount:new Map,docTermFieldHits:new Map};for(const n of e)G(i,n,t,s);return i}(this._myIndex.records,this._myIndex.keys.length,e)}this._invalidateSearcherCache()}add(e){if(!o(e))return;this._docs.push(e);const t=this._myIndex.add(e,this._docs.length-1);if(this._invertedIndex&&t){const e=J({isCaseSensitive:this.options.isCaseSensitive,ignoreDiacritics:this.options.ignoreDiacritics,tokenize:this.options.tokenize});!function(e,t,s,i){G(e,t,s,i)}(this._invertedIndex,t,this._myIndex.keys.length,e)}this._invalidateSearcherCache()}remove(e=()=>!1){const t=[],s=[];for(let i=0,n=this._docs.length;i!e.has(s)),this._myIndex.removeAll(s),this._invalidateSearcherCache()}return t}removeAt(e){if(!Number.isInteger(e)||e<0||e>=this._docs.length)throw new Error(h);this._invertedIndex&&Y(this._invertedIndex,[e]);const t=this._docs.splice(e,1)[0];return this._myIndex.removeAt(e),this._invalidateSearcherCache(),t}_invalidateSearcherCache(){this._lastQuery=null,this._lastSearcher=null}getIndex(){return this._myIndex}search(e,t){const{limit:n=-1}=t||{},{includeMatches:r,includeScore:o,shouldSort:c,sortFn:u,ignoreFieldNorm:h}=this.options;if(s(e)&&!e.trim()){let e=this._docs.map((e,t)=>({item:e,refIndex:t}));return i(n)&&n>-1&&(e=e.slice(0,n)),e}let a;if(i(n)&&n>0&&s(e)){const t=new P(n);s(this._docs[0])?this._searchStringList(e,{heap:t,ignoreFieldNorm:h}):this._searchObjectList(e,{heap:t,ignoreFieldNorm:h}),a=t.extractSorted(u)}else a=s(e)?s(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e),function(e,{ignoreFieldNorm:t=p.ignoreFieldNorm}){e.forEach(e=>{e.score=K(e.matches,{ignoreFieldNorm:t})})}(a,{ignoreFieldNorm:h}),c&&a.sort(u),i(n)&&n>-1&&(a=a.slice(0,n));return Q(a,this._docs,{includeMatches:r,includeScore:o})}_searchStringList(e,{heap:t,ignoreFieldNorm:s}={}){const i=this._getSearcher(e),n=this.options.useTokenSearch&&"all"===this.options.tokenMatch,{records:r}=this._myIndex,c=t?null:[];return r.forEach(({v:e,i:r,n:u})=>{if(!o(e))return;const h=i.searchIn(e);if(h.isMatch){const i={score:h.score,value:e,norm:u,indices:h.indices};n&&(i.matchedMask=h.matchedMask,i.matchedTerms=h.matchedTerms,i.termCount=h.termCount);const o=[i];if(!n||this._coversAllTokens(o)){const i={item:e,idx:r,matches:o};t?(i.score=K(i.matches,{ignoreFieldNorm:s}),t.shouldInsert(i.score)&&t.insert(i)):c.push(i)}}}),c}_searchLogical(e){const t=H(e,this.options),s=(e,t,i)=>{if(!("children"in e)){const{keyId:s,searcher:n}=e;let r;return null===s?(r=[],this._myIndex.keys.forEach((e,s)=>{r.push(...this._findMatches({key:e,value:t[s],searcher:n}))})):r=this._findMatches({key:this._keyStore.get(s),value:this._myIndex.getValueForItemAtKeyId(t,s),searcher:n}),r&&r.length?[{idx:i,item:t,matches:r}]:[]}const{children:n,operator:r}=e,o=[];for(let e=0,c=n.length;e{if(o(e)){const o=s(t,e,i);o.length&&(n.has(i)||(n.set(i,{idx:i,item:e,matches:[]}),r.push(n.get(i))),o.forEach(({matches:e})=>{n.get(i).matches.push(...e)}))}}),r}_searchObjectList(e,{heap:t,ignoreFieldNorm:s}={}){const i=this._getSearcher(e),n=this.options.useTokenSearch&&"all"===this.options.tokenMatch,{keys:r,records:c}=this._myIndex,u=t?null:[];return c.forEach(({$:e,i:c})=>{if(!o(e))return;const h=[];let a=!1,l=!1;if(r.forEach((t,s)=>{const n=this._findMatches({key:t,value:e[s],searcher:i});n.length?(h.push(...n),n[0].hasInverse&&(l=!0)):a=!0}),(!l||!a)&&h.length&&(!n||this._coversAllTokens(h))){const i={idx:c,item:e,matches:h};t?(i.score=K(i.matches,{ignoreFieldNorm:s}),t.shouldInsert(i.score)&&t.insert(i)):u.push(i)}}),u}_findMatches({key:t,value:s,searcher:i}){if(!o(s))return[];const n=[];if(e(s))s.forEach(({v:e,i:s,n:r})=>{if(!o(e))return;const c=i.searchIn(e);if(c.isMatch){const i={score:c.score,key:t,value:e,idx:s,norm:r,indices:c.indices,hasInverse:c.hasInverse};void 0!==c.termCount&&(i.matchedMask=c.matchedMask,i.matchedTerms=c.matchedTerms,i.termCount=c.termCount),n.push(i)}});else{const{v:e,n:r}=s,o=i.searchIn(e);if(o.isMatch){const s={score:o.score,key:t,value:e,norm:r,indices:o.indices,hasInverse:o.hasInverse};void 0!==o.termCount&&(s.matchedMask=o.matchedMask,s.matchedTerms=o.matchedTerms,s.termCount=o.termCount),n.push(s)}}return n}_coversAllTokens(e){const t=e.length?e[0].termCount:void 0;if(void 0===t)return!0;if(t<=31){let s=0;for(let t=0;t{const{id:t,method:s,args:i}=e.data;try{let e;switch(s){case"init":{const[t,s]=i;ee=new Z(t,s),e=!0;break}case"search":e=ee.search(i[0],i[1]);break;case"add":ee.add(i[0]),e=!0;break;case"setCollection":ee.setCollection(i[0]),e=!0}self.postMessage({id:t,result:e})}catch(e){self.postMessage({id:t,error:e.message})}}; \ No newline at end of file +function e(e){return Array.isArray?Array.isArray(e):u(e)===`[object Array]`}function t(e){if(typeof e==`string`)return e;if(typeof e==`bigint`)return e.toString();let t=e+``;return t==`0`&&1/e==-1/0?`-0`:t}function n(e){return e==null?``:t(e)}function r(e){return typeof e==`string`}function i(e){return typeof e==`number`}function a(e){return e===!0||e===!1||s(e)&&u(e)==`[object Boolean]`}function o(e){return typeof e==`object`}function s(e){return o(e)&&e!==null}function c(e){return e!=null}function l(e){return!e.trim().length}function u(e){return e==null?e===void 0?`[object Undefined]`:`[object Null]`:Object.prototype.toString.call(e)}const d=`Invalid doc index: must be a non-negative integer within the bounds of the docs array`,f=e=>`Invalid value for key ${e}`,p=e=>`Pattern length exceeds max of ${e}.`,m=e=>`Missing ${e} property in key`,h=e=>`Property 'weight' in key '${e}' must be a positive integer`,g=Object.prototype.hasOwnProperty;var _=class{constructor(e){this._keys=[],this._keyMap={};let t=0;e.forEach(e=>{let n=v(e);this._keys.push(n),this._keyMap[n.id]=n,t+=n.weight}),this._keys.forEach(e=>{e.weight/=t})}get(e){return this._keyMap[e]}keys(){return this._keys}toJSON(){return JSON.stringify(this._keys)}};function v(t){let n=null,i=null,a=null,o=1,s=null;if(r(t)||e(t))a=t,n=y(t),i=b(t);else{if(!g.call(t,`name`))throw Error(m(`name`));let e=t.name;if(a=e,g.call(t,`weight`)&&t.weight!==void 0&&(o=t.weight,o<=0))throw Error(h(b(e)));n=y(e),i=b(e),s=t.getFn??null}return{path:n,id:i,weight:o,src:a,getFn:s}}function y(t){return e(t)?t:t.split(`.`)}function b(t){return e(t)?t.join(`.`):t}function x(t,o){let s=[],l=!1,u=(t,o,d,f)=>{if(c(t))if(!o[d])s.push(f===void 0?t:{v:t,i:f});else{let p=t[o[d]];if(!c(p))return;if(d===o.length-1&&(r(p)||i(p)||a(p)||typeof p==`bigint`))s.push(f===void 0?n(p):{v:n(p),i:f});else if(e(p)){l=!0;for(let e=0,t=p.length;ee.score===t.score?e.idx{this._keysMap[e.id]=t})}create(){if(this.isCreated||!this.docs.length)return;this.isCreated=!0;let e=this.docs.length;this.records=Array(e);let t=0;if(r(this.docs[0]))for(let n=0;ne&&--this.records[t].i}removeAll(e){let t=new Set;for(let n of e)Number.isInteger(n)&&n>=0&&t.add(n);if(t.size===0)return;this.records=this.records.filter(e=>!t.has(e.i));let n=Array.from(t).sort((e,t)=>e-t);for(let e of this.records){let t=0,r=n.length;for(;t>>1;n[i]t),records:this.records}}};function O(e,t,{getFn:n=E.getFn,fieldNormWeight:r=E.fieldNormWeight}={}){let i=new D({getFn:n,fieldNormWeight:r});return i.setKeys(e.map(v)),i.setSources(t),i.create(),i}function k(e=[],t=E.minMatchCharLength){let n=[],r=-1,i=-1,a=0;for(let o=e.length;a=t&&n.push([r,i]),r=-1)}return e[a-1]&&a-r>=t&&n.push([r,a-1]),n}function te(e,t,n,{location:r=E.location,distance:i=E.distance,threshold:a=E.threshold,findAllMatches:o=E.findAllMatches,minMatchCharLength:s=E.minMatchCharLength,includeMatches:c=E.includeMatches,ignoreLocation:l=E.ignoreLocation}={}){if(t.length>32)throw Error(p(32));let u=t.length,d=e.length,f=Math.max(0,Math.min(r,d)),m=a,h=f,g=(e,t)=>{let n=e/u;if(l)return n;let r=Math.abs(f-t);return i?n+r/i:r?1:n},_=s>1||c,v=_?Array(d):[],y;for(;(y=e.indexOf(t,h))>-1;){let e=g(0,y);if(m=Math.min(e,m),h=y+u,_){let e=0;for(;e=a;--r){let i=r-1,o=n[e[i]];if(c[r]=(c[r+1]<<1|1)&o,t&&(c[r]|=(b[r+1]|b[r])<<1|1|b[r+1]),c[r]&w&&(x=g(t,i),x<=m)){if(m=x,h=i,S=t,h<=f)break;a=Math.max(1,2*f-h)}}if(g(t+1,f)>m)break;b=c}if(_&&h>=0){let t=Math.min(d-1,h+u-1+S);for(let r=h;r<=t;r+=1)n[e[r]]&&(v[r]=1)}let T={isMatch:h>=0,score:Math.max(.001,x)};if(_){let e=k(v,s);e.length?c&&(T.indices=e):T.isMatch=!1}return T}function ne(e){let t={};for(let n=0,r=e.length;ne[0]-t[0]||e[1]-t[1]);let t=[e[0]];for(let n=1,r=e.length;ne.normalize(`NFD`).replace(/[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]/g,``).replace(re,e=>j[e]):e=>e;var N=class{constructor(e,{location:t=E.location,threshold:n=E.threshold,distance:r=E.distance,includeMatches:i=E.includeMatches,findAllMatches:a=E.findAllMatches,minMatchCharLength:o=E.minMatchCharLength,isCaseSensitive:s=E.isCaseSensitive,ignoreDiacritics:c=E.ignoreDiacritics,ignoreLocation:l=E.ignoreLocation}={}){if(this.options={location:t,threshold:n,distance:r,includeMatches:i,findAllMatches:a,minMatchCharLength:o,isCaseSensitive:s,ignoreDiacritics:c,ignoreLocation:l},e=s?e:e.toLowerCase(),e=c?M(e):e,this.pattern=e,this.chunks=[],!this.pattern.length)return;let u=(e,t)=>{this.chunks.push({pattern:e,alphabet:ne(e),startIndex:t})},d=this.pattern.length;if(d>32){let e=0,t=d%32,n=d-t;for(;e{let{isMatch:m,score:h,indices:g}=te(e,t,n,{location:i+p,distance:a,threshold:o,findAllMatches:s,minMatchCharLength:c,includeMatches:r,ignoreLocation:l});m&&(f=!0),d+=h,m&&g&&u.push(...g)});let p={isMatch:f,score:f?d/this.chunks.length:1};return f&&r&&(p.indices=A(u)),p}};const ie=new Set([`fuzzy`,`include`]);function ae(e){return e.startsWith(`inverse`)}const P=[{type:`exact`,multiRegex:/^="(.*)"$/,singleRegex:/^=(.*)$/,create:e=>({type:`exact`,search(t){let n=t===e;return{isMatch:n,score:+!n,indices:[0,e.length-1]}}})},{type:`include`,multiRegex:/^'"(.*)"$/,singleRegex:/^'(.*)$/,create:e=>({type:`include`,search(t){let n=0,r,i=[],a=e.length;for(;(r=t.indexOf(e,n))>-1;)n=r+a,i.push([r,n-1]);let o=!!i.length;return{isMatch:o,score:+!o,indices:i}}})},{type:`prefix-exact`,multiRegex:/^\^"(.*)"$/,singleRegex:/^\^(.*)$/,create:e=>({type:`prefix-exact`,search(t){let n=t.startsWith(e);return{isMatch:n,score:+!n,indices:[0,e.length-1]}}})},{type:`inverse-prefix-exact`,multiRegex:/^!\^"(.*)"$/,singleRegex:/^!\^(.*)$/,create:e=>({type:`inverse-prefix-exact`,search(t){let n=!t.startsWith(e);return{isMatch:n,score:+!n,indices:[0,t.length-1]}}})},{type:`inverse-suffix-exact`,multiRegex:/^!"(.*)"\$$/,singleRegex:/^!(.*)\$$/,create:e=>({type:`inverse-suffix-exact`,search(t){let n=!t.endsWith(e);return{isMatch:n,score:+!n,indices:[0,t.length-1]}}})},{type:`suffix-exact`,multiRegex:/^"(.*)"\$$/,singleRegex:/^(.*)\$$/,create:e=>({type:`suffix-exact`,search(t){let n=t.endsWith(e);return{isMatch:n,score:+!n,indices:[t.length-e.length,t.length-1]}}})},{type:`inverse-exact`,multiRegex:/^!"(.*)"$/,singleRegex:/^!(.*)$/,create:e=>({type:`inverse-exact`,search(t){let n=t.indexOf(e)===-1;return{isMatch:n,score:+!n,indices:[0,t.length-1]}}})},{type:`fuzzy`,multiRegex:/^"(.*)"$/,singleRegex:/^(.*)$/,create:(e,t={})=>{let n=new N(e,{location:t.location??E.location,threshold:t.threshold??E.threshold,distance:t.distance??E.distance,includeMatches:t.includeMatches??E.includeMatches,findAllMatches:t.findAllMatches??E.findAllMatches,minMatchCharLength:t.minMatchCharLength??E.minMatchCharLength,isCaseSensitive:t.isCaseSensitive??E.isCaseSensitive,ignoreDiacritics:t.ignoreDiacritics??E.ignoreDiacritics,ignoreLocation:t.ignoreLocation??E.ignoreLocation});return{type:`fuzzy`,search(e){return n.searchIn(e)}}}}],F=P.length;function oe(e){let t=[],n=e.length,r=0;for(;r=n)break;let i=r;for(;i=n||e[t]===` `){i++;break}if(e[t]===`$`&&(t+1>=n||e[t+1]===` `)){i+=2;break}}i++}t.push(e.substring(r,i)),r=i}else{for(;i{let n=oe(e.replace(/\u0000/g,`|`).trim()).filter(e=>e&&!!e.trim()),r=[];for(let e=0,i=n.length;e!!(e[B.AND]||e[B.OR]),U=e=>!!e[V.PATH],W=t=>!e(t)&&o(t)&&!H(t),G=e=>({[B.AND]:Object.keys(e).map(t=>({[t]:e[t]}))});function le(t,n,{auto:i=!0}={}){let a=t=>{if(r(t)){let e={keyId:null,pattern:t};return i&&(e.searcher=z(t,n)),e}let o=Object.keys(t),s=U(t);if(!s&&o.length>1&&!H(t))return a(G(t));if(W(t)){let e=s?t[V.PATH]:o[0],a=s?t[V.PATTERN]:t[e];if(!r(a))throw Error(f(e));let c={keyId:b(e),pattern:a};return i&&(c.searcher=z(a,n)),c}let c={children:[],operator:o[0]};return o.forEach(n=>{let r=t[n];e(r)&&r.forEach(e=>{c.children.push(a(e))})}),c};return H(t)||(t=G(t)),a(t)}function K(e,{ignoreFieldNorm:t=E.ignoreFieldNorm}){let n=1;return e.forEach(({key:e,norm:r,score:i})=>{let a=e?e.weight:null;n*=(i===0&&a?2**-52:i)**+((a||1)*(t?1:r))}),n}function ue(e,{ignoreFieldNorm:t=E.ignoreFieldNorm}){e.forEach(e=>{e.score=K(e.matches,{ignoreFieldNorm:t})})}var de=class{constructor(e){this.limit=e,this.heap=[]}get size(){return this.heap.length}shouldInsert(e){return this.size0;){let n=e-1>>1;if(t[e].score<=t[n].score)break;let r=t[e];t[e]=t[n],t[n]=r,e=n}}_sinkDown(e){let t=this.heap,n=t.length,r=e;do{e=r;let i=2*e+1,a=2*e+2;if(it[r].score&&(r=i),at[r].score&&(r=a),r!==e){let n=t[e];t[e]=t[r],t[r]=n}}while(r!==e)}};function fe(e){let t=[];return e.matches.forEach(e=>{if(!c(e.indices)||!e.indices.length)return;let n={indices:e.indices,value:e.value};e.key&&(n.key=e.key.id),e.idx>-1&&(n.refIndex=e.idx),t.push(n)}),t}function pe(e,t,{includeMatches:n=E.includeMatches,includeScore:r=E.includeScore}={}){return e.map(e=>{let{idx:i}=e,a={item:t[i],refIndex:i};return n&&(a.matches=fe(e)),r&&(a.score=e.score),a})}const me=/[\p{L}\p{M}\p{N}_]+/gu;function he(e){return typeof e==`function`?t=>e(t):e instanceof RegExp?(e.global,t=>t.match(e)||[]):e=>e.match(me)||[]}function q({isCaseSensitive:e=!1,ignoreDiacritics:t=!1,tokenize:n}={}){let r=he(n);return{tokenize(n){return e||(n=n.toLowerCase()),t&&(n=M(n)),r(n)}}}var J=class{static condition(e,t){return t.useTokenSearch}constructor(e,t){this.options=t,this.analyzer=q({isCaseSensitive:t.isCaseSensitive,ignoreDiacritics:t.ignoreDiacritics,tokenize:t.tokenize});let n=this.analyzer.tokenize(e),{df:r,fieldCount:i}=t._invertedIndex;this.termSearchers=[],this.idfWeights=[];for(let e of n){this.termSearchers.push(new N(e,{location:t.location,threshold:t.threshold,distance:t.distance,includeMatches:t.includeMatches,findAllMatches:t.findAllMatches,minMatchCharLength:t.minMatchCharLength,isCaseSensitive:t.isCaseSensitive,ignoreDiacritics:t.ignoreDiacritics,ignoreLocation:!0}));let n=r.get(e)||0,a=Math.log(1+(i-n+.5)/(n+.5));this.idfWeights.push(a)}this.combineAll=t.tokenMatch===`all`,this.numTerms=this.termSearchers.length,this.useMask=this.numTerms<=31}searchIn(e){if(!this.termSearchers.length)return{isMatch:!1,score:1};let t=[],n=0,r=0,i=0,a=0,o=this.combineAll&&!this.useMask?new Set:null;for(let s=0;s0?1-n/r:0,c={isMatch:!0,score:Math.max(.001,s)};return this.options.includeMatches&&t.length&&(c.indices=A(t)),this.combineAll&&(this.useMask?c.matchedMask=a:c.matchedTerms=o,c.termCount=this.numTerms),c}};function Y(e,t,n,r){let i=r.tokenize(t);if(!i.length)return;e.fieldCount++,e.docFieldCount.set(n,(e.docFieldCount.get(n)||0)+1);let a=new Set(i),o=e.docTermFieldHits.get(n);o||(o=new Map,e.docTermFieldHits.set(n,o));for(let t of a)o.set(t,(o.get(t)||0)+1),e.df.set(t,(e.df.get(t)||0)+1)}function X(e,t,n,r){let{i,v:a,$:o}=t;if(a!==void 0){Y(e,a,i,r);return}if(o)for(let t=0;te-t);for(let t of n)ve(e,t);let r=e=>{let t=0,r=n.length;for(;t>>1;n[i]i?r(t):t,n);e.docFieldCount=a;let o=new Map;for(let[t,n]of e.docTermFieldHits)o.set(t>i?r(t):t,n);e.docTermFieldHits=o}var Q=class{constructor(e,t,n){this.options={...E,...t},this.options.useExtendedSearch,this.options.useTokenSearch,this._keyStore=new _(this.options.keys),this._docs=e,this._myIndex=null,this._invertedIndex=null,this.setCollection(e,n),this._lastQuery=null,this._lastSearcher=null}_getSearcher(e){if(this._lastQuery===e)return this._lastSearcher;let t=z(e,this._invertedIndex?{...this.options,_invertedIndex:this._invertedIndex}:this.options);return this._lastQuery=e,this._lastSearcher=t,t}setCollection(e,t){if(this._docs=e,t&&!(t instanceof D))throw Error(`Incorrect 'index' type`);if(this._myIndex=t||O(this.options.keys,this._docs,{getFn:this.options.getFn,fieldNormWeight:this.options.fieldNormWeight}),this.options.useTokenSearch){let e=q({isCaseSensitive:this.options.isCaseSensitive,ignoreDiacritics:this.options.ignoreDiacritics,tokenize:this.options.tokenize});this._invertedIndex=ge(this._myIndex.records,this._myIndex.keys.length,e)}this._invalidateSearcherCache()}add(e){if(!c(e))return;this._docs.push(e);let t=this._myIndex.add(e,this._docs.length-1);if(this._invertedIndex&&t){let e=q({isCaseSensitive:this.options.isCaseSensitive,ignoreDiacritics:this.options.ignoreDiacritics,tokenize:this.options.tokenize});_e(this._invertedIndex,t,this._myIndex.keys.length,e)}this._invalidateSearcherCache()}remove(e=()=>!1){let t=[],n=[];for(let r=0,i=this._docs.length;r!e.has(n)),this._myIndex.removeAll(n),this._invalidateSearcherCache()}return t}removeAt(e){if(!Number.isInteger(e)||e<0||e>=this._docs.length)throw Error(d);this._invertedIndex&&Z(this._invertedIndex,[e]);let t=this._docs.splice(e,1)[0];return this._myIndex.removeAt(e),this._invalidateSearcherCache(),t}_invalidateSearcherCache(){this._lastQuery=null,this._lastSearcher=null}getIndex(){return this._myIndex}search(e,t){let{limit:n=-1}=t||{},{includeMatches:a,includeScore:o,shouldSort:s,sortFn:c,ignoreFieldNorm:l}=this.options;if(r(e)&&!e.trim()){let e=this._docs.map((e,t)=>({item:e,refIndex:t}));return i(n)&&n>-1&&(e=e.slice(0,n)),e}let u=i(n)&&n>0&&r(e),d;if(u){let t=new de(n);r(this._docs[0])?this._searchStringList(e,{heap:t,ignoreFieldNorm:l}):this._searchObjectList(e,{heap:t,ignoreFieldNorm:l}),d=t.extractSorted(c)}else d=r(e)?r(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e),ue(d,{ignoreFieldNorm:l}),s&&d.sort(c),i(n)&&n>-1&&(d=d.slice(0,n));return pe(d,this._docs,{includeMatches:a,includeScore:o})}_searchStringList(e,{heap:t,ignoreFieldNorm:n}={}){let r=this._getSearcher(e),i=this.options.useTokenSearch&&this.options.tokenMatch===`all`,{records:a}=this._myIndex,o=t?null:[];return a.forEach(({v:e,i:a,n:s})=>{if(!c(e))return;let l=r.searchIn(e);if(l.isMatch){let r={score:l.score,value:e,norm:s,indices:l.indices};i&&(r.matchedMask=l.matchedMask,r.matchedTerms=l.matchedTerms,r.termCount=l.termCount);let c=[r];if(!i||this._coversAllTokens(c)){let r={item:e,idx:a,matches:c};t?(r.score=K(r.matches,{ignoreFieldNorm:n}),t.shouldInsert(r.score)&&t.insert(r)):o.push(r)}}}),o}_searchLogical(e){let t=le(e,this.options),n=(e,t,r)=>{if(!(`children`in e)){let{keyId:n,searcher:i}=e,a;return n===null?(a=[],this._myIndex.keys.forEach((e,n)=>{a.push(...this._findMatches({key:e,value:t[n],searcher:i}))})):a=this._findMatches({key:this._keyStore.get(n),value:this._myIndex.getValueForItemAtKeyId(t,n),searcher:i}),a&&a.length?[{idx:r,item:t,matches:a}]:[]}let{children:i,operator:a}=e,o=[];for(let e=0,s=i.length;e{if(c(e)){let o=n(t,e,r);o.length&&(i.has(r)||(i.set(r,{idx:r,item:e,matches:[]}),a.push(i.get(r))),o.forEach(({matches:e})=>{i.get(r).matches.push(...e)}))}}),a}_searchObjectList(e,{heap:t,ignoreFieldNorm:n}={}){let r=this._getSearcher(e),i=this.options.useTokenSearch&&this.options.tokenMatch===`all`,{keys:a,records:o}=this._myIndex,s=t?null:[];return o.forEach(({$:e,i:o})=>{if(!c(e))return;let l=[],u=!1,d=!1;if(a.forEach((t,n)=>{let i=this._findMatches({key:t,value:e[n],searcher:r});i.length?(l.push(...i),i[0].hasInverse&&(d=!0)):u=!0}),!(d&&u)&&l.length&&(!i||this._coversAllTokens(l))){let r={idx:o,item:e,matches:l};t?(r.score=K(r.matches,{ignoreFieldNorm:n}),t.shouldInsert(r.score)&&t.insert(r)):s.push(r)}}),s}_findMatches({key:t,value:n,searcher:r}){if(!c(n))return[];let i=[];if(e(n))n.forEach(({v:e,i:n,n:a})=>{if(!c(e))return;let o=r.searchIn(e);if(o.isMatch){let r={score:o.score,key:t,value:e,idx:n,norm:a,indices:o.indices,hasInverse:o.hasInverse};o.termCount!==void 0&&(r.matchedMask=o.matchedMask,r.matchedTerms=o.matchedTerms,r.termCount=o.termCount),i.push(r)}});else{let{v:e,n:a}=n,o=r.searchIn(e);if(o.isMatch){let n={score:o.score,key:t,value:e,norm:a,indices:o.indices,hasInverse:o.hasInverse};o.termCount!==void 0&&(n.matchedMask=o.matchedMask,n.matchedTerms=o.matchedTerms,n.termCount=o.termCount),i.push(n)}}return i}_coversAllTokens(e){let t=e.length?e[0].termCount:void 0;if(t===void 0)return!0;if(t<=31){let n=0;for(let t=0;t{let{id:t,method:n,args:r}=e.data;try{let e;switch(n){case`init`:{let[t,n]=r;$=new Q(t,n),e=!0;break}case`search`:e=$.search(r[0],r[1]);break;case`add`:$.add(r[0]),e=!0;break;case`setCollection`:$.setCollection(r[0]),e=!0;break}self.postMessage({id:t,result:e})}catch(e){self.postMessage({id:t,error:e.message})}}; \ No newline at end of file diff --git a/dist/fuse.worker.mjs b/dist/fuse.worker.mjs index c9a997a0f..099dcbc0b 100644 --- a/dist/fuse.worker.mjs +++ b/dist/fuse.worker.mjs @@ -6,2246 +6,1612 @@ * * http://www.apache.org/licenses/LICENSE-2.0 */ - +//#region src/helpers/typeGuards.ts function isArray(value) { - return !Array.isArray ? getTag(value) === '[object Array]' : Array.isArray(value); + return !Array.isArray ? getTag(value) === "[object Array]" : Array.isArray(value); } function baseToString(value) { - // Exit early for strings to avoid a performance hit in some environments. - if (typeof value == 'string') { - return value; - } - if (typeof value === 'bigint') { - return value.toString(); - } - const result = value + ''; - return result == '0' && 1 / value == -Infinity ? '-0' : result; + if (typeof value == "string") return value; + if (typeof value === "bigint") return value.toString(); + const result = value + ""; + return result == "0" && 1 / value == -Infinity ? "-0" : result; } function toString(value) { - return value == null ? '' : baseToString(value); + return value == null ? "" : baseToString(value); } function isString(value) { - return typeof value === 'string'; + return typeof value === "string"; } function isNumber(value) { - return typeof value === 'number'; + return typeof value === "number"; } - -// Adapted from: https://github.com/lodash/lodash/blob/master/isBoolean.js function isBoolean(value) { - return value === true || value === false || isObjectLike(value) && getTag(value) == '[object Boolean]'; + return value === true || value === false || isObjectLike(value) && getTag(value) == "[object Boolean]"; } function isObject(value) { - return typeof value === 'object'; + return typeof value === "object"; } - -// Checks if `value` is object-like. function isObjectLike(value) { - return isObject(value) && value !== null; + return isObject(value) && value !== null; } function isDefined(value) { - return value !== undefined && value !== null; + return value !== void 0 && value !== null; } function isBlank(value) { - return !value.trim().length; + return !value.trim().length; } - -// Gets the `toStringTag` of `value`. -// Adapted from: https://github.com/lodash/lodash/blob/master/.internal/getTag.js function getTag(value) { - return value == null ? value === undefined ? '[object Undefined]' : '[object Null]' : Object.prototype.toString.call(value); + return value == null ? value === void 0 ? "[object Undefined]" : "[object Null]" : Object.prototype.toString.call(value); } +//#endregion +//#region src/core/errorMessages.ts const INCORRECT_INDEX_TYPE = "Incorrect 'index' type"; -const INVALID_DOC_INDEX = 'Invalid doc index: must be a non-negative integer within the bounds of the docs array'; -const LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY = key => `Invalid value for key ${key}`; -const PATTERN_LENGTH_TOO_LARGE = max => `Pattern length exceeds max of ${max}.`; -const MISSING_KEY_PROPERTY = name => `Missing ${name} property in key`; -const INVALID_KEY_WEIGHT_VALUE = key => `Property 'weight' in key '${key}' must be a positive integer`; - +const INVALID_DOC_INDEX = "Invalid doc index: must be a non-negative integer within the bounds of the docs array"; +const LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY = (key) => `Invalid value for key ${key}`; +const PATTERN_LENGTH_TOO_LARGE = (max) => `Pattern length exceeds max of ${max}.`; +const MISSING_KEY_PROPERTY = (name) => `Missing ${name} property in key`; +const INVALID_KEY_WEIGHT_VALUE = (key) => `Property 'weight' in key '${key}' must be a positive integer`; + +//#endregion +//#region src/tools/KeyStore.ts const hasOwn = Object.prototype.hasOwnProperty; -class KeyStore { - constructor(keys) { - this._keys = []; - this._keyMap = {}; - let totalWeight = 0; - keys.forEach(key => { - const obj = createKey(key); - this._keys.push(obj); - this._keyMap[obj.id] = obj; - totalWeight += obj.weight; - }); - - // Normalize weights so that their sum is equal to 1 - this._keys.forEach(key => { - key.weight /= totalWeight; - }); - } - get(keyId) { - return this._keyMap[keyId]; - } - keys() { - return this._keys; - } - toJSON() { - return JSON.stringify(this._keys); - } -} +var KeyStore = class { + constructor(keys) { + this._keys = []; + this._keyMap = {}; + let totalWeight = 0; + keys.forEach((key) => { + const obj = createKey(key); + this._keys.push(obj); + this._keyMap[obj.id] = obj; + totalWeight += obj.weight; + }); + this._keys.forEach((key) => { + key.weight /= totalWeight; + }); + } + get(keyId) { + return this._keyMap[keyId]; + } + keys() { + return this._keys; + } + toJSON() { + return JSON.stringify(this._keys); + } +}; function createKey(key) { - let path = null; - let id = null; - let src = null; - let weight = 1; - let getFn = null; - if (isString(key) || isArray(key)) { - src = key; - path = createKeyPath(key); - id = createKeyId(key); - } else { - if (!hasOwn.call(key, 'name')) { - throw new Error(MISSING_KEY_PROPERTY('name')); - } - const name = key.name; - src = name; - if (hasOwn.call(key, 'weight') && key.weight !== undefined) { - weight = key.weight; - if (weight <= 0) { - throw new Error(INVALID_KEY_WEIGHT_VALUE(createKeyId(name))); - } - } - path = createKeyPath(name); - id = createKeyId(name); - getFn = key.getFn ?? null; - } - return { - path: path, - id: id, - weight, - src: src, - getFn - }; + let path = null; + let id = null; + let src = null; + let weight = 1; + let getFn = null; + if (isString(key) || isArray(key)) { + src = key; + path = createKeyPath(key); + id = createKeyId(key); + } else { + if (!hasOwn.call(key, "name")) throw new Error(MISSING_KEY_PROPERTY("name")); + const name = key.name; + src = name; + if (hasOwn.call(key, "weight") && key.weight !== void 0) { + weight = key.weight; + if (weight <= 0) throw new Error(INVALID_KEY_WEIGHT_VALUE(createKeyId(name))); + } + path = createKeyPath(name); + id = createKeyId(name); + getFn = key.getFn ?? null; + } + return { + path, + id, + weight, + src, + getFn + }; } function createKeyPath(key) { - return isArray(key) ? key : key.split('.'); + return isArray(key) ? key : key.split("."); } function createKeyId(key) { - return isArray(key) ? key.join('.') : key; + return isArray(key) ? key.join(".") : key; } +//#endregion +//#region src/helpers/get.ts function get(obj, path) { - const list = []; - let arr = false; - const deepGet = (obj, path, index, arrayIndex) => { - if (!isDefined(obj)) { - return; - } - if (!path[index]) { - // If there's no path left, we've arrived at the object we care about. - list.push(arrayIndex !== undefined ? { - v: obj, - i: arrayIndex - } : obj); - } else { - const key = path[index]; - const value = obj[key]; - if (!isDefined(value)) { - return; - } - - // If we're at the last value in the path, and if it's a string/number/bool, - // add it to the list - if (index === path.length - 1 && (isString(value) || isNumber(value) || isBoolean(value) || typeof value === 'bigint')) { - list.push(arrayIndex !== undefined ? { - v: toString(value), - i: arrayIndex - } : toString(value)); - } else if (isArray(value)) { - arr = true; - // Search each item in the array. - for (let i = 0, len = value.length; i < len; i += 1) { - deepGet(value[i], path, index + 1, i); - } - } else if (path.length) { - // An object. Recurse further. - deepGet(value, path, index + 1, arrayIndex); - } - } - }; - - // Backwards compatibility (since path used to be a string) - deepGet(obj, isString(path) ? path.split('.') : path, 0); - return arr ? list : list[0]; + const list = []; + let arr = false; + const deepGet = (obj, path, index, arrayIndex) => { + if (!isDefined(obj)) return; + if (!path[index]) list.push(arrayIndex !== void 0 ? { + v: obj, + i: arrayIndex + } : obj); + else { + const value = obj[path[index]]; + if (!isDefined(value)) return; + if (index === path.length - 1 && (isString(value) || isNumber(value) || isBoolean(value) || typeof value === "bigint")) list.push(arrayIndex !== void 0 ? { + v: toString(value), + i: arrayIndex + } : toString(value)); + else if (isArray(value)) { + arr = true; + for (let i = 0, len = value.length; i < len; i += 1) deepGet(value[i], path, index + 1, i); + } else if (path.length) deepGet(value, path, index + 1, arrayIndex); + } + }; + deepGet(obj, isString(path) ? path.split(".") : path, 0); + return arr ? list : list[0]; } +//#endregion +//#region src/core/config.ts const MatchOptions = { - includeMatches: false, - findAllMatches: false, - minMatchCharLength: 1 + includeMatches: false, + findAllMatches: false, + minMatchCharLength: 1 }; const BasicOptions = { - isCaseSensitive: false, - ignoreDiacritics: false, - includeScore: false, - keys: [], - shouldSort: true, - sortFn: (a, b) => a.score === b.score ? a.idx < b.idx ? -1 : 1 : a.score < b.score ? -1 : 1 + isCaseSensitive: false, + ignoreDiacritics: false, + includeScore: false, + keys: [], + shouldSort: true, + sortFn: (a, b) => a.score === b.score ? a.idx < b.idx ? -1 : 1 : a.score < b.score ? -1 : 1 }; const FuzzyOptions = { - location: 0, - threshold: 0.6, - distance: 100 + location: 0, + threshold: .6, + distance: 100 }; const AdvancedOptions = { - useExtendedSearch: false, - useTokenSearch: false, - tokenize: undefined, - tokenMatch: 'any', - getFn: get, - ignoreLocation: false, - ignoreFieldNorm: false, - fieldNormWeight: 1 + useExtendedSearch: false, + useTokenSearch: false, + tokenize: void 0, + tokenMatch: "any", + getFn: get, + ignoreLocation: false, + ignoreFieldNorm: false, + fieldNormWeight: 1 }; const Config = Object.freeze({ - ...BasicOptions, - ...MatchOptions, - ...FuzzyOptions, - ...AdvancedOptions + ...BasicOptions, + ...MatchOptions, + ...FuzzyOptions, + ...AdvancedOptions }); -// Field-length norm: the shorter the field, the higher the weight. -// Set to 3 decimals to reduce index size. +//#endregion +//#region src/tools/fieldNorm.ts function norm(weight = 1, mantissa = 3) { - const cache = new Map(); - const m = Math.pow(10, mantissa); - return { - get(value) { - // Count words by counting space transitions — avoids allocating a regex match array - let numTokens = 1; - let inSpace = false; - for (let i = 0; i < value.length; i++) { - if (value.charCodeAt(i) === 32) { - if (!inSpace) { - numTokens++; - inSpace = true; - } - } else { - inSpace = false; - } - } - if (cache.has(numTokens)) { - return cache.get(numTokens); - } - - // Default function is 1/sqrt(x), weight makes that variable - const n = Math.round(m / Math.pow(numTokens, 0.5 * weight)) / m; - cache.set(numTokens, n); - return n; - }, - clear() { - cache.clear(); - } - }; + const cache = /* @__PURE__ */ new Map(); + const m = Math.pow(10, mantissa); + return { + get(value) { + let numTokens = 1; + let inSpace = false; + for (let i = 0; i < value.length; i++) if (value.charCodeAt(i) === 32) { + if (!inSpace) { + numTokens++; + inSpace = true; + } + } else inSpace = false; + if (cache.has(numTokens)) return cache.get(numTokens); + const n = Math.round(m / Math.pow(numTokens, .5 * weight)) / m; + cache.set(numTokens, n); + return n; + }, + clear() { + cache.clear(); + } + }; } -class FuseIndex { - constructor({ - getFn = Config.getFn, - fieldNormWeight = Config.fieldNormWeight - } = {}) { - this.norm = norm(fieldNormWeight, 3); - this.getFn = getFn; - this.isCreated = false; - this.docs = []; - this.keys = []; - this._keysMap = {}; - this.setIndexRecords(); - } - setSources(docs = []) { - this.docs = docs; - } - setIndexRecords(records = []) { - this.records = records; - } - setKeys(keys = []) { - this.keys = keys; - this._keysMap = {}; - keys.forEach((key, idx) => { - this._keysMap[key.id] = idx; - }); - } - create() { - if (this.isCreated || !this.docs.length) { - return; - } - this.isCreated = true; - const len = this.docs.length; - this.records = new Array(len); - let recordCount = 0; - - // List is Array - if (isString(this.docs[0])) { - for (let i = 0; i < len; i++) { - const record = this._createStringRecord(this.docs[i], i); - if (record) { - this.records[recordCount++] = record; - } - } - } else { - // List is Array - for (let i = 0; i < len; i++) { - this.records[recordCount++] = this._createObjectRecord(this.docs[i], i); - } - } - this.records.length = recordCount; - this.norm.clear(); - } - // Appends a record for `doc` at `docIndex` (the doc's position in the source - // array). Returns the appended record, or null when `doc` is a blank string - // (those are skipped at record creation; see `_createStringRecord`). Callers - // use the return value to gate downstream bookkeeping like the inverted - // index, which must not be touched when no record was produced. - add(doc, docIndex) { - if (!Number.isInteger(docIndex) || docIndex < 0) { - throw new Error(INVALID_DOC_INDEX); - } - if (isString(doc)) { - const record = this._createStringRecord(doc, docIndex); - if (record) { - this.records.push(record); - } - return record; - } - const record = this._createObjectRecord(doc, docIndex); - this.records.push(record); - return record; - } - // Removes the record for the doc at the specified source-array (docs) index. - // Blank string docs have no record; callers may pass such an index and the - // splice is a no-op, but subsequent records still need their .i decremented - // to track the docs array that the caller is splicing in parallel. - removeAt(idx) { - if (!Number.isInteger(idx) || idx < 0) { - throw new Error(INVALID_DOC_INDEX); - } - - // Find and remove the record at this doc-index, if one exists. Records are - // typically sorted by .i but the algorithm doesn't depend on it — parsed - // indexes via setIndexRecords may arrive in arbitrary order. - for (let i = 0, len = this.records.length; i < len; i += 1) { - if (this.records[i].i === idx) { - this.records.splice(i, 1); - break; - } - } - - // Decrement every record whose source-array index is now stale. - for (let i = 0, len = this.records.length; i < len; i += 1) { - if (this.records[i].i > idx) { - this.records[i].i -= 1; - } - } - } - // Removes records for the docs at the specified source-array indices, then - // shifts every surviving record's .i down by the count of removed indices - // strictly less than it (mirrors removeAndShiftInvertedIndex's shift math). - // Invalid entries (non-integer, negative) in `indices` are dropped silently - // — removeAll's natural use case is "caller passed a list of matched doc - // indices"; asymmetric throw-vs-no-op would be more surprising than a clean - // filter. - removeAll(indices) { - const toRemove = new Set(); - for (const v of indices) { - if (Number.isInteger(v) && v >= 0) { - toRemove.add(v); - } - } - if (toRemove.size === 0) { - return; - } - this.records = this.records.filter(r => !toRemove.has(r.i)); - const sorted = Array.from(toRemove).sort((a, b) => a - b); - for (const record of this.records) { - // shift = count of removed indices strictly less than record.i - let lo = 0; - let hi = sorted.length; - while (lo < hi) { - const mid = lo + hi >>> 1; - if (sorted[mid] < record.i) lo = mid + 1;else hi = mid; - } - record.i -= lo; - } - } - getValueForItemAtKeyId(item, keyId) { - return item[this._keysMap[keyId]]; - } - size() { - return this.records.length; - } - _createStringRecord(doc, docIndex) { - if (!isDefined(doc) || isBlank(doc)) { - return null; - } - return { - v: doc, - i: docIndex, - n: this.norm.get(doc) - }; - } - _createObjectRecord(doc, docIndex) { - const record = { - i: docIndex, - $: {} - }; - - // Iterate over every key (i.e, path), and fetch the value at that key - for (let keyIndex = 0, keyLen = this.keys.length; keyIndex < keyLen; keyIndex++) { - const key = this.keys[keyIndex]; - const value = key.getFn ? key.getFn(doc) : this.getFn(doc, key.path); - if (!isDefined(value)) { - continue; - } - if (isArray(value)) { - const subRecords = []; - for (let i = 0, len = value.length; i < len; i += 1) { - const item = value[i]; - if (!isDefined(item)) { - continue; - } - if (isString(item)) { - // Custom getFn returning plain string array (backward compat) - if (!isBlank(item)) { - const subRecord = { - v: item, - i: i, - n: this.norm.get(item) - }; - subRecords.push(subRecord); - } - } else if (isDefined(item.v)) { - // Default get() returns {v, i} objects with original array indices - const text = isString(item.v) ? item.v : toString(item.v); - if (!isBlank(text)) { - const subRecord = { - v: text, - i: item.i, - n: this.norm.get(text) - }; - subRecords.push(subRecord); - } - } - } - record.$[keyIndex] = subRecords; - } else if (isString(value) && !isBlank(value)) { - const subRecord = { - v: value, - n: this.norm.get(value) - }; - record.$[keyIndex] = subRecord; - } - } - return record; - } - toJSON() { - return { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - keys: this.keys.map(({ - getFn, - ...key - }) => key), - records: this.records - }; - } -} -function createIndex(keys, docs, { - getFn = Config.getFn, - fieldNormWeight = Config.fieldNormWeight -} = {}) { - const myIndex = new FuseIndex({ - getFn, - fieldNormWeight - }); - myIndex.setKeys(keys.map(createKey)); - myIndex.setSources(docs); - myIndex.create(); - return myIndex; +//#endregion +//#region src/tools/FuseIndex.ts +var FuseIndex = class { + constructor({ getFn = Config.getFn, fieldNormWeight = Config.fieldNormWeight } = {}) { + this.norm = norm(fieldNormWeight, 3); + this.getFn = getFn; + this.isCreated = false; + this.docs = []; + this.keys = []; + this._keysMap = {}; + this.setIndexRecords(); + } + setSources(docs = []) { + this.docs = docs; + } + setIndexRecords(records = []) { + this.records = records; + } + setKeys(keys = []) { + this.keys = keys; + this._keysMap = {}; + keys.forEach((key, idx) => { + this._keysMap[key.id] = idx; + }); + } + create() { + if (this.isCreated || !this.docs.length) return; + this.isCreated = true; + const len = this.docs.length; + this.records = new Array(len); + let recordCount = 0; + if (isString(this.docs[0])) for (let i = 0; i < len; i++) { + const record = this._createStringRecord(this.docs[i], i); + if (record) this.records[recordCount++] = record; + } + else for (let i = 0; i < len; i++) this.records[recordCount++] = this._createObjectRecord(this.docs[i], i); + this.records.length = recordCount; + this.norm.clear(); + } + add(doc, docIndex) { + if (!Number.isInteger(docIndex) || docIndex < 0) throw new Error(INVALID_DOC_INDEX); + if (isString(doc)) { + const record = this._createStringRecord(doc, docIndex); + if (record) this.records.push(record); + return record; + } + const record = this._createObjectRecord(doc, docIndex); + this.records.push(record); + return record; + } + removeAt(idx) { + if (!Number.isInteger(idx) || idx < 0) throw new Error(INVALID_DOC_INDEX); + for (let i = 0, len = this.records.length; i < len; i += 1) if (this.records[i].i === idx) { + this.records.splice(i, 1); + break; + } + for (let i = 0, len = this.records.length; i < len; i += 1) if (this.records[i].i > idx) this.records[i].i -= 1; + } + removeAll(indices) { + const toRemove = /* @__PURE__ */ new Set(); + for (const v of indices) if (Number.isInteger(v) && v >= 0) toRemove.add(v); + if (toRemove.size === 0) return; + this.records = this.records.filter((r) => !toRemove.has(r.i)); + const sorted = Array.from(toRemove).sort((a, b) => a - b); + for (const record of this.records) { + let lo = 0; + let hi = sorted.length; + while (lo < hi) { + const mid = lo + hi >>> 1; + if (sorted[mid] < record.i) lo = mid + 1; + else hi = mid; + } + record.i -= lo; + } + } + getValueForItemAtKeyId(item, keyId) { + return item[this._keysMap[keyId]]; + } + size() { + return this.records.length; + } + _createStringRecord(doc, docIndex) { + if (!isDefined(doc) || isBlank(doc)) return null; + return { + v: doc, + i: docIndex, + n: this.norm.get(doc) + }; + } + _createObjectRecord(doc, docIndex) { + const record = { + i: docIndex, + $: {} + }; + for (let keyIndex = 0, keyLen = this.keys.length; keyIndex < keyLen; keyIndex++) { + const key = this.keys[keyIndex]; + const value = key.getFn ? key.getFn(doc) : this.getFn(doc, key.path); + if (!isDefined(value)) continue; + if (isArray(value)) { + const subRecords = []; + for (let i = 0, len = value.length; i < len; i += 1) { + const item = value[i]; + if (!isDefined(item)) continue; + if (isString(item)) { + if (!isBlank(item)) { + const subRecord = { + v: item, + i, + n: this.norm.get(item) + }; + subRecords.push(subRecord); + } + } else if (isDefined(item.v)) { + const text = isString(item.v) ? item.v : toString(item.v); + if (!isBlank(text)) { + const subRecord = { + v: text, + i: item.i, + n: this.norm.get(text) + }; + subRecords.push(subRecord); + } + } + } + record.$[keyIndex] = subRecords; + } else if (isString(value) && !isBlank(value)) { + const subRecord = { + v: value, + n: this.norm.get(value) + }; + record.$[keyIndex] = subRecord; + } + } + return record; + } + toJSON() { + return { + keys: this.keys.map(({ getFn, ...key }) => key), + records: this.records + }; + } +}; +function createIndex(keys, docs, { getFn = Config.getFn, fieldNormWeight = Config.fieldNormWeight } = {}) { + const myIndex = new FuseIndex({ + getFn, + fieldNormWeight + }); + myIndex.setKeys(keys.map(createKey)); + myIndex.setSources(docs); + myIndex.create(); + return myIndex; } +//#endregion +//#region src/search/bitap/convertMaskToIndices.ts function convertMaskToIndices(matchmask = [], minMatchCharLength = Config.minMatchCharLength) { - const indices = []; - let start = -1; - let end = -1; - let i = 0; - for (let len = matchmask.length; i < len; i += 1) { - const match = matchmask[i]; - if (match && start === -1) { - start = i; - } else if (!match && start !== -1) { - end = i - 1; - if (end - start + 1 >= minMatchCharLength) { - indices.push([start, end]); - } - start = -1; - } - } - - // (i-1 - start) + 1 => i - start - if (matchmask[i - 1] && i - start >= minMatchCharLength) { - indices.push([start, i - 1]); - } - return indices; + const indices = []; + let start = -1; + let end = -1; + let i = 0; + for (let len = matchmask.length; i < len; i += 1) { + const match = matchmask[i]; + if (match && start === -1) start = i; + else if (!match && start !== -1) { + end = i - 1; + if (end - start + 1 >= minMatchCharLength) indices.push([start, end]); + start = -1; + } + } + if (matchmask[i - 1] && i - start >= minMatchCharLength) indices.push([start, i - 1]); + return indices; } -// Machine word size -const MAX_BITS = 32; - -function search(text, pattern, patternAlphabet, { - location = Config.location, - distance = Config.distance, - threshold = Config.threshold, - findAllMatches = Config.findAllMatches, - minMatchCharLength = Config.minMatchCharLength, - includeMatches = Config.includeMatches, - ignoreLocation = Config.ignoreLocation -} = {}) { - if (pattern.length > MAX_BITS) { - throw new Error(PATTERN_LENGTH_TOO_LARGE(MAX_BITS)); - } - const patternLen = pattern.length; - // Set starting location at beginning text and initialize the alphabet. - const textLen = text.length; - // Handle the case when location > text.length - const expectedLocation = Math.max(0, Math.min(location, textLen)); - // Highest score beyond which we give up. - let currentThreshold = threshold; - // Is there a nearby exact match? (speedup) - let bestLocation = expectedLocation; - - // Inlined score computation — avoids object allocation per call in hot loops. - // See ./computeScore.ts for the documented version of this formula. - const calcScore = (errors, currentLocation) => { - const accuracy = errors / patternLen; - if (ignoreLocation) return accuracy; - const proximity = Math.abs(expectedLocation - currentLocation); - if (!distance) return proximity ? 1.0 : accuracy; - return accuracy + proximity / distance; - }; - - // Performance: only computer matches when the minMatchCharLength > 1 - // OR if `includeMatches` is true. - const computeMatches = minMatchCharLength > 1 || includeMatches; - // A mask of the matches, used for building the indices - const matchMask = computeMatches ? Array(textLen) : []; - let index; - - // Get all exact matches, here for speed up - while ((index = text.indexOf(pattern, bestLocation)) > -1) { - const score = calcScore(0, index); - currentThreshold = Math.min(score, currentThreshold); - bestLocation = index + patternLen; - if (computeMatches) { - let i = 0; - while (i < patternLen) { - matchMask[index + i] = 1; - i += 1; - } - } - } - - // Reset the best location - bestLocation = -1; - let lastBitArr = []; - let finalScore = 1; - let bestErrors = 0; - let binMax = patternLen + textLen; - const mask = 1 << patternLen - 1; - for (let i = 0; i < patternLen; i += 1) { - // Scan for the best match; each iteration allows for one more error. - // Run a binary search to determine how far from the match location we can stray - // at this error level. - let binMin = 0; - let binMid = binMax; - while (binMin < binMid) { - const score = calcScore(i, expectedLocation + binMid); - if (score <= currentThreshold) { - binMin = binMid; - } else { - binMax = binMid; - } - binMid = Math.floor((binMax - binMin) / 2 + binMin); - } - - // Use the result from this iteration as the maximum for the next. - binMax = binMid; - let start = Math.max(1, expectedLocation - binMid + 1); - const finish = findAllMatches ? textLen : Math.min(expectedLocation + binMid, textLen) + patternLen; - - // Initialize the bit array - const bitArr = Array(finish + 2); - bitArr[finish + 1] = (1 << i) - 1; - for (let j = finish; j >= start; j -= 1) { - const currentLocation = j - 1; - const charMatch = patternAlphabet[text[currentLocation]]; - - // First pass: exact match - bitArr[j] = (bitArr[j + 1] << 1 | 1) & charMatch; - - // Subsequent passes: fuzzy match - if (i) { - bitArr[j] |= (lastBitArr[j + 1] | lastBitArr[j]) << 1 | 1 | lastBitArr[j + 1]; - } - if (bitArr[j] & mask) { - finalScore = calcScore(i, currentLocation); - - // This match will almost certainly be better than any existing match. - // But check anyway. - if (finalScore <= currentThreshold) { - // Indeed it is - currentThreshold = finalScore; - bestLocation = currentLocation; - bestErrors = i; - - // Already passed `loc`, downhill from here on in. - if (bestLocation <= expectedLocation) { - break; - } - - // When passing `bestLocation`, don't exceed our current distance from `expectedLocation`. - start = Math.max(1, 2 * expectedLocation - bestLocation); - } - } - } - - // No hope for a (better) match at greater error levels. - const score = calcScore(i + 1, expectedLocation); - if (score > currentThreshold) { - break; - } - lastBitArr = bitArr; - } - - // Fill matchMask across the matched window only. Bitap anchors a match at - // bestLocation (the start), spanning patternLen characters plus up to - // bestErrors extra characters when errors are text-side insertions. Marking - // alphabet positions in that window keeps the highlight indices honest about - // what actually matched, instead of every pattern-alphabet character the - // scan happened to visit. - if (computeMatches && bestLocation >= 0) { - const matchEnd = Math.min(textLen - 1, bestLocation + patternLen - 1 + bestErrors); - for (let k = bestLocation; k <= matchEnd; k += 1) { - if (patternAlphabet[text[k]]) { - matchMask[k] = 1; - } - } - } - const result = { - isMatch: bestLocation >= 0, - // Count exact matches (those with a score of 0) to be "almost" exact - score: Math.max(0.001, finalScore) - }; - if (computeMatches) { - const indices = convertMaskToIndices(matchMask, minMatchCharLength); - if (!indices.length) { - result.isMatch = false; - } else if (includeMatches) { - result.indices = indices; - } - } - return result; +//#endregion +//#region src/search/bitap/search.ts +function search(text, pattern, patternAlphabet, { location = Config.location, distance = Config.distance, threshold = Config.threshold, findAllMatches = Config.findAllMatches, minMatchCharLength = Config.minMatchCharLength, includeMatches = Config.includeMatches, ignoreLocation = Config.ignoreLocation } = {}) { + if (pattern.length > 32) throw new Error(PATTERN_LENGTH_TOO_LARGE(32)); + const patternLen = pattern.length; + const textLen = text.length; + const expectedLocation = Math.max(0, Math.min(location, textLen)); + let currentThreshold = threshold; + let bestLocation = expectedLocation; + const calcScore = (errors, currentLocation) => { + const accuracy = errors / patternLen; + if (ignoreLocation) return accuracy; + const proximity = Math.abs(expectedLocation - currentLocation); + if (!distance) return proximity ? 1 : accuracy; + return accuracy + proximity / distance; + }; + const computeMatches = minMatchCharLength > 1 || includeMatches; + const matchMask = computeMatches ? Array(textLen) : []; + let index; + while ((index = text.indexOf(pattern, bestLocation)) > -1) { + const score = calcScore(0, index); + currentThreshold = Math.min(score, currentThreshold); + bestLocation = index + patternLen; + if (computeMatches) { + let i = 0; + while (i < patternLen) { + matchMask[index + i] = 1; + i += 1; + } + } + } + bestLocation = -1; + let lastBitArr = []; + let finalScore = 1; + let bestErrors = 0; + let binMax = patternLen + textLen; + const mask = 1 << patternLen - 1; + for (let i = 0; i < patternLen; i += 1) { + let binMin = 0; + let binMid = binMax; + while (binMin < binMid) { + if (calcScore(i, expectedLocation + binMid) <= currentThreshold) binMin = binMid; + else binMax = binMid; + binMid = Math.floor((binMax - binMin) / 2 + binMin); + } + binMax = binMid; + let start = Math.max(1, expectedLocation - binMid + 1); + const finish = findAllMatches ? textLen : Math.min(expectedLocation + binMid, textLen) + patternLen; + const bitArr = Array(finish + 2); + bitArr[finish + 1] = (1 << i) - 1; + for (let j = finish; j >= start; j -= 1) { + const currentLocation = j - 1; + const charMatch = patternAlphabet[text[currentLocation]]; + bitArr[j] = (bitArr[j + 1] << 1 | 1) & charMatch; + if (i) bitArr[j] |= (lastBitArr[j + 1] | lastBitArr[j]) << 1 | 1 | lastBitArr[j + 1]; + if (bitArr[j] & mask) { + finalScore = calcScore(i, currentLocation); + if (finalScore <= currentThreshold) { + currentThreshold = finalScore; + bestLocation = currentLocation; + bestErrors = i; + if (bestLocation <= expectedLocation) break; + start = Math.max(1, 2 * expectedLocation - bestLocation); + } + } + } + if (calcScore(i + 1, expectedLocation) > currentThreshold) break; + lastBitArr = bitArr; + } + if (computeMatches && bestLocation >= 0) { + const matchEnd = Math.min(textLen - 1, bestLocation + patternLen - 1 + bestErrors); + for (let k = bestLocation; k <= matchEnd; k += 1) if (patternAlphabet[text[k]]) matchMask[k] = 1; + } + const result = { + isMatch: bestLocation >= 0, + score: Math.max(.001, finalScore) + }; + if (computeMatches) { + const indices = convertMaskToIndices(matchMask, minMatchCharLength); + if (!indices.length) result.isMatch = false; + else if (includeMatches) result.indices = indices; + } + return result; } +//#endregion +//#region src/search/bitap/createPatternAlphabet.ts function createPatternAlphabet(pattern) { - const mask = {}; - for (let i = 0, len = pattern.length; i < len; i += 1) { - const char = pattern.charAt(i); - mask[char] = (mask[char] || 0) | 1 << len - i - 1; - } - return mask; + const mask = {}; + for (let i = 0, len = pattern.length; i < len; i += 1) { + const char = pattern.charAt(i); + mask[char] = (mask[char] || 0) | 1 << len - i - 1; + } + return mask; } +//#endregion +//#region src/helpers/mergeIndices.ts function mergeIndices(indices) { - if (indices.length <= 1) return indices; - indices.sort((a, b) => a[0] - b[0] || a[1] - b[1]); - const merged = [indices[0]]; - for (let i = 1, len = indices.length; i < len; i += 1) { - const last = merged[merged.length - 1]; - const curr = indices[i]; - if (curr[0] <= last[1] + 1) { - last[1] = Math.max(last[1], curr[1]); - } else { - merged.push(curr); - } - } - return merged; + if (indices.length <= 1) return indices; + indices.sort((a, b) => a[0] - b[0] || a[1] - b[1]); + const merged = [indices[0]]; + for (let i = 1, len = indices.length; i < len; i += 1) { + const last = merged[merged.length - 1]; + const curr = indices[i]; + if (curr[0] <= last[1] + 1) last[1] = Math.max(last[1], curr[1]); + else merged.push(curr); + } + return merged; } -// Characters that survive NFD normalization unchanged and need explicit mapping +//#endregion +//#region src/helpers/diacritics.ts const NON_DECOMPOSABLE_MAP = { - '\u0142': 'l', - // ł - '\u0141': 'L', - // Ł - '\u0111': 'd', - // đ - '\u0110': 'D', - // Đ - '\u00F8': 'o', - // ø - '\u00D8': 'O', - // Ø - '\u0127': 'h', - // ħ - '\u0126': 'H', - // Ħ - '\u0167': 't', - // ŧ - '\u0166': 'T', - // Ŧ - '\u0131': 'i', - // ı - '\u00DF': 'ss' // ß + "ł": "l", + "Ł": "L", + "đ": "d", + "Đ": "D", + "ø": "o", + "Ø": "O", + "ħ": "h", + "Ħ": "H", + "ŧ": "t", + "Ŧ": "T", + "ı": "i", + "ß": "ss" +}; +const NON_DECOMPOSABLE_RE = new RegExp("[" + Object.keys(NON_DECOMPOSABLE_MAP).join("") + "]", "g"); +const stripDiacritics = typeof String.prototype.normalize === "function" ? (str) => str.normalize("NFD").replace(/[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]/g, "").replace(NON_DECOMPOSABLE_RE, (ch) => NON_DECOMPOSABLE_MAP[ch]) : (str) => str; + +//#endregion +//#region src/search/bitap/index.ts +var BitapSearch = class { + constructor(pattern, { location = Config.location, threshold = Config.threshold, distance = Config.distance, includeMatches = Config.includeMatches, findAllMatches = Config.findAllMatches, minMatchCharLength = Config.minMatchCharLength, isCaseSensitive = Config.isCaseSensitive, ignoreDiacritics = Config.ignoreDiacritics, ignoreLocation = Config.ignoreLocation } = {}) { + this.options = { + location, + threshold, + distance, + includeMatches, + findAllMatches, + minMatchCharLength, + isCaseSensitive, + ignoreDiacritics, + ignoreLocation + }; + pattern = isCaseSensitive ? pattern : pattern.toLowerCase(); + pattern = ignoreDiacritics ? stripDiacritics(pattern) : pattern; + this.pattern = pattern; + this.chunks = []; + if (!this.pattern.length) return; + const addChunk = (pattern, startIndex) => { + this.chunks.push({ + pattern, + alphabet: createPatternAlphabet(pattern), + startIndex + }); + }; + const len = this.pattern.length; + if (len > 32) { + let i = 0; + const remainder = len % 32; + const end = len - remainder; + while (i < end) { + addChunk(this.pattern.substr(i, 32), i); + i += 32; + } + if (remainder) { + const startIndex = len - 32; + addChunk(this.pattern.substr(startIndex), startIndex); + } + } else addChunk(this.pattern, 0); + } + searchIn(text) { + const { isCaseSensitive, ignoreDiacritics, includeMatches } = this.options; + text = isCaseSensitive ? text : text.toLowerCase(); + text = ignoreDiacritics ? stripDiacritics(text) : text; + if (this.pattern === text) { + const result = { + isMatch: true, + score: 0 + }; + if (includeMatches) result.indices = [[0, text.length - 1]]; + return result; + } + const { location, distance, threshold, findAllMatches, minMatchCharLength, ignoreLocation } = this.options; + const allIndices = []; + let totalScore = 0; + let hasMatches = false; + this.chunks.forEach(({ pattern, alphabet, startIndex }) => { + const { isMatch, score, indices } = search(text, pattern, alphabet, { + location: location + startIndex, + distance, + threshold, + findAllMatches, + minMatchCharLength, + includeMatches, + ignoreLocation + }); + if (isMatch) hasMatches = true; + totalScore += score; + if (isMatch && indices) allIndices.push(...indices); + }); + const result = { + isMatch: hasMatches, + score: hasMatches ? totalScore / this.chunks.length : 1 + }; + if (hasMatches && includeMatches) result.indices = mergeIndices(allIndices); + return result; + } }; -const NON_DECOMPOSABLE_RE = new RegExp('[' + Object.keys(NON_DECOMPOSABLE_MAP).join('') + ']', 'g'); -const stripDiacritics = typeof String.prototype.normalize === 'function' ? str => str.normalize('NFD').replace(/[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]/g, '').replace(NON_DECOMPOSABLE_RE, ch => NON_DECOMPOSABLE_MAP[ch]) : str => str; - -class BitapSearch { - constructor(pattern, { - location = Config.location, - threshold = Config.threshold, - distance = Config.distance, - includeMatches = Config.includeMatches, - findAllMatches = Config.findAllMatches, - minMatchCharLength = Config.minMatchCharLength, - isCaseSensitive = Config.isCaseSensitive, - ignoreDiacritics = Config.ignoreDiacritics, - ignoreLocation = Config.ignoreLocation - } = {}) { - this.options = { - location, - threshold, - distance, - includeMatches, - findAllMatches, - minMatchCharLength, - isCaseSensitive, - ignoreDiacritics, - ignoreLocation - }; - pattern = isCaseSensitive ? pattern : pattern.toLowerCase(); - pattern = ignoreDiacritics ? stripDiacritics(pattern) : pattern; - this.pattern = pattern; - this.chunks = []; - if (!this.pattern.length) { - return; - } - const addChunk = (pattern, startIndex) => { - this.chunks.push({ - pattern, - alphabet: createPatternAlphabet(pattern), - startIndex - }); - }; - const len = this.pattern.length; - if (len > MAX_BITS) { - let i = 0; - const remainder = len % MAX_BITS; - const end = len - remainder; - while (i < end) { - addChunk(this.pattern.substr(i, MAX_BITS), i); - i += MAX_BITS; - } - if (remainder) { - const startIndex = len - MAX_BITS; - addChunk(this.pattern.substr(startIndex), startIndex); - } - } else { - addChunk(this.pattern, 0); - } - } - searchIn(text) { - const { - isCaseSensitive, - ignoreDiacritics, - includeMatches - } = this.options; - text = isCaseSensitive ? text : text.toLowerCase(); - text = ignoreDiacritics ? stripDiacritics(text) : text; - - // Exact match - if (this.pattern === text) { - const result = { - isMatch: true, - score: 0 - }; - if (includeMatches) { - result.indices = [[0, text.length - 1]]; - } - return result; - } - - // Otherwise, use Bitap algorithm - const { - location, - distance, - threshold, - findAllMatches, - minMatchCharLength, - ignoreLocation - } = this.options; - const allIndices = []; - let totalScore = 0; - let hasMatches = false; - this.chunks.forEach(({ - pattern, - alphabet, - startIndex - }) => { - const { - isMatch, - score, - indices - } = search(text, pattern, alphabet, { - location: location + startIndex, - distance, - threshold, - findAllMatches, - minMatchCharLength, - includeMatches, - ignoreLocation - }); - if (isMatch) { - hasMatches = true; - } - totalScore += score; - if (isMatch && indices) { - allIndices.push(...indices); - } - }); - const result = { - isMatch: hasMatches, - score: hasMatches ? totalScore / this.chunks.length : 1 - }; - if (hasMatches && includeMatches) { - result.indices = mergeIndices(allIndices); - } - return result; - } -} - -// ── Matcher interface ───────────────────────────────────────────── -// -// Each matcher is a lightweight object with a type tag and a search -// function. No class hierarchy needed — the search logic for most -// matchers is a one-liner. - -// ── Matcher definition ──────────────────────────────────────────── -// -// A definition pairs the detection regexes (used by parseQuery to -// recognize string-syntax operators like ^, =, !) with a factory -// that creates a Matcher instance. - -// Whether a matcher type can return multiple index ranges -const MULTI_MATCH_TYPES = new Set(['fuzzy', 'include']); -// Whether a matcher type is an inverse match +//#endregion +//#region src/search/extended/matchers.ts +const MULTI_MATCH_TYPES = new Set(["fuzzy", "include"]); function isInverse(type) { - return type.startsWith('inverse'); + return type.startsWith("inverse"); } - -// ── Matcher definitions ─────────────────────────────────────────── -// -// Order matters — parseQuery tries each in sequence and uses the -// first match. FuzzyMatch (catch-all) must be last. - -// prettier-ignore const matchers = [ -// =term — exact match -{ - type: 'exact', - multiRegex: /^="(.*)"$/, - singleRegex: /^=(.*)$/, - create: pattern => ({ - type: 'exact', - search(text) { - const isMatch = text === pattern; - return { - isMatch, - score: isMatch ? 0 : 1, - indices: [0, pattern.length - 1] - }; - } - }) -}, -// 'term — include (substring) match -{ - type: 'include', - multiRegex: /^'"(.*)"$/, - singleRegex: /^'(.*)$/, - create: pattern => ({ - type: 'include', - search(text) { - let location = 0; - let index; - const indices = []; - const patternLen = pattern.length; - while ((index = text.indexOf(pattern, location)) > -1) { - location = index + patternLen; - indices.push([index, location - 1]); - } - const isMatch = !!indices.length; - return { - isMatch, - score: isMatch ? 0 : 1, - indices - }; - } - }) -}, -// ^term — prefix match -{ - type: 'prefix-exact', - multiRegex: /^\^"(.*)"$/, - singleRegex: /^\^(.*)$/, - create: pattern => ({ - type: 'prefix-exact', - search(text) { - const isMatch = text.startsWith(pattern); - return { - isMatch, - score: isMatch ? 0 : 1, - indices: [0, pattern.length - 1] - }; - } - }) -}, -// !^term — inverse prefix match -{ - type: 'inverse-prefix-exact', - multiRegex: /^!\^"(.*)"$/, - singleRegex: /^!\^(.*)$/, - create: pattern => ({ - type: 'inverse-prefix-exact', - search(text) { - const isMatch = !text.startsWith(pattern); - return { - isMatch, - score: isMatch ? 0 : 1, - indices: [0, text.length - 1] - }; - } - }) -}, -// !term$ — inverse suffix match -{ - type: 'inverse-suffix-exact', - multiRegex: /^!"(.*)"\$$/, - singleRegex: /^!(.*)\$$/, - create: pattern => ({ - type: 'inverse-suffix-exact', - search(text) { - const isMatch = !text.endsWith(pattern); - return { - isMatch, - score: isMatch ? 0 : 1, - indices: [0, text.length - 1] - }; - } - }) -}, -// term$ — suffix match -{ - type: 'suffix-exact', - multiRegex: /^"(.*)"\$$/, - singleRegex: /^(.*)\$$/, - create: pattern => ({ - type: 'suffix-exact', - search(text) { - const isMatch = text.endsWith(pattern); - return { - isMatch, - score: isMatch ? 0 : 1, - indices: [text.length - pattern.length, text.length - 1] - }; - } - }) -}, -// !term — inverse exact (does not contain) -{ - type: 'inverse-exact', - multiRegex: /^!"(.*)"$/, - singleRegex: /^!(.*)$/, - create: pattern => ({ - type: 'inverse-exact', - search(text) { - const isMatch = text.indexOf(pattern) === -1; - return { - isMatch, - score: isMatch ? 0 : 1, - indices: [0, text.length - 1] - }; - } - }) -}, -// term — fuzzy match (catch-all, must be last) -{ - type: 'fuzzy', - multiRegex: /^"(.*)"$/, - singleRegex: /^(.*)$/, - create: (pattern, options = {}) => { - const bitap = new BitapSearch(pattern, { - location: options.location ?? Config.location, - threshold: options.threshold ?? Config.threshold, - distance: options.distance ?? Config.distance, - includeMatches: options.includeMatches ?? Config.includeMatches, - findAllMatches: options.findAllMatches ?? Config.findAllMatches, - minMatchCharLength: options.minMatchCharLength ?? Config.minMatchCharLength, - isCaseSensitive: options.isCaseSensitive ?? Config.isCaseSensitive, - ignoreDiacritics: options.ignoreDiacritics ?? Config.ignoreDiacritics, - ignoreLocation: options.ignoreLocation ?? Config.ignoreLocation - }); - return { - type: 'fuzzy', - search(text) { - return bitap.searchIn(text); - } - }; - } -}]; - + { + type: "exact", + multiRegex: /^="(.*)"$/, + singleRegex: /^=(.*)$/, + create: (pattern) => ({ + type: "exact", + search(text) { + const isMatch = text === pattern; + return { + isMatch, + score: isMatch ? 0 : 1, + indices: [0, pattern.length - 1] + }; + } + }) + }, + { + type: "include", + multiRegex: /^'"(.*)"$/, + singleRegex: /^'(.*)$/, + create: (pattern) => ({ + type: "include", + search(text) { + let location = 0; + let index; + const indices = []; + const patternLen = pattern.length; + while ((index = text.indexOf(pattern, location)) > -1) { + location = index + patternLen; + indices.push([index, location - 1]); + } + const isMatch = !!indices.length; + return { + isMatch, + score: isMatch ? 0 : 1, + indices + }; + } + }) + }, + { + type: "prefix-exact", + multiRegex: /^\^"(.*)"$/, + singleRegex: /^\^(.*)$/, + create: (pattern) => ({ + type: "prefix-exact", + search(text) { + const isMatch = text.startsWith(pattern); + return { + isMatch, + score: isMatch ? 0 : 1, + indices: [0, pattern.length - 1] + }; + } + }) + }, + { + type: "inverse-prefix-exact", + multiRegex: /^!\^"(.*)"$/, + singleRegex: /^!\^(.*)$/, + create: (pattern) => ({ + type: "inverse-prefix-exact", + search(text) { + const isMatch = !text.startsWith(pattern); + return { + isMatch, + score: isMatch ? 0 : 1, + indices: [0, text.length - 1] + }; + } + }) + }, + { + type: "inverse-suffix-exact", + multiRegex: /^!"(.*)"\$$/, + singleRegex: /^!(.*)\$$/, + create: (pattern) => ({ + type: "inverse-suffix-exact", + search(text) { + const isMatch = !text.endsWith(pattern); + return { + isMatch, + score: isMatch ? 0 : 1, + indices: [0, text.length - 1] + }; + } + }) + }, + { + type: "suffix-exact", + multiRegex: /^"(.*)"\$$/, + singleRegex: /^(.*)\$$/, + create: (pattern) => ({ + type: "suffix-exact", + search(text) { + const isMatch = text.endsWith(pattern); + return { + isMatch, + score: isMatch ? 0 : 1, + indices: [text.length - pattern.length, text.length - 1] + }; + } + }) + }, + { + type: "inverse-exact", + multiRegex: /^!"(.*)"$/, + singleRegex: /^!(.*)$/, + create: (pattern) => ({ + type: "inverse-exact", + search(text) { + const isMatch = text.indexOf(pattern) === -1; + return { + isMatch, + score: isMatch ? 0 : 1, + indices: [0, text.length - 1] + }; + } + }) + }, + { + type: "fuzzy", + multiRegex: /^"(.*)"$/, + singleRegex: /^(.*)$/, + create: (pattern, options = {}) => { + const bitap = new BitapSearch(pattern, { + location: options.location ?? Config.location, + threshold: options.threshold ?? Config.threshold, + distance: options.distance ?? Config.distance, + includeMatches: options.includeMatches ?? Config.includeMatches, + findAllMatches: options.findAllMatches ?? Config.findAllMatches, + minMatchCharLength: options.minMatchCharLength ?? Config.minMatchCharLength, + isCaseSensitive: options.isCaseSensitive ?? Config.isCaseSensitive, + ignoreDiacritics: options.ignoreDiacritics ?? Config.ignoreDiacritics, + ignoreLocation: options.ignoreLocation ?? Config.ignoreLocation + }); + return { + type: "fuzzy", + search(text) { + return bitap.searchIn(text); + } + }; + } + } +]; + +//#endregion +//#region src/search/extended/parseQuery.ts const matchersLen = matchers.length; -const ESCAPED_PIPE = '\u0000'; // placeholder for escaped \| -const OR_TOKEN = '|'; - -// Tokenize a query string into individual search terms. -// Respects multi-match quoted tokens like ="said "test"" or ^"hello world"$ -// where inner spaces and quotes are part of the token. +const ESCAPED_PIPE = "\0"; +const OR_TOKEN = "|"; function tokenize(pattern) { - const tokens = []; - const len = pattern.length; - let i = 0; - while (i < len) { - // Skip spaces - while (i < len && pattern[i] === ' ') i++; - if (i >= len) break; - - // Scan past prefix characters (=, !, ^, ') to see if a quote follows - let j = i; - while (j < len && pattern[j] !== ' ' && pattern[j] !== '"') j++; - if (j < len && pattern[j] === '"') { - // Multi-match token: prefix + "content" (possibly with inner quotes) - // Find the closing " that ends this token: - // it must be followed by optional $, then space or end-of-string - j++; // skip opening quote - while (j < len) { - if (pattern[j] === '"') { - // Check if this is the closing quote - const next = j + 1; - if (next >= len || pattern[next] === ' ') { - j++; // include closing quote - break; - } - if (pattern[next] === '$' && (next + 1 >= len || pattern[next + 1] === ' ')) { - j += 2; // include "$ - break; - } - } - j++; - } - tokens.push(pattern.substring(i, j)); - i = j; - } else { - // Regular (unquoted) token: read until space or end - while (j < len && pattern[j] !== ' ') j++; - tokens.push(pattern.substring(i, j)); - i = j; - } - } - return tokens; + const tokens = []; + const len = pattern.length; + let i = 0; + while (i < len) { + while (i < len && pattern[i] === " ") i++; + if (i >= len) break; + let j = i; + while (j < len && pattern[j] !== " " && pattern[j] !== "\"") j++; + if (j < len && pattern[j] === "\"") { + j++; + while (j < len) { + if (pattern[j] === "\"") { + const next = j + 1; + if (next >= len || pattern[next] === " ") { + j++; + break; + } + if (pattern[next] === "$" && (next + 1 >= len || pattern[next + 1] === " ")) { + j += 2; + break; + } + } + j++; + } + tokens.push(pattern.substring(i, j)); + i = j; + } else { + while (j < len && pattern[j] !== " ") j++; + tokens.push(pattern.substring(i, j)); + i = j; + } + } + return tokens; } function getMatch(pattern, exp) { - const matches = pattern.match(exp); - return matches ? matches[1] : null; + const matches = pattern.match(exp); + return matches ? matches[1] : null; } - -// Return a 2D array representation of the query, for simpler parsing. -// Example: -// "^core go$ | rb$ | py$ xy$" => [["^core", "go$"], ["rb$"], ["py$", "xy$"]] function parseQuery(pattern, options = {}) { - // Replace escaped \| with placeholder before splitting on | - const escaped = pattern.replace(/\\\|/g, ESCAPED_PIPE); - return escaped.split(OR_TOKEN).map(item => { - // Restore escaped pipes in each OR group - const restored = item.replace(/\u0000/g, '|'); - const query = tokenize(restored.trim()).filter(item => item && !!item.trim()); - const results = []; - for (let i = 0, len = query.length; i < len; i += 1) { - const queryItem = query[i]; - - // 1. Handle multiple query match (i.e, ones that are quoted, like `"hello world"`) - let found = false; - let idx = -1; - while (!found && ++idx < matchersLen) { - const def = matchers[idx]; - const token = getMatch(queryItem, def.multiRegex); - if (token) { - results.push(def.create(token, options)); - found = true; - } - } - if (found) { - continue; - } - - // 2. Handle single query matches (i.e, ones that are *not* quoted) - idx = -1; - while (++idx < matchersLen) { - const def = matchers[idx]; - const token = getMatch(queryItem, def.singleRegex); - if (token) { - results.push(def.create(token, options)); - break; - } - } - } - return results; - }); + return pattern.replace(/\\\|/g, ESCAPED_PIPE).split(OR_TOKEN).map((item) => { + const query = tokenize(item.replace(/\u0000/g, "|").trim()).filter((item) => item && !!item.trim()); + const results = []; + for (let i = 0, len = query.length; i < len; i += 1) { + const queryItem = query[i]; + let found = false; + let idx = -1; + while (!found && ++idx < matchersLen) { + const def = matchers[idx]; + const token = getMatch(queryItem, def.multiRegex); + if (token) { + results.push(def.create(token, options)); + found = true; + } + } + if (found) continue; + idx = -1; + while (++idx < matchersLen) { + const def = matchers[idx]; + const token = getMatch(queryItem, def.singleRegex); + if (token) { + results.push(def.create(token, options)); + break; + } + } + } + return results; + }); } -class ExtendedSearch { - constructor(pattern, { - isCaseSensitive = Config.isCaseSensitive, - ignoreDiacritics = Config.ignoreDiacritics, - includeMatches = Config.includeMatches, - minMatchCharLength = Config.minMatchCharLength, - ignoreLocation = Config.ignoreLocation, - findAllMatches = Config.findAllMatches, - location = Config.location, - threshold = Config.threshold, - distance = Config.distance - } = {}) { - this.query = null; - this.options = { - isCaseSensitive, - ignoreDiacritics, - includeMatches, - minMatchCharLength, - findAllMatches, - ignoreLocation, - location, - threshold, - distance - }; - pattern = isCaseSensitive ? pattern : pattern.toLowerCase(); - pattern = ignoreDiacritics ? stripDiacritics(pattern) : pattern; - this.pattern = pattern; - this.query = parseQuery(this.pattern, this.options); - } - static condition(_, options) { - return options.useExtendedSearch; - } - - // Note: searchIn operates on a single text value and sets hasInverse on the - // result when inverse patterns are involved. _searchObjectList uses this to - // switch from "ANY key" to "ALL keys" aggregation. See #712. - searchIn(text) { - const query = this.query; - if (!query) { - return { - isMatch: false, - score: 1 - }; - } - const { - includeMatches, - isCaseSensitive, - ignoreDiacritics - } = this.options; - text = isCaseSensitive ? text : text.toLowerCase(); - text = ignoreDiacritics ? stripDiacritics(text) : text; - let numMatches = 0; - const allIndices = []; - let totalScore = 0; - let hasInverse = false; - - // ORs - for (let i = 0, qLen = query.length; i < qLen; i += 1) { - const searchers = query[i]; - - // Reset indices - allIndices.length = 0; - numMatches = 0; - hasInverse = false; - - // ANDs - for (let j = 0, pLen = searchers.length; j < pLen; j += 1) { - const matcher = searchers[j]; - const { - isMatch, - indices, - score - } = matcher.search(text); - if (isMatch) { - numMatches += 1; - totalScore += score; - if (isInverse(matcher.type)) { - hasInverse = true; - } - if (includeMatches) { - if (MULTI_MATCH_TYPES.has(matcher.type)) { - allIndices.push(...indices); - } else { - allIndices.push(indices); - } - } - } else { - totalScore = 0; - numMatches = 0; - allIndices.length = 0; - hasInverse = false; - break; - } - } - - // OR condition, so if TRUE, return - if (numMatches) { - const result = { - isMatch: true, - score: totalScore / numMatches - }; - if (hasInverse) { - result.hasInverse = true; - } - if (includeMatches) { - result.indices = mergeIndices(allIndices); - } - return result; - } - } - - // Nothing was matched - return { - isMatch: false, - score: 1 - }; - } -} +//#endregion +//#region src/search/extended/index.ts +var ExtendedSearch = class { + constructor(pattern, { isCaseSensitive = Config.isCaseSensitive, ignoreDiacritics = Config.ignoreDiacritics, includeMatches = Config.includeMatches, minMatchCharLength = Config.minMatchCharLength, ignoreLocation = Config.ignoreLocation, findAllMatches = Config.findAllMatches, location = Config.location, threshold = Config.threshold, distance = Config.distance } = {}) { + this.query = null; + this.options = { + isCaseSensitive, + ignoreDiacritics, + includeMatches, + minMatchCharLength, + findAllMatches, + ignoreLocation, + location, + threshold, + distance + }; + pattern = isCaseSensitive ? pattern : pattern.toLowerCase(); + pattern = ignoreDiacritics ? stripDiacritics(pattern) : pattern; + this.pattern = pattern; + this.query = parseQuery(this.pattern, this.options); + } + static condition(_, options) { + return options.useExtendedSearch; + } + searchIn(text) { + const query = this.query; + if (!query) return { + isMatch: false, + score: 1 + }; + const { includeMatches, isCaseSensitive, ignoreDiacritics } = this.options; + text = isCaseSensitive ? text : text.toLowerCase(); + text = ignoreDiacritics ? stripDiacritics(text) : text; + let numMatches = 0; + const allIndices = []; + let totalScore = 0; + let hasInverse = false; + for (let i = 0, qLen = query.length; i < qLen; i += 1) { + const searchers = query[i]; + allIndices.length = 0; + numMatches = 0; + hasInverse = false; + for (let j = 0, pLen = searchers.length; j < pLen; j += 1) { + const matcher = searchers[j]; + const { isMatch, indices, score } = matcher.search(text); + if (isMatch) { + numMatches += 1; + totalScore += score; + if (isInverse(matcher.type)) hasInverse = true; + if (includeMatches) if (MULTI_MATCH_TYPES.has(matcher.type)) allIndices.push(...indices); + else allIndices.push(indices); + } else { + totalScore = 0; + numMatches = 0; + allIndices.length = 0; + hasInverse = false; + break; + } + } + if (numMatches) { + const result = { + isMatch: true, + score: totalScore / numMatches + }; + if (hasInverse) result.hasInverse = true; + if (includeMatches) result.indices = mergeIndices(allIndices); + return result; + } + } + return { + isMatch: false, + score: 1 + }; + } +}; +//#endregion +//#region src/core/register.ts const registeredSearchers = []; function register(...args) { - registeredSearchers.push(...args); + registeredSearchers.push(...args); } function createSearcher(pattern, options) { - for (let i = 0, len = registeredSearchers.length; i < len; i += 1) { - const searcherClass = registeredSearchers[i]; - if (searcherClass.condition(pattern, options)) { - return new searcherClass(pattern, options); - } - } - return new BitapSearch(pattern, options); + for (let i = 0, len = registeredSearchers.length; i < len; i += 1) { + const searcherClass = registeredSearchers[i]; + if (searcherClass.condition(pattern, options)) return new searcherClass(pattern, options); + } + return new BitapSearch(pattern, options); } +//#endregion +//#region src/core/queryParser.ts const LogicalOperator = { - AND: '$and', - OR: '$or' + AND: "$and", + OR: "$or" }; const KeyType = { - PATH: '$path', - PATTERN: '$val' + PATH: "$path", + PATTERN: "$val" }; -const isExpression = query => !!(query[LogicalOperator.AND] || query[LogicalOperator.OR]); -const isPath = query => !!query[KeyType.PATH]; -const isLeaf = query => !isArray(query) && isObject(query) && !isExpression(query); -const convertToExplicit = query => ({ - [LogicalOperator.AND]: Object.keys(query).map(key => ({ - [key]: query[key] - })) -}); - -// When `auto` is `true`, the parse function will infer and initialize and add -// the appropriate `Searcher` instance -function parse(query, options, { - auto = true -} = {}) { - const next = query => { - // Keyless string entry: search across all keys - if (isString(query)) { - const obj = { - keyId: null, - pattern: query - }; - if (auto) { - obj.searcher = createSearcher(query, options); - } - return obj; - } - const keys = Object.keys(query); - const isQueryPath = isPath(query); - if (!isQueryPath && keys.length > 1 && !isExpression(query)) { - return next(convertToExplicit(query)); - } - if (isLeaf(query)) { - const key = isQueryPath ? query[KeyType.PATH] : keys[0]; - const pattern = isQueryPath ? query[KeyType.PATTERN] : query[key]; - if (!isString(pattern)) { - throw new Error(LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY(key)); - } - const obj = { - keyId: createKeyId(key), - pattern - }; - if (auto) { - obj.searcher = createSearcher(pattern, options); - } - return obj; - } - const node = { - children: [], - operator: keys[0] - }; - keys.forEach(key => { - const value = query[key]; - if (isArray(value)) { - value.forEach(item => { - node.children.push(next(item)); - }); - } - }); - return node; - }; - if (!isExpression(query)) { - query = convertToExplicit(query); - } - return next(query); +const isExpression = (query) => !!(query[LogicalOperator.AND] || query[LogicalOperator.OR]); +const isPath = (query) => !!query[KeyType.PATH]; +const isLeaf = (query) => !isArray(query) && isObject(query) && !isExpression(query); +const convertToExplicit = (query) => ({ [LogicalOperator.AND]: Object.keys(query).map((key) => ({ [key]: query[key] })) }); +function parse(query, options, { auto = true } = {}) { + const next = (query) => { + if (isString(query)) { + const obj = { + keyId: null, + pattern: query + }; + if (auto) obj.searcher = createSearcher(query, options); + return obj; + } + const keys = Object.keys(query); + const isQueryPath = isPath(query); + if (!isQueryPath && keys.length > 1 && !isExpression(query)) return next(convertToExplicit(query)); + if (isLeaf(query)) { + const key = isQueryPath ? query[KeyType.PATH] : keys[0]; + const pattern = isQueryPath ? query[KeyType.PATTERN] : query[key]; + if (!isString(pattern)) throw new Error(LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY(key)); + const obj = { + keyId: createKeyId(key), + pattern + }; + if (auto) obj.searcher = createSearcher(pattern, options); + return obj; + } + const node = { + children: [], + operator: keys[0] + }; + keys.forEach((key) => { + const value = query[key]; + if (isArray(value)) value.forEach((item) => { + node.children.push(next(item)); + }); + }); + return node; + }; + if (!isExpression(query)) query = convertToExplicit(query); + return next(query); } -function computeScoreSingle(matches, { - ignoreFieldNorm = Config.ignoreFieldNorm -}) { - let totalScore = 1; - matches.forEach(({ - key, - norm, - score - }) => { - const weight = key ? key.weight : null; - totalScore *= Math.pow(score === 0 && weight ? Number.EPSILON : score, (weight || 1) * (ignoreFieldNorm ? 1 : norm)); - }); - return totalScore; +//#endregion +//#region src/core/computeScore.ts +function computeScoreSingle(matches, { ignoreFieldNorm = Config.ignoreFieldNorm }) { + let totalScore = 1; + matches.forEach(({ key, norm, score }) => { + const weight = key ? key.weight : null; + totalScore *= Math.pow(score === 0 && weight ? Number.EPSILON : score, (weight || 1) * (ignoreFieldNorm ? 1 : norm)); + }); + return totalScore; } -function computeScore(results, { - ignoreFieldNorm = Config.ignoreFieldNorm -}) { - results.forEach(result => { - result.score = computeScoreSingle(result.matches, { - ignoreFieldNorm - }); - }); +function computeScore(results, { ignoreFieldNorm = Config.ignoreFieldNorm }) { + results.forEach((result) => { + result.score = computeScoreSingle(result.matches, { ignoreFieldNorm }); + }); } -// Max-heap by score: keeps the worst (highest) score at the top -// so we can efficiently evict it when a better result arrives. -class MaxHeap { - constructor(limit) { - this.limit = limit; - this.heap = []; - } - get size() { - return this.heap.length; - } - shouldInsert(score) { - return this.size < this.limit || score < this.heap[0].score; - } - insert(item) { - if (this.size < this.limit) { - this.heap.push(item); - this._bubbleUp(this.size - 1); - } else if (item.score < this.heap[0].score) { - this.heap[0] = item; - this._sinkDown(0); - } - } - extractSorted(sortFn) { - return this.heap.sort(sortFn); - } - _bubbleUp(i) { - const heap = this.heap; - while (i > 0) { - const parent = i - 1 >> 1; - if (heap[i].score <= heap[parent].score) break; - const tmp = heap[i]; - heap[i] = heap[parent]; - heap[parent] = tmp; - i = parent; - } - } - _sinkDown(i) { - const heap = this.heap; - const len = heap.length; - let largest = i; - do { - i = largest; - const left = 2 * i + 1; - const right = 2 * i + 2; - if (left < len && heap[left].score > heap[largest].score) { - largest = left; - } - if (right < len && heap[right].score > heap[largest].score) { - largest = right; - } - if (largest !== i) { - const tmp = heap[i]; - heap[i] = heap[largest]; - heap[largest] = tmp; - } - } while (largest !== i); - } -} +//#endregion +//#region src/tools/MaxHeap.ts +var MaxHeap = class { + constructor(limit) { + this.limit = limit; + this.heap = []; + } + get size() { + return this.heap.length; + } + shouldInsert(score) { + return this.size < this.limit || score < this.heap[0].score; + } + insert(item) { + if (this.size < this.limit) { + this.heap.push(item); + this._bubbleUp(this.size - 1); + } else if (item.score < this.heap[0].score) { + this.heap[0] = item; + this._sinkDown(0); + } + } + extractSorted(sortFn) { + return this.heap.sort(sortFn); + } + _bubbleUp(i) { + const heap = this.heap; + while (i > 0) { + const parent = i - 1 >> 1; + if (heap[i].score <= heap[parent].score) break; + const tmp = heap[i]; + heap[i] = heap[parent]; + heap[parent] = tmp; + i = parent; + } + } + _sinkDown(i) { + const heap = this.heap; + const len = heap.length; + let largest = i; + do { + i = largest; + const left = 2 * i + 1; + const right = 2 * i + 2; + if (left < len && heap[left].score > heap[largest].score) largest = left; + if (right < len && heap[right].score > heap[largest].score) largest = right; + if (largest !== i) { + const tmp = heap[i]; + heap[i] = heap[largest]; + heap[largest] = tmp; + } + } while (largest !== i); + } +}; +//#endregion +//#region src/core/formatMatches.ts function formatMatches(result) { - const matches = []; - result.matches.forEach(match => { - if (!isDefined(match.indices) || !match.indices.length) { - return; - } - const obj = { - indices: match.indices, - value: match.value - }; - if (match.key) { - // `key.id` is the canonical dotted-string identity (array paths joined - // with '.'); `key.src` is the raw user input and can be a string[]. - obj.key = match.key.id; - } - if (match.idx > -1) { - obj.refIndex = match.idx; - } - matches.push(obj); - }); - return matches; + const matches = []; + result.matches.forEach((match) => { + if (!isDefined(match.indices) || !match.indices.length) return; + const obj = { + indices: match.indices, + value: match.value + }; + if (match.key) obj.key = match.key.id; + if (match.idx > -1) obj.refIndex = match.idx; + matches.push(obj); + }); + return matches; } -function format(results, docs, { - includeMatches = Config.includeMatches, - includeScore = Config.includeScore -} = {}) { - return results.map(result => { - const { - idx - } = result; - const data = { - item: docs[idx], - refIndex: idx - }; - if (includeMatches) data.matches = formatMatches(result); - if (includeScore) data.score = result.score; - return data; - }); +//#endregion +//#region src/core/format.ts +function format(results, docs, { includeMatches = Config.includeMatches, includeScore = Config.includeScore } = {}) { + return results.map((result) => { + const { idx } = result; + const data = { + item: docs[idx], + refIndex: idx + }; + if (includeMatches) data.matches = formatMatches(result); + if (includeScore) data.score = result.score; + return data; + }); } -// Includes \p{M} (Mark) so combining marks stay attached to their base -// letter — without it, scripts like Devanagari and NFD-normalized Latin -// shatter (e.g. 'हिन्दी' → ['ह','न','द'], 'café'.normalize('NFD') → ['cafe']). +//#endregion +//#region src/search/token/analyzer.ts const DEFAULT_TOKEN = /[\p{L}\p{M}\p{N}_]+/gu; function resolveTokenize(tokenize) { - if (typeof tokenize === 'function') { - return text => { - const result = tokenize(text); - return result; - }; - } - if (tokenize instanceof RegExp) { - if (!tokenize.global) ; - return text => text.match(tokenize) || []; - } - return text => text.match(DEFAULT_TOKEN) || []; + if (typeof tokenize === "function") return (text) => { + return tokenize(text); + }; + if (tokenize instanceof RegExp) { + if (!tokenize.global); + return (text) => text.match(tokenize) || []; + } + return (text) => text.match(DEFAULT_TOKEN) || []; } -function createAnalyzer({ - isCaseSensitive = false, - ignoreDiacritics = false, - tokenize -} = {}) { - const tokenizeFn = resolveTokenize(tokenize); - return { - tokenize(text) { - if (!isCaseSensitive) { - text = text.toLowerCase(); - } - if (ignoreDiacritics) { - text = stripDiacritics(text); - } - return tokenizeFn(text); - } - }; +function createAnalyzer({ isCaseSensitive = false, ignoreDiacritics = false, tokenize } = {}) { + const tokenizeFn = resolveTokenize(tokenize); + return { tokenize(text) { + if (!isCaseSensitive) text = text.toLowerCase(); + if (ignoreDiacritics) text = stripDiacritics(text); + return tokenizeFn(text); + } }; } -// `tokenMatch: 'all'` packs per-term coverage into a bitmask. JS bitwise ops -// are 32-bit *signed*, so bit 31 is the sign bit — only bits 0..30 are safe. -// Queries with more than this many terms fall back to a Set (no bit limit). +//#endregion +//#region src/search/token/index.ts const MAX_MASK_TERMS = 31; -class TokenSearch { - // `tokenMatch: 'all'` (AND) coverage. When true, searchIn reports which - // query terms matched each text so the core loop can require record-level - // coverage of every term. Bitmask is the ≤31-term fast path; Set is the - // ≥32-term fallback (JS bitwise ops are 32-bit signed). - - static condition(_, options) { - return options.useTokenSearch; - } - constructor(pattern, options) { - this.options = options; - this.analyzer = createAnalyzer({ - isCaseSensitive: options.isCaseSensitive, - ignoreDiacritics: options.ignoreDiacritics, - tokenize: options.tokenize - }); - const queryTerms = this.analyzer.tokenize(pattern); - const invertedIndex = options._invertedIndex; - const { - df, - fieldCount - } = invertedIndex; - this.termSearchers = []; - this.idfWeights = []; - for (const term of queryTerms) { - this.termSearchers.push(new BitapSearch(term, { - location: options.location, - threshold: options.threshold, - distance: options.distance, - includeMatches: options.includeMatches, - findAllMatches: options.findAllMatches, - minMatchCharLength: options.minMatchCharLength, - isCaseSensitive: options.isCaseSensitive, - ignoreDiacritics: options.ignoreDiacritics, - ignoreLocation: true - })); - const docFreq = df.get(term) || 0; - const idf = Math.log(1 + (fieldCount - docFreq + 0.5) / (docFreq + 0.5)); - this.idfWeights.push(idf); - } - this.combineAll = options.tokenMatch === 'all'; - this.numTerms = this.termSearchers.length; - this.useMask = this.numTerms <= MAX_MASK_TERMS; - } - searchIn(text) { - if (!this.termSearchers.length) { - return { - isMatch: false, - score: 1 - }; - } - const allIndices = []; - let weightedScore = 0; - let maxPossibleScore = 0; - let matchedCount = 0; - - // `tokenMatch: 'all'` coverage for this text (untouched in the default - // 'any' path, so it allocates nothing there). - let matchedMask = 0; - const matchedTerms = this.combineAll && !this.useMask ? new Set() : null; - for (let i = 0; i < this.termSearchers.length; i++) { - const result = this.termSearchers[i].searchIn(text); - const idf = this.idfWeights[i]; - maxPossibleScore += idf; - if (result.isMatch) { - matchedCount++; - weightedScore += idf * (1 - result.score); - if (result.indices) { - allIndices.push(...result.indices); - } - if (this.combineAll) { - if (this.useMask) { - matchedMask |= 1 << i; - } else { - matchedTerms.add(i); - } - } - } - } - if (matchedCount === 0) { - return { - isMatch: false, - score: 1 - }; - } - const normalized = maxPossibleScore > 0 ? 1 - weightedScore / maxPossibleScore : 0; - const searchResult = { - isMatch: true, - score: Math.max(0.001, normalized) - }; - if (this.options.includeMatches && allIndices.length) { - searchResult.indices = mergeIndices(allIndices); - } - - // Report term coverage so the core loop can enforce record-level AND. - if (this.combineAll) { - if (this.useMask) { - searchResult.matchedMask = matchedMask; - } else { - searchResult.matchedTerms = matchedTerms; - } - searchResult.termCount = this.numTerms; - } - return searchResult; - } -} - -// Stats-only inverted index for token search (per Plan 008 Direction B). -// -// The query path consumes only `df` and `fieldCount` (IDF weighting). The -// per-doc maps exist solely to keep `df` and `fieldCount` correct under -// `add` / `remove` / `removeAt`: -// -// docFieldCount[doc] = # distinct fields the doc contributed; subtracted -// from `fieldCount` on remove. -// docTermFieldHits[doc] = Map; each entry decrements `df[term]` by -// that count on remove. -// -// `df` is incremented once per (doc, term, field) at index time. Removing a -// doc decrements `df` by the same count, mirroring the increment exactly. +var TokenSearch = class { + static condition(_, options) { + return options.useTokenSearch; + } + constructor(pattern, options) { + this.options = options; + this.analyzer = createAnalyzer({ + isCaseSensitive: options.isCaseSensitive, + ignoreDiacritics: options.ignoreDiacritics, + tokenize: options.tokenize + }); + const queryTerms = this.analyzer.tokenize(pattern); + const { df, fieldCount } = options._invertedIndex; + this.termSearchers = []; + this.idfWeights = []; + for (const term of queryTerms) { + this.termSearchers.push(new BitapSearch(term, { + location: options.location, + threshold: options.threshold, + distance: options.distance, + includeMatches: options.includeMatches, + findAllMatches: options.findAllMatches, + minMatchCharLength: options.minMatchCharLength, + isCaseSensitive: options.isCaseSensitive, + ignoreDiacritics: options.ignoreDiacritics, + ignoreLocation: true + })); + const docFreq = df.get(term) || 0; + const idf = Math.log(1 + (fieldCount - docFreq + .5) / (docFreq + .5)); + this.idfWeights.push(idf); + } + this.combineAll = options.tokenMatch === "all"; + this.numTerms = this.termSearchers.length; + this.useMask = this.numTerms <= 31; + } + searchIn(text) { + if (!this.termSearchers.length) return { + isMatch: false, + score: 1 + }; + const allIndices = []; + let weightedScore = 0; + let maxPossibleScore = 0; + let matchedCount = 0; + let matchedMask = 0; + const matchedTerms = this.combineAll && !this.useMask ? /* @__PURE__ */ new Set() : null; + for (let i = 0; i < this.termSearchers.length; i++) { + const result = this.termSearchers[i].searchIn(text); + const idf = this.idfWeights[i]; + maxPossibleScore += idf; + if (result.isMatch) { + matchedCount++; + weightedScore += idf * (1 - result.score); + if (result.indices) allIndices.push(...result.indices); + if (this.combineAll) if (this.useMask) matchedMask |= 1 << i; + else matchedTerms.add(i); + } + } + if (matchedCount === 0) return { + isMatch: false, + score: 1 + }; + const normalized = maxPossibleScore > 0 ? 1 - weightedScore / maxPossibleScore : 0; + const searchResult = { + isMatch: true, + score: Math.max(.001, normalized) + }; + if (this.options.includeMatches && allIndices.length) searchResult.indices = mergeIndices(allIndices); + if (this.combineAll) { + if (this.useMask) searchResult.matchedMask = matchedMask; + else searchResult.matchedTerms = matchedTerms; + searchResult.termCount = this.numTerms; + } + return searchResult; + } +}; +//#endregion +//#region src/search/token/InvertedIndex.ts function addField(index, text, docIdx, analyzer) { - const tokens = analyzer.tokenize(text); - if (!tokens.length) return; - index.fieldCount++; - index.docFieldCount.set(docIdx, (index.docFieldCount.get(docIdx) || 0) + 1); - - // We count each (doc, term, field) once — repeated occurrences within the - // same field don't multiply df. - const distinctTerms = new Set(tokens); - let perDocTerms = index.docTermFieldHits.get(docIdx); - if (!perDocTerms) { - perDocTerms = new Map(); - index.docTermFieldHits.set(docIdx, perDocTerms); - } - for (const term of distinctTerms) { - perDocTerms.set(term, (perDocTerms.get(term) || 0) + 1); - index.df.set(term, (index.df.get(term) || 0) + 1); - } + const tokens = analyzer.tokenize(text); + if (!tokens.length) return; + index.fieldCount++; + index.docFieldCount.set(docIdx, (index.docFieldCount.get(docIdx) || 0) + 1); + const distinctTerms = new Set(tokens); + let perDocTerms = index.docTermFieldHits.get(docIdx); + if (!perDocTerms) { + perDocTerms = /* @__PURE__ */ new Map(); + index.docTermFieldHits.set(docIdx, perDocTerms); + } + for (const term of distinctTerms) { + perDocTerms.set(term, (perDocTerms.get(term) || 0) + 1); + index.df.set(term, (index.df.get(term) || 0) + 1); + } } function ingestRecord(index, record, keyCount, analyzer) { - const { - i: docIdx, - v, - $: fields - } = record; - if (v !== undefined) { - addField(index, v, docIdx, analyzer); - return; - } - if (!fields) return; - for (let keyIdx = 0; keyIdx < keyCount; keyIdx++) { - const value = fields[keyIdx]; - if (!value) continue; - if (Array.isArray(value)) { - for (const sub of value) addField(index, sub.v, docIdx, analyzer); - } else { - addField(index, value.v, docIdx, analyzer); - } - } + const { i: docIdx, v, $: fields } = record; + if (v !== void 0) { + addField(index, v, docIdx, analyzer); + return; + } + if (!fields) return; + for (let keyIdx = 0; keyIdx < keyCount; keyIdx++) { + const value = fields[keyIdx]; + if (!value) continue; + if (Array.isArray(value)) for (const sub of value) addField(index, sub.v, docIdx, analyzer); + else addField(index, value.v, docIdx, analyzer); + } } function buildInvertedIndex(records, keyCount, analyzer) { - const index = { - fieldCount: 0, - df: new Map(), - docFieldCount: new Map(), - docTermFieldHits: new Map() - }; - for (const record of records) { - ingestRecord(index, record, keyCount, analyzer); - } - return index; + const index = { + fieldCount: 0, + df: /* @__PURE__ */ new Map(), + docFieldCount: /* @__PURE__ */ new Map(), + docTermFieldHits: /* @__PURE__ */ new Map() + }; + for (const record of records) ingestRecord(index, record, keyCount, analyzer); + return index; } function addToInvertedIndex(index, record, keyCount, analyzer) { - ingestRecord(index, record, keyCount, analyzer); + ingestRecord(index, record, keyCount, analyzer); } function removeFromInvertedIndex(index, docIdx) { - const fieldCount = index.docFieldCount.get(docIdx); - if (fieldCount === undefined) return; - index.fieldCount -= fieldCount; - index.docFieldCount.delete(docIdx); - const perDocTerms = index.docTermFieldHits.get(docIdx); - if (!perDocTerms) return; - for (const [term, hits] of perDocTerms) { - const next = (index.df.get(term) || 0) - hits; - if (next <= 0) { - index.df.delete(term); - } else { - index.df.set(term, next); - } - } - index.docTermFieldHits.delete(docIdx); + const fieldCount = index.docFieldCount.get(docIdx); + if (fieldCount === void 0) return; + index.fieldCount -= fieldCount; + index.docFieldCount.delete(docIdx); + const perDocTerms = index.docTermFieldHits.get(docIdx); + if (!perDocTerms) return; + for (const [term, hits] of perDocTerms) { + const next = (index.df.get(term) || 0) - hits; + if (next <= 0) index.df.delete(term); + else index.df.set(term, next); + } + index.docTermFieldHits.delete(docIdx); } - -// Removes the given docIdx entries and renumbers the remaining per-doc maps -// so they stay in sync with FuseIndex's contiguous renumbering on remove. function removeAndShiftInvertedIndex(index, removedIndices) { - if (removedIndices.length === 0) return; - - // De-dup and sort so the shift computation is O(log k) per lookup. - const sorted = Array.from(new Set(removedIndices)).sort((a, b) => a - b); - for (const idx of sorted) { - removeFromInvertedIndex(index, idx); - } - - // For any surviving oldIdx, its new idx is oldIdx minus the number of - // removed indices strictly less than oldIdx. - const shift = oldIdx => { - let lo = 0; - let hi = sorted.length; - while (lo < hi) { - const mid = lo + hi >>> 1; - if (sorted[mid] < oldIdx) lo = mid + 1;else hi = mid; - } - return oldIdx - lo; - }; - const firstRemoved = sorted[0]; - const shiftedDocFieldCount = new Map(); - for (const [oldKey, count] of index.docFieldCount) { - shiftedDocFieldCount.set(oldKey > firstRemoved ? shift(oldKey) : oldKey, count); - } - index.docFieldCount = shiftedDocFieldCount; - const shiftedDocTermFieldHits = new Map(); - for (const [oldKey, terms] of index.docTermFieldHits) { - shiftedDocTermFieldHits.set(oldKey > firstRemoved ? shift(oldKey) : oldKey, terms); - } - index.docTermFieldHits = shiftedDocTermFieldHits; -} - -class Fuse { - // Statics are assigned in entry.ts - - constructor(docs, options, index) { - this.options = { - ...Config, - ...options - }; - if (this.options.useExtendedSearch && false) ; - if (this.options.useTokenSearch && false) ; - this._keyStore = new KeyStore(this.options.keys); - this._docs = docs; - this._myIndex = null; - this._invertedIndex = null; - this.setCollection(docs, index); - this._lastQuery = null; - this._lastSearcher = null; - } - _getSearcher(query) { - if (this._lastQuery === query) { - return this._lastSearcher; - } - const opts = this._invertedIndex ? { - ...this.options, - _invertedIndex: this._invertedIndex - } : this.options; - const searcher = createSearcher(query, opts); - this._lastQuery = query; - this._lastSearcher = searcher; - return searcher; - } - setCollection(docs, index) { - this._docs = docs; - if (index && !(index instanceof FuseIndex)) { - throw new Error(INCORRECT_INDEX_TYPE); - } - this._myIndex = index || createIndex(this.options.keys, this._docs, { - getFn: this.options.getFn, - fieldNormWeight: this.options.fieldNormWeight - }); - if (this.options.useTokenSearch) { - const analyzer = createAnalyzer({ - isCaseSensitive: this.options.isCaseSensitive, - ignoreDiacritics: this.options.ignoreDiacritics, - tokenize: this.options.tokenize - }); - this._invertedIndex = buildInvertedIndex(this._myIndex.records, this._myIndex.keys.length, analyzer); - } - this._invalidateSearcherCache(); - } - add(doc) { - if (!isDefined(doc)) { - return; - } - this._docs.push(doc); - const record = this._myIndex.add(doc, this._docs.length - 1); - - // Skip inverted-index bookkeeping when no record was appended (blank - // strings produce null). The previous code read `records[records.length-1]` - // unconditionally, which would re-ingest the previous doc on `add("")`. - if (this._invertedIndex && record) { - const analyzer = createAnalyzer({ - isCaseSensitive: this.options.isCaseSensitive, - ignoreDiacritics: this.options.ignoreDiacritics, - tokenize: this.options.tokenize - }); - addToInvertedIndex(this._invertedIndex, record, this._myIndex.keys.length, analyzer); - } - this._invalidateSearcherCache(); - } - remove(predicate = () => false) { - const results = []; - const indicesToRemove = []; - for (let i = 0, len = this._docs.length; i < len; i += 1) { - if (predicate(this._docs[i], i)) { - results.push(this._docs[i]); - indicesToRemove.push(i); - } - } - if (indicesToRemove.length) { - if (this._invertedIndex) { - removeAndShiftInvertedIndex(this._invertedIndex, indicesToRemove); - } - - // Filter docs in a single pass instead of reverse-splicing - const toRemove = new Set(indicesToRemove); - this._docs = this._docs.filter((_, i) => !toRemove.has(i)); - this._myIndex.removeAll(indicesToRemove); - this._invalidateSearcherCache(); - } - return results; - } - removeAt(idx) { - // Validate before any mutation. The previous code spliced `_docs` first - // and let FuseIndex.removeAt throw afterward — partial-state on invalid - // input. Atomic now. - if (!Number.isInteger(idx) || idx < 0 || idx >= this._docs.length) { - throw new Error(INVALID_DOC_INDEX); - } - if (this._invertedIndex) { - removeAndShiftInvertedIndex(this._invertedIndex, [idx]); - } - const doc = this._docs.splice(idx, 1)[0]; - this._myIndex.removeAt(idx); - this._invalidateSearcherCache(); - return doc; - } - _invalidateSearcherCache() { - this._lastQuery = null; - this._lastSearcher = null; - } - getIndex() { - return this._myIndex; - } - search(query, options) { - const { - limit = -1 - } = options || {}; - const { - includeMatches, - includeScore, - shouldSort, - sortFn, - ignoreFieldNorm - } = this.options; - - // Empty string query returns all docs (useful for search UIs) - if (isString(query) && !query.trim()) { - let docs = this._docs.map((item, idx) => ({ - item, - refIndex: idx - })); - if (isNumber(limit) && limit > -1) { - docs = docs.slice(0, limit); - } - return docs; - } - const useHeap = isNumber(limit) && limit > 0 && isString(query); - let results; - if (useHeap) { - const heap = new MaxHeap(limit); - if (isString(this._docs[0])) { - this._searchStringList(query, { - heap, - ignoreFieldNorm - }); - } else { - this._searchObjectList(query, { - heap, - ignoreFieldNorm - }); - } - results = heap.extractSorted(sortFn); - } else { - results = isString(query) ? isString(this._docs[0]) ? this._searchStringList(query) : this._searchObjectList(query) : this._searchLogical(query); - computeScore(results, { - ignoreFieldNorm - }); - if (shouldSort) { - results.sort(sortFn); - } - if (isNumber(limit) && limit > -1) { - results = results.slice(0, limit); - } - } - return format(results, this._docs, { - includeMatches, - includeScore - }); - } - _searchStringList(query, { - heap, - ignoreFieldNorm - } = {}) { - const searcher = this._getSearcher(query); - const requireAllTokens = this.options.useTokenSearch && this.options.tokenMatch === 'all'; - const { - records - } = this._myIndex; - const results = heap ? null : []; - - // Iterate over every string in the index - records.forEach(({ - v: text, - i: idx, - n: norm - }) => { - if (!isDefined(text)) { - return; - } - const searchResult = searcher.searchIn(text); - if (searchResult.isMatch) { - const match = { - score: searchResult.score, - value: text, - norm: norm, - indices: searchResult.indices - }; - if (requireAllTokens) { - match.matchedMask = searchResult.matchedMask; - match.matchedTerms = searchResult.matchedTerms; - match.termCount = searchResult.termCount; - } - const matches = [match]; - - // Record-level AND gate (token search `tokenMatch: 'all'`), applied - // before heap insertion so `limit` returns the same top-N as unlimited. - if (!requireAllTokens || this._coversAllTokens(matches)) { - const result = { - item: text, - idx, - matches - }; - if (heap) { - result.score = computeScoreSingle(result.matches, { - ignoreFieldNorm - }); - if (heap.shouldInsert(result.score)) { - heap.insert(result); - } - } else { - results.push(result); - } - } - } - }); - return results; - } - _searchLogical(query) { - const expression = parse(query, this.options); - const evaluate = (node, item, idx) => { - if (!('children' in node)) { - const { - keyId, - searcher - } = node; - let matches; - if (keyId === null) { - // Keyless entry: search across all keys - matches = []; - this._myIndex.keys.forEach((key, keyIndex) => { - matches.push(...this._findMatches({ - key, - value: item[keyIndex], - searcher: searcher - })); - }); - } else { - matches = this._findMatches({ - key: this._keyStore.get(keyId), - value: this._myIndex.getValueForItemAtKeyId(item, keyId), - searcher: searcher - }); - } - if (matches && matches.length) { - return [{ - idx, - item, - matches - }]; - } - return []; - } - const { - children, - operator - } = node; - const res = []; - for (let i = 0, len = children.length; i < len; i += 1) { - const child = children[i]; - const result = evaluate(child, item, idx); - if (result.length) { - res.push(...result); - } else if (operator === LogicalOperator.AND) { - return []; - } - } - return res; - }; - const records = this._myIndex.records; - const resultMap = new Map(); - const results = []; - records.forEach(({ - $: item, - i: idx - }) => { - if (isDefined(item)) { - const expResults = evaluate(expression, item, idx); - if (expResults.length) { - // Dedupe when adding - if (!resultMap.has(idx)) { - resultMap.set(idx, { - idx, - item, - matches: [] - }); - results.push(resultMap.get(idx)); - } - expResults.forEach(({ - matches - }) => { - resultMap.get(idx).matches.push(...matches); - }); - } - } - }); - return results; - } - - // When a search involves inverse patterns (e.g. !Syrup), the aggregation - // across keys switches from "ANY key matches" to "ALL keys must match." - // This is signaled by hasInverse on the SearchResult from ExtendedSearch. - // - // For mixed patterns like "^hello !Syrup", a key failure is ambiguous — - // it could be the positive or inverse term that failed. In that case we - // conservatively exclude the item, which is strictly better than the old - // behavior of including it. See: https://github.com/krisk/Fuse/issues/712 - _searchObjectList(query, { - heap, - ignoreFieldNorm - } = {}) { - const searcher = this._getSearcher(query); - const requireAllTokens = this.options.useTokenSearch && this.options.tokenMatch === 'all'; - const { - keys, - records - } = this._myIndex; - const results = heap ? null : []; - - // List is Array - records.forEach(({ - $: item, - i: idx - }) => { - if (!isDefined(item)) { - return; - } - const matches = []; - let anyKeyFailed = false; - let hasInverse = false; - - // Iterate over every key (i.e, path), and fetch the value at that key - keys.forEach((key, keyIndex) => { - const keyMatches = this._findMatches({ - key, - value: item[keyIndex], - searcher - }); - if (keyMatches.length) { - matches.push(...keyMatches); - if (keyMatches[0].hasInverse) { - hasInverse = true; - } - } else { - anyKeyFailed = true; - } - }); - - // If the search involves inverse patterns, ALL keys must match - if (hasInverse && anyKeyFailed) { - return; - } - - // Record-level AND gate (token search `tokenMatch: 'all'`): every query - // term must be covered across the record's field/array-element matches. - // Applied before heap insertion so `limit` returns the same top-N. - if (matches.length && (!requireAllTokens || this._coversAllTokens(matches))) { - const result = { - idx, - item, - matches - }; - if (heap) { - result.score = computeScoreSingle(result.matches, { - ignoreFieldNorm - }); - if (heap.shouldInsert(result.score)) { - heap.insert(result); - } - } else { - results.push(result); - } - } - }); - return results; - } - _findMatches({ - key, - value, - searcher - }) { - if (!isDefined(value)) { - return []; - } - const matches = []; - if (isArray(value)) { - value.forEach(({ - v: text, - i: idx, - n: norm - }) => { - if (!isDefined(text)) { - return; - } - const searchResult = searcher.searchIn(text); - if (searchResult.isMatch) { - const match = { - score: searchResult.score, - key, - value: text, - idx, - norm, - indices: searchResult.indices, - hasInverse: searchResult.hasInverse - }; - // Carry token-search AND coverage only when present, so the default - // (non-token / 'any') MatchScore keeps its original object shape. - if (searchResult.termCount !== undefined) { - match.matchedMask = searchResult.matchedMask; - match.matchedTerms = searchResult.matchedTerms; - match.termCount = searchResult.termCount; - } - matches.push(match); - } - }); - } else { - const { - v: text, - n: norm - } = value; - const searchResult = searcher.searchIn(text); - if (searchResult.isMatch) { - const match = { - score: searchResult.score, - key, - value: text, - norm, - indices: searchResult.indices, - hasInverse: searchResult.hasInverse - }; - if (searchResult.termCount !== undefined) { - match.matchedMask = searchResult.matchedMask; - match.matchedTerms = searchResult.matchedTerms; - match.termCount = searchResult.termCount; - } - matches.push(match); - } - } - return matches; - } - - // Record-level AND gate for token search (`tokenMatch: 'all'`). Returns true - // unless the matched terms across ALL of a record's field/array-element - // matches fail to cover every query term. `termCount` is only set by - // TokenSearch in 'all' mode, so non-token / 'any' searches always pass. - _coversAllTokens(matches) { - const termCount = matches.length ? matches[0].termCount : undefined; - if (termCount === undefined) { - return true; - } - if (termCount <= MAX_MASK_TERMS) { - let coverage = 0; - for (let i = 0; i < matches.length; i++) { - coverage |= matches[i].matchedMask || 0; - } - return coverage === 2 ** termCount - 1; - } - const coverage = new Set(); - for (let i = 0; i < matches.length; i++) { - const terms = matches[i].matchedTerms; - if (terms) { - for (const t of terms) { - coverage.add(t); - } - } - } - return coverage.size === termCount; - } + if (removedIndices.length === 0) return; + const sorted = Array.from(new Set(removedIndices)).sort((a, b) => a - b); + for (const idx of sorted) removeFromInvertedIndex(index, idx); + const shift = (oldIdx) => { + let lo = 0; + let hi = sorted.length; + while (lo < hi) { + const mid = lo + hi >>> 1; + if (sorted[mid] < oldIdx) lo = mid + 1; + else hi = mid; + } + return oldIdx - lo; + }; + const firstRemoved = sorted[0]; + const shiftedDocFieldCount = /* @__PURE__ */ new Map(); + for (const [oldKey, count] of index.docFieldCount) shiftedDocFieldCount.set(oldKey > firstRemoved ? shift(oldKey) : oldKey, count); + index.docFieldCount = shiftedDocFieldCount; + const shiftedDocTermFieldHits = /* @__PURE__ */ new Map(); + for (const [oldKey, terms] of index.docTermFieldHits) shiftedDocTermFieldHits.set(oldKey > firstRemoved ? shift(oldKey) : oldKey, terms); + index.docTermFieldHits = shiftedDocTermFieldHits; } -/// - +//#endregion +//#region src/core/index.ts +var Fuse = class { + constructor(docs, options, index) { + this.options = { + ...Config, + ...options + }; + if (this.options.useExtendedSearch && false); + if (this.options.useTokenSearch && false); + this._keyStore = new KeyStore(this.options.keys); + this._docs = docs; + this._myIndex = null; + this._invertedIndex = null; + this.setCollection(docs, index); + this._lastQuery = null; + this._lastSearcher = null; + } + _getSearcher(query) { + if (this._lastQuery === query) return this._lastSearcher; + const searcher = createSearcher(query, this._invertedIndex ? { + ...this.options, + _invertedIndex: this._invertedIndex + } : this.options); + this._lastQuery = query; + this._lastSearcher = searcher; + return searcher; + } + setCollection(docs, index) { + this._docs = docs; + if (index && !(index instanceof FuseIndex)) throw new Error(INCORRECT_INDEX_TYPE); + this._myIndex = index || createIndex(this.options.keys, this._docs, { + getFn: this.options.getFn, + fieldNormWeight: this.options.fieldNormWeight + }); + if (this.options.useTokenSearch) { + const analyzer = createAnalyzer({ + isCaseSensitive: this.options.isCaseSensitive, + ignoreDiacritics: this.options.ignoreDiacritics, + tokenize: this.options.tokenize + }); + this._invertedIndex = buildInvertedIndex(this._myIndex.records, this._myIndex.keys.length, analyzer); + } + this._invalidateSearcherCache(); + } + add(doc) { + if (!isDefined(doc)) return; + this._docs.push(doc); + const record = this._myIndex.add(doc, this._docs.length - 1); + if (this._invertedIndex && record) { + const analyzer = createAnalyzer({ + isCaseSensitive: this.options.isCaseSensitive, + ignoreDiacritics: this.options.ignoreDiacritics, + tokenize: this.options.tokenize + }); + addToInvertedIndex(this._invertedIndex, record, this._myIndex.keys.length, analyzer); + } + this._invalidateSearcherCache(); + } + remove(predicate = () => false) { + const results = []; + const indicesToRemove = []; + for (let i = 0, len = this._docs.length; i < len; i += 1) if (predicate(this._docs[i], i)) { + results.push(this._docs[i]); + indicesToRemove.push(i); + } + if (indicesToRemove.length) { + if (this._invertedIndex) removeAndShiftInvertedIndex(this._invertedIndex, indicesToRemove); + const toRemove = new Set(indicesToRemove); + this._docs = this._docs.filter((_, i) => !toRemove.has(i)); + this._myIndex.removeAll(indicesToRemove); + this._invalidateSearcherCache(); + } + return results; + } + removeAt(idx) { + if (!Number.isInteger(idx) || idx < 0 || idx >= this._docs.length) throw new Error(INVALID_DOC_INDEX); + if (this._invertedIndex) removeAndShiftInvertedIndex(this._invertedIndex, [idx]); + const doc = this._docs.splice(idx, 1)[0]; + this._myIndex.removeAt(idx); + this._invalidateSearcherCache(); + return doc; + } + _invalidateSearcherCache() { + this._lastQuery = null; + this._lastSearcher = null; + } + getIndex() { + return this._myIndex; + } + search(query, options) { + const { limit = -1 } = options || {}; + const { includeMatches, includeScore, shouldSort, sortFn, ignoreFieldNorm } = this.options; + if (isString(query) && !query.trim()) { + let docs = this._docs.map((item, idx) => ({ + item, + refIndex: idx + })); + if (isNumber(limit) && limit > -1) docs = docs.slice(0, limit); + return docs; + } + const useHeap = isNumber(limit) && limit > 0 && isString(query); + let results; + if (useHeap) { + const heap = new MaxHeap(limit); + if (isString(this._docs[0])) this._searchStringList(query, { + heap, + ignoreFieldNorm + }); + else this._searchObjectList(query, { + heap, + ignoreFieldNorm + }); + results = heap.extractSorted(sortFn); + } else { + results = isString(query) ? isString(this._docs[0]) ? this._searchStringList(query) : this._searchObjectList(query) : this._searchLogical(query); + computeScore(results, { ignoreFieldNorm }); + if (shouldSort) results.sort(sortFn); + if (isNumber(limit) && limit > -1) results = results.slice(0, limit); + } + return format(results, this._docs, { + includeMatches, + includeScore + }); + } + _searchStringList(query, { heap, ignoreFieldNorm } = {}) { + const searcher = this._getSearcher(query); + const requireAllTokens = this.options.useTokenSearch && this.options.tokenMatch === "all"; + const { records } = this._myIndex; + const results = heap ? null : []; + records.forEach(({ v: text, i: idx, n: norm }) => { + if (!isDefined(text)) return; + const searchResult = searcher.searchIn(text); + if (searchResult.isMatch) { + const match = { + score: searchResult.score, + value: text, + norm, + indices: searchResult.indices + }; + if (requireAllTokens) { + match.matchedMask = searchResult.matchedMask; + match.matchedTerms = searchResult.matchedTerms; + match.termCount = searchResult.termCount; + } + const matches = [match]; + if (!requireAllTokens || this._coversAllTokens(matches)) { + const result = { + item: text, + idx, + matches + }; + if (heap) { + result.score = computeScoreSingle(result.matches, { ignoreFieldNorm }); + if (heap.shouldInsert(result.score)) heap.insert(result); + } else results.push(result); + } + } + }); + return results; + } + _searchLogical(query) { + const expression = parse(query, this.options); + const evaluate = (node, item, idx) => { + if (!("children" in node)) { + const { keyId, searcher } = node; + let matches; + if (keyId === null) { + matches = []; + this._myIndex.keys.forEach((key, keyIndex) => { + matches.push(...this._findMatches({ + key, + value: item[keyIndex], + searcher + })); + }); + } else matches = this._findMatches({ + key: this._keyStore.get(keyId), + value: this._myIndex.getValueForItemAtKeyId(item, keyId), + searcher + }); + if (matches && matches.length) return [{ + idx, + item, + matches + }]; + return []; + } + const { children, operator } = node; + const res = []; + for (let i = 0, len = children.length; i < len; i += 1) { + const child = children[i]; + const result = evaluate(child, item, idx); + if (result.length) res.push(...result); + else if (operator === LogicalOperator.AND) return []; + } + return res; + }; + const records = this._myIndex.records; + const resultMap = /* @__PURE__ */ new Map(); + const results = []; + records.forEach(({ $: item, i: idx }) => { + if (isDefined(item)) { + const expResults = evaluate(expression, item, idx); + if (expResults.length) { + if (!resultMap.has(idx)) { + resultMap.set(idx, { + idx, + item, + matches: [] + }); + results.push(resultMap.get(idx)); + } + expResults.forEach(({ matches }) => { + resultMap.get(idx).matches.push(...matches); + }); + } + } + }); + return results; + } + _searchObjectList(query, { heap, ignoreFieldNorm } = {}) { + const searcher = this._getSearcher(query); + const requireAllTokens = this.options.useTokenSearch && this.options.tokenMatch === "all"; + const { keys, records } = this._myIndex; + const results = heap ? null : []; + records.forEach(({ $: item, i: idx }) => { + if (!isDefined(item)) return; + const matches = []; + let anyKeyFailed = false; + let hasInverse = false; + keys.forEach((key, keyIndex) => { + const keyMatches = this._findMatches({ + key, + value: item[keyIndex], + searcher + }); + if (keyMatches.length) { + matches.push(...keyMatches); + if (keyMatches[0].hasInverse) hasInverse = true; + } else anyKeyFailed = true; + }); + if (hasInverse && anyKeyFailed) return; + if (matches.length && (!requireAllTokens || this._coversAllTokens(matches))) { + const result = { + idx, + item, + matches + }; + if (heap) { + result.score = computeScoreSingle(result.matches, { ignoreFieldNorm }); + if (heap.shouldInsert(result.score)) heap.insert(result); + } else results.push(result); + } + }); + return results; + } + _findMatches({ key, value, searcher }) { + if (!isDefined(value)) return []; + const matches = []; + if (isArray(value)) value.forEach(({ v: text, i: idx, n: norm }) => { + if (!isDefined(text)) return; + const searchResult = searcher.searchIn(text); + if (searchResult.isMatch) { + const match = { + score: searchResult.score, + key, + value: text, + idx, + norm, + indices: searchResult.indices, + hasInverse: searchResult.hasInverse + }; + if (searchResult.termCount !== void 0) { + match.matchedMask = searchResult.matchedMask; + match.matchedTerms = searchResult.matchedTerms; + match.termCount = searchResult.termCount; + } + matches.push(match); + } + }); + else { + const { v: text, n: norm } = value; + const searchResult = searcher.searchIn(text); + if (searchResult.isMatch) { + const match = { + score: searchResult.score, + key, + value: text, + norm, + indices: searchResult.indices, + hasInverse: searchResult.hasInverse + }; + if (searchResult.termCount !== void 0) { + match.matchedMask = searchResult.matchedMask; + match.matchedTerms = searchResult.matchedTerms; + match.termCount = searchResult.termCount; + } + matches.push(match); + } + } + return matches; + } + _coversAllTokens(matches) { + const termCount = matches.length ? matches[0].termCount : void 0; + if (termCount === void 0) return true; + if (termCount <= 31) { + let coverage = 0; + for (let i = 0; i < matches.length; i++) coverage |= matches[i].matchedMask || 0; + return coverage === 2 ** termCount - 1; + } + const coverage = /* @__PURE__ */ new Set(); + for (let i = 0; i < matches.length; i++) { + const terms = matches[i].matchedTerms; + if (terms) for (const t of terms) coverage.add(t); + } + return coverage.size === termCount; + } +}; -// Register all search plugins so the worker supports full features -{ - register(ExtendedSearch); -} -{ - register(TokenSearch); -} +//#endregion +//#region src/workers/worker.ts +register(ExtendedSearch); +register(TokenSearch); Fuse.createIndex = createIndex; Fuse.config = Config; let fuse = null; -self.onmessage = e => { - const { - id, - method, - args - } = e.data; - try { - let result; - switch (method) { - case 'init': - { - const [docs, options] = args; - fuse = new Fuse(docs, options); - result = true; - break; - } - case 'search': - { - result = fuse.search(args[0], args[1]); - break; - } - case 'add': - { - fuse.add(args[0]); - result = true; - break; - } - case 'setCollection': - { - fuse.setCollection(args[0]); - result = true; - break; - } - } - self.postMessage({ - id, - result - }); - } catch (err) { - self.postMessage({ - id, - error: err.message - }); - } +self.onmessage = (e) => { + const { id, method, args } = e.data; + try { + let result; + switch (method) { + case "init": { + const [docs, options] = args; + fuse = new Fuse(docs, options); + result = true; + break; + } + case "search": + result = fuse.search(args[0], args[1]); + break; + case "add": + fuse.add(args[0]); + result = true; + break; + case "setCollection": + fuse.setCollection(args[0]); + result = true; + break; + } + self.postMessage({ + id, + result + }); + } catch (err) { + self.postMessage({ + id, + error: err.message + }); + } }; + +//#endregion \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9407e3b55..eb2e55093 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,9 @@ "version": "7.4.1", "license": "Apache-2.0", "devDependencies": { - "@babel/core": "^7.20.12", - "@babel/preset-typescript": "7.18.6", "@commitlint/cli": "^17.4.2", "@commitlint/config-conventional": "^17.4.2", "@monaco-editor/loader": "^1.3.2", - "@rollup/plugin-babel": "^7.0.0", - "@rollup/plugin-node-resolve": "^16.0.3", - "@rollup/plugin-replace": "^6.0.3", "@sapphire/stopwatch": "^1.5.0", "@sapphire/utilities": "^3.11.0", "eslint": "^9.39.4", @@ -25,10 +20,8 @@ "monaco-editor": "^0.34.1", "prettier": "^3.8.3", "replace-in-file": "^6.3.5", - "rollup": "^4.60.1", - "rollup-plugin-dts": "^6.4.1", "standard-version": "^9.5.0", - "terser": "^5.16.1", + "tsdown": "0.22.1", "typescript": "^6.0.2", "typescript-eslint": "^8.58.0", "vitepress": "^1.6.4", @@ -316,227 +309,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", - "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.6", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", - "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", - "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -557,30 +329,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/parser": { "version": "7.29.2", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", @@ -597,94 +345,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", - "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", - "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-typescript": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.18.6.tgz", - "integrity": "sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-validator-option": "^7.18.6", - "@babel/plugin-transform-typescript": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/types": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", @@ -1087,6 +747,40 @@ } } }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -1787,17 +1481,6 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1808,17 +1491,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -1832,118 +1504,345 @@ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@monaco-editor/loader": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", + "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@quansync/fs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@quansync/fs/-/fs-1.0.0.tgz", + "integrity": "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "quansync": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@monaco-editor/loader": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", - "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", - "dependencies": { - "state-local": "^1.0.6" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rollup/plugin-babel": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-7.0.0.tgz", - "integrity": "sha512-NS2+P7v80N3MQqehZEjgpaFb9UyX3URNMW/zvoECKGo4PY4DvJfQusTI7BX/Ks+CPvtTfk3TqcR6S9VYBi/C+A==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.18.6", - "@rollup/pluginutils": "^5.0.1" - }, + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0", - "@types/babel__core": "^7.1.9", - "rollup": "^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "@types/babel__core": { - "optional": true - }, - "rollup": { - "optional": true - } + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", - "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" }, "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rollup/plugin-replace": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.3.tgz", - "integrity": "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "magic-string": "^0.30.3" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rollup/pluginutils": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", - "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.60.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", @@ -2433,6 +2332,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2450,6 +2360,13 @@ "@types/unist": "*" } }, + "node_modules/@types/jsesc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz", + "integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2502,13 +2419,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/resolve": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -3304,6 +3214,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ansis": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.3.1.tgz", + "integrity": "sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -3345,26 +3265,98 @@ "node": ">=12" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/ast-kit": { + "version": "3.0.0-beta.1", + "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-3.0.0-beta.1.tgz", + "integrity": "sha512-trmleAnZ2PxN/loHWVhhx1qeOHSRXq4TDsBBxq3GqeJitfk3+jTQ+v/C1km/KYq9M7wKqCewMh+/NAvVH7m+bw==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@babel/parser": "^8.0.0-beta.4", + "estree-walker": "^3.0.3", + "pathe": "^2.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz", - "integrity": "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==", + "node_modules/ast-kit/node_modules/@babel/helper-string-parser": { + "version": "8.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0-rc.6.tgz", + "integrity": "sha512-BCkFy+zN6kXQed3YOT7aJl93NfDSzQc3pBfsvTVPs9gU9X3V0aefEF5kwBT0E+mDWH9QgKaZstYUQN9VdQZT4g==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/ast-kit/node_modules/@babel/helper-validator-identifier": { + "version": "8.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.0-rc.6.tgz", + "integrity": "sha512-nVJ+1JcCgntv8d78rRo++o2wuODT0Irknx2BF8Np4Ft2CRgjLqIs4qzSZ8b66yGbBdMWGmZBO9WEZv1hhNiSpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/ast-kit/node_modules/@babel/parser": { + "version": "8.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.0-rc.6.tgz", + "integrity": "sha512-rOS8IpdO7mQELkTPlCsTgPejO0bFuZdEDCGQJouYbYf9e1FLTym7Fei2pEjq8q7MWbX0ravcd7QQYKs1TxOuog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^8.0.0-rc.6" + }, "bin": { - "baseline-browser-mapping": "dist/cli.cjs" + "parser": "bin/babel-parser.js" }, "engines": { - "node": ">=6.0.0" + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/ast-kit/node_modules/@babel/types": { + "version": "8.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.0-rc.6.tgz", + "integrity": "sha512-p7/ABylAYlexb31wtRdIfH9L9A0Z2T/9H6zAqzqndkY2PLkvNNc580wGhp/gGKN4Sp9sQvSkhc6Oga8/O+wTyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^8.0.0-rc.6", + "@babel/helper-validator-identifier": "^8.0.0-rc.6" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/ast-kit/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" } }, + "node_modules/ast-kit/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/birpc": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", @@ -3386,40 +3378,6 @@ "concat-map": "0.0.1" } }, - "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3475,27 +3433,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001788", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", - "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -3642,13 +3579,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, "node_modules/compare-func": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", @@ -4163,13 +4093,6 @@ "node": ">=10" } }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, "node_modules/copy-anything": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", @@ -4357,15 +4280,12 @@ "dev": true, "license": "MIT" }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "license": "MIT" }, "node_modules/dequal": { "version": "2.0.3", @@ -4524,12 +4444,26 @@ "node": ">=4" } }, - "node_modules/electron-to-chromium": { - "version": "1.5.340", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.340.tgz", - "integrity": "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==", + "node_modules/dts-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/dts-resolver/-/dts-resolver-3.0.0.tgz", + "integrity": "sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==", "dev": true, - "license": "ISC" + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "oxc-resolver": ">=11.0.0" + }, + "peerDependenciesMeta": { + "oxc-resolver": { + "optional": true + } + } }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -4545,6 +4479,16 @@ "dev": true, "license": "MIT" }, + "node_modules/empathic": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.1.tgz", + "integrity": "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -5075,16 +5019,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -5226,10 +5160,26 @@ "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "5.0.0-beta.5", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.5.tgz", + "integrity": "sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "engines": { + "node": ">=20.20.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, "node_modules/git-raw-commits": { @@ -5557,6 +5507,19 @@ "node": ">=4" } }, + "node_modules/import-without-cache": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/import-without-cache/-/import-without-cache-0.4.0.tgz", + "integrity": "sha512-NkJQA7oZ4YHQhd2+H3BoRFKF3d/XNsiKpHZCQEMH9pDX27hQQLsTyOocyRgaIVtf8gHX3Nt3LPkR4e5EdtPAGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -5659,13 +5622,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", - "dev": true, - "license": "MIT" - }, "node_modules/is-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", @@ -5807,19 +5763,6 @@ "dev": true, "license": "ISC" }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", @@ -6048,16 +5991,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -6373,13 +6306,6 @@ "dev": true, "license": "MIT" }, - "node_modules/node-releases": { - "version": "2.0.37", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", - "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", - "dev": true, - "license": "MIT" - }, "node_modules/normalize-package-data": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", @@ -6422,6 +6348,17 @@ "node": ">=8" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -6759,6 +6696,23 @@ "teleport": ">=0.2.0" } }, + "node_modules/quansync": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-1.0.0.tgz", + "integrity": "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, "node_modules/quick-lru": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", @@ -7058,6 +7012,16 @@ "node": ">=8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -7065,6 +7029,162 @@ "dev": true, "license": "MIT" }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "node_modules/rolldown-plugin-dts": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/rolldown-plugin-dts/-/rolldown-plugin-dts-0.25.2.tgz", + "integrity": "sha512-nMhN/R+vmR8GM45ZW1FWMSjRTSDDn/6w4GTf8RNrEFCBdl8B1kySWrU1ixPtbwzXoRlcO+R/S88VgXuJQwfdDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/generator": "8.0.0-rc.6", + "@babel/helper-validator-identifier": "8.0.0-rc.6", + "@babel/parser": "8.0.0-rc.6", + "ast-kit": "^3.0.0-beta.1", + "birpc": "^4.0.0", + "dts-resolver": "^3.0.0", + "get-tsconfig": "5.0.0-beta.5", + "obug": "^2.1.1" + }, + "engines": { + "node": "^22.18.0 || >=24.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@ts-macro/tsc": "^0.3.6", + "@typescript/native-preview": ">=7.0.0-dev.20260325.1", + "rolldown": "^1.0.0", + "typescript": "^5.0.0 || ^6.0.0", + "vue-tsc": "~3.2.0" + }, + "peerDependenciesMeta": { + "@ts-macro/tsc": { + "optional": true + }, + "@typescript/native-preview": { + "optional": true + }, + "typescript": { + "optional": true + }, + "vue-tsc": { + "optional": true + } + } + }, + "node_modules/rolldown-plugin-dts/node_modules/@babel/generator": { + "version": "8.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0-rc.6.tgz", + "integrity": "sha512-6mIzgVK8DgEzvIapoQwhXTMnnkuE4STQmVv9H03i/tZ2ml8oev3TRvZJgTenK2Bsq0YWNtzOrFdTyNzCMFtjJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^8.0.0-rc.6", + "@babel/types": "^8.0.0-rc.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "@types/jsesc": "^2.5.0", + "jsesc": "^3.0.2" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/rolldown-plugin-dts/node_modules/@babel/helper-string-parser": { + "version": "8.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0-rc.6.tgz", + "integrity": "sha512-BCkFy+zN6kXQed3YOT7aJl93NfDSzQc3pBfsvTVPs9gU9X3V0aefEF5kwBT0E+mDWH9QgKaZstYUQN9VdQZT4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/rolldown-plugin-dts/node_modules/@babel/helper-validator-identifier": { + "version": "8.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.0-rc.6.tgz", + "integrity": "sha512-nVJ+1JcCgntv8d78rRo++o2wuODT0Irknx2BF8Np4Ft2CRgjLqIs4qzSZ8b66yGbBdMWGmZBO9WEZv1hhNiSpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/rolldown-plugin-dts/node_modules/@babel/parser": { + "version": "8.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.0-rc.6.tgz", + "integrity": "sha512-rOS8IpdO7mQELkTPlCsTgPejO0bFuZdEDCGQJouYbYf9e1FLTym7Fei2pEjq8q7MWbX0ravcd7QQYKs1TxOuog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^8.0.0-rc.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/rolldown-plugin-dts/node_modules/@babel/types": { + "version": "8.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.0-rc.6.tgz", + "integrity": "sha512-p7/ABylAYlexb31wtRdIfH9L9A0Z2T/9H6zAqzqndkY2PLkvNNc580wGhp/gGKN4Sp9sQvSkhc6Oga8/O+wTyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^8.0.0-rc.6", + "@babel/helper-validator-identifier": "^8.0.0-rc.6" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/rolldown-plugin-dts/node_modules/birpc": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-4.0.0.tgz", + "integrity": "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/rollup": { "version": "4.60.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", @@ -7110,32 +7230,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/rollup-plugin-dts": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-6.4.1.tgz", - "integrity": "sha512-l//F3Zf7ID5GoOfLfD8kroBjQKEKpy1qfhtAdnpibFZMffPaylrg1CoDC2vGkPeTeyxUe4bVFCln2EFuL7IGGg==", - "dev": true, - "license": "LGPL-3.0-only", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "@jridgewell/sourcemap-codec": "^1.5.5", - "convert-source-map": "^2.0.0", - "magic-string": "^0.30.21" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/Swatinem" - }, - "optionalDependencies": { - "@babel/code-frame": "^7.29.0" - }, - "peerDependencies": { - "rollup": "^3.29.4 || ^4", - "typescript": "^4.5 || ^5.0 || ^6.0" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -7241,17 +7335,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/space-separated-tokens": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", @@ -7705,25 +7788,6 @@ "dev": true, "license": "MIT" }, - "node_modules/terser": { - "version": "5.46.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", - "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/text-extensions": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", @@ -7812,6 +7876,16 @@ "node": ">=14.0.0" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -7890,6 +7964,119 @@ } } }, + "node_modules/tsdown": { + "version": "0.22.1", + "resolved": "https://registry.npmjs.org/tsdown/-/tsdown-0.22.1.tgz", + "integrity": "sha512-Ldx1jLyDFEzsN/fMBi2TBVaZe4fuEJhIiHjQhX0pV7oa5uYz5Imdivs5mNzEXOrMEtFRR6C9BQ2YqLoroffB+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansis": "^4.3.0", + "cac": "^7.0.0", + "defu": "^6.1.7", + "empathic": "^2.0.1", + "hookable": "^6.1.1", + "import-without-cache": "^0.4.0", + "obug": "^2.1.1", + "picomatch": "^4.0.4", + "rolldown": "^1.0.2", + "rolldown-plugin-dts": "^0.25.1", + "semver": "^7.8.0", + "tinyexec": "^1.1.2", + "tinyglobby": "^0.2.16", + "tree-kill": "^1.2.2", + "unconfig-core": "^7.5.0" + }, + "bin": { + "tsdown": "dist/run.mjs" + }, + "engines": { + "node": "^22.18.0 || >=24.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@arethetypeswrong/core": "^0.18.1", + "@tsdown/css": "0.22.1", + "@tsdown/exe": "0.22.1", + "@vitejs/devtools": "*", + "publint": "^0.3.8", + "tsx": "*", + "typescript": "^5.0.0 || ^6.0.0", + "unplugin-unused": "^0.5.0", + "unrun": "*" + }, + "peerDependenciesMeta": { + "@arethetypeswrong/core": { + "optional": true + }, + "@tsdown/css": { + "optional": true + }, + "@tsdown/exe": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "publint": { + "optional": true + }, + "tsx": { + "optional": true + }, + "typescript": { + "optional": true + }, + "unplugin-unused": { + "optional": true + }, + "unrun": { + "optional": true + } + } + }, + "node_modules/tsdown/node_modules/cac": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cac/-/cac-7.0.0.tgz", + "integrity": "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/tsdown/node_modules/hookable": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.1.tgz", + "integrity": "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tsdown/node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tsdown/node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -7982,6 +8169,20 @@ "node": ">=0.8.0" } }, + "node_modules/unconfig-core": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/unconfig-core/-/unconfig-core-7.5.0.tgz", + "integrity": "sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@quansync/fs": "^1.0.0", + "quansync": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/unist-util-is": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", @@ -8065,37 +8266,6 @@ "node": ">= 10.0.0" } }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -8573,13 +8743,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 1d59bc5c8..b9c5fc3b9 100644 --- a/package.json +++ b/package.json @@ -64,9 +64,9 @@ "bitap" ], "scripts": { - "dev": "rollup -w -c scripts/configs.cjs --environment TARGET:esm-dev-full", - "dev:cjs": "rollup -w -c scripts/configs.cjs --environment TARGET:commonjs-dev-full", - "build": "rm -r dist && mkdir dist && node ./scripts/build.main.cjs", + "dev": "tsdown --watch", + "dev:cjs": "tsdown --watch", + "build": "rm -rf dist && tsdown", "test": "vitest run", "typecheck": "tsc -p src/tsconfig.json --noEmit", "lint": "eslint src test", @@ -102,14 +102,9 @@ "fast-uri": "^3.1.2" }, "devDependencies": { - "@babel/core": "^7.20.12", - "@babel/preset-typescript": "7.18.6", "@commitlint/cli": "^17.4.2", "@commitlint/config-conventional": "^17.4.2", "@monaco-editor/loader": "^1.3.2", - "@rollup/plugin-babel": "^7.0.0", - "@rollup/plugin-node-resolve": "^16.0.3", - "@rollup/plugin-replace": "^6.0.3", "@sapphire/stopwatch": "^1.5.0", "@sapphire/utilities": "^3.11.0", "eslint": "^9.39.4", @@ -118,10 +113,8 @@ "monaco-editor": "^0.34.1", "prettier": "^3.8.3", "replace-in-file": "^6.3.5", - "rollup": "^4.60.1", - "rollup-plugin-dts": "^6.4.1", "standard-version": "^9.5.0", - "terser": "^5.16.1", + "tsdown": "0.22.1", "typescript": "^6.0.2", "typescript-eslint": "^8.58.0", "vitepress": "^1.6.4", diff --git a/scripts/build.main.cjs b/scripts/build.main.cjs deleted file mode 100644 index e67ac71b3..000000000 --- a/scripts/build.main.cjs +++ /dev/null @@ -1,118 +0,0 @@ -const fs = require('fs') -const path = require('path') -const zlib = require('zlib') -const terser = require('terser') -const rollup = require('rollup') -const configs = require('./configs.cjs') -const { rollupConfigs, literalTypes } = require('./config-types.cjs') - -if (!fs.existsSync('dist')) { - fs.mkdirSync('dist') -} - -build(Object.keys(configs).map((key) => configs[key])) - -async function build(builds) { - for (const entry of builds) { - try { - await buildEntry(entry) - } catch (err) { - logError(err) - } - } - - try { - await buildTypes() - } catch (err) { - logError(err) - } -} - -async function buildEntry(config) { - const output = config.output - const { file, banner } = output - const isProd = /(min|prod)\.(?:c|m)?js$/.test(file) - - const bundle = await rollup.rollup(config) - const { - output: [{ code }] - } = await bundle.generate(output) - - return isProd - ? write(file, await minify(banner, code), true) - : write(file, code) -} - -async function buildTypes() { - for (const config of rollupConfigs) { - const { output } = config - const { file } = output - - const bundle = await rollup.rollup(config) - const { - output: [{ code }] - } = await bundle.generate(output) - - await write(file, code) - } - - // Hand-written declarations that rollup-plugin-dts can't synthesize. - for (const { file, code } of literalTypes) { - await write(file, code) - } -} - -async function minify(banner, code) { - const output = await terser.minify(code, { - toplevel: true, - output: { - ascii_only: true - }, - compress: { - pure_funcs: ['makeMap'] - } - }) - return (banner || '') + output.code -} - -function write(dest, code, zip) { - return new Promise((resolve, reject) => { - function report(extra) { - console.log( - blue(path.relative(process.cwd(), dest)) + - ' ' + - getSize(code) + - (extra || '') - ) - resolve() - } - - fs.writeFile(dest, code, (err) => { - if (err) return reject(err) - if (zip) { - zlib.gzip(code, (err, zipped) => { - if (err) return reject(err) - report(` (gzipped: ${getSize(zipped)})`) - }) - } else { - report() - } - }) - }) -} - -function getSize(code) { - return (code.length / 1024).toFixed(2) + 'kb' -} - -function logError(e) { - // Fail the process so a partial or failed build can't pass CI or be - // published by the release flow. We still log and keep going so every - // error in the run is reported, but the exit code ends up non-zero. - process.exitCode = 1 - console.error(e) -} - -function blue(str) { - return `\x1b[1m\x1b[34m${str}\x1b[39m\x1b[22m` -} diff --git a/scripts/config-types.cjs b/scripts/config-types.cjs deleted file mode 100644 index 9bba51095..000000000 --- a/scripts/config-types.cjs +++ /dev/null @@ -1,57 +0,0 @@ -const path = require('path') -const resolve = (_path) => path.resolve(__dirname, '../', _path) -const dts = require('rollup-plugin-dts').default; -const pckg = require('../package.json') -const typescript = require('typescript') - -const FILENAME = 'fuse' -const VERSION = process.env.VERSION || pckg.version - -const banner = [ - `// Type definitions for Fuse.js v${VERSION}`, - `// TypeScript v${typescript.version}` -].join('\n') - -function genTypeConfig(input, dest) { - return { - input: resolve(input), - output: { - banner, - file: resolve(dest), - format: 'es', - name: 'Fuse' - }, - plugins: [ - dts({ - tsconfig: resolve('src/tsconfig.json') - }) - ], - cache: false - } -} - -// One rollup-dts bundle per public entry point. The worker API ships from a -// separate entry (fuse.js/worker), so it needs its own declaration file. -const rollupConfigs = [ - genTypeConfig('src/entry.ts', `dist/${FILENAME}.d.ts`), - genTypeConfig('src/workers/index.ts', `dist/${FILENAME}-worker.d.ts`) -] - -// The ./worker-script entry (dist/fuse.worker.mjs) is a side-effect Worker -// module with no exports; loading it on the main thread throws. Bundlers -// resolve the import to the script's asset URL (a string), which is the only -// honest public type. rollup-plugin-dts can't synthesize a string default -// from a no-export module, so emit this declaration literally. -const literalTypes = [ - { - file: resolve(`dist/${FILENAME}.worker.d.ts`), - code: - banner + - '\n// `fuse.js/worker-script` resolves to the worker script URL,' + - ' surfaced\n// by your bundler as a string.\n' + - 'declare const workerScriptUrl: string\n' + - 'export default workerScriptUrl\n' - } -] - -module.exports = { rollupConfigs, literalTypes }; diff --git a/scripts/configs.cjs b/scripts/configs.cjs deleted file mode 100644 index 00006dbc0..000000000 --- a/scripts/configs.cjs +++ /dev/null @@ -1,198 +0,0 @@ -const path = require('path') -const replace = require('@rollup/plugin-replace') -const { nodeResolve } = require('@rollup/plugin-node-resolve') -const { babel } = require('@rollup/plugin-babel') -const pckg = require('../package.json') - -const FILENAME = 'fuse' -const VERSION = process.env.VERSION || pckg.version -const AUTHOR = pckg.author -const HOMEPAGE = pckg.homepage -const DESCRIPTION = pckg.description - -const banner = `/** - * Fuse.js v${VERSION} - ${DESCRIPTION} (${HOMEPAGE}) - * - * Copyright (c) ${new Date().getFullYear()} ${AUTHOR.name} (${AUTHOR.url}) - * All Rights Reserved. Apache Software License 2.0 - * - * http://www.apache.org/licenses/LICENSE-2.0 - */\n` - -const resolve = (_path) => path.resolve(__dirname, '../', _path) - -const FeatureFlags = { - LOGICAL_SEARCH_ENABLED: false, - EXTENDED_SEARCH_ENABLED: false, - TOKEN_SEARCH_ENABLED: false -} - -const fullBuildFeatures = { - LOGICAL_SEARCH_ENABLED: true, - EXTENDED_SEARCH_ENABLED: true, - TOKEN_SEARCH_ENABLED: true -} - -const builds = { - // CommonJS full build - 'commonjs-dev-full': { - dest: `dist/${FILENAME}.cjs`, - env: 'development', - features: { - ...fullBuildFeatures - }, - format: 'cjs' - }, - // CommonJS full minified build - 'commonjs-prod-full': { - dest: `dist/${FILENAME}.min.cjs`, - env: 'production', - features: { - ...fullBuildFeatures - }, - format: 'cjs' - }, - // CommonJS basic build - 'commonjs-dev-basic': { - dest: `dist/${FILENAME}.basic.cjs`, - env: 'development', - format: 'cjs' - }, - // CommonJS basic minified build - 'commonjs-prod-basic': { - dest: `dist/${FILENAME}.basic.min.cjs`, - env: 'production', - format: 'cjs' - }, - // ES modules build (for bundlers) - 'esm-dev-full': { - dest: `dist/${FILENAME}.mjs`, - format: 'es', - env: 'development', - features: { - ...fullBuildFeatures - }, - - }, - 'esm-prod-full': { - dest: `dist/${FILENAME}.min.mjs`, - format: 'es', - env: 'production', - features: { - ...fullBuildFeatures - }, - - }, - 'esm-basic': { - dest: `dist/${FILENAME}.basic.mjs`, - format: 'es', - env: 'development', - - }, - 'esm-prod-basic': { - dest: `dist/${FILENAME}.basic.min.mjs`, - format: 'es', - env: 'production', - - }, - // FuseWorker class (lightweight proxy, no Fuse bundled) - 'fuse-worker-esm': { - input: 'src/workers/index.ts', - dest: `dist/${FILENAME}-worker.mjs`, - format: 'es', - env: 'production' - }, - 'fuse-worker-cjs': { - input: 'src/workers/index.ts', - dest: `dist/${FILENAME}-worker.cjs`, - format: 'cjs', - exports: 'named', - env: 'production' - }, - // Worker script (self-contained, includes Fuse) - 'worker-esm': { - input: 'src/workers/worker.ts', - dest: `dist/${FILENAME}.worker.mjs`, - format: 'es', - env: 'production', - features: { - ...fullBuildFeatures - } - }, - 'worker-esm-min': { - input: 'src/workers/worker.ts', - dest: `dist/${FILENAME}.worker.min.mjs`, - format: 'es', - env: 'production', - features: { - ...fullBuildFeatures - } - } -} - -const defaultVars = { - __VERSION__: VERSION, - preventAssignment: true -} - -const defaultFeatures = Object.keys(FeatureFlags).reduce((map, key) => { - map[`process.env.${key}`] = FeatureFlags[key] - return map -}, {}) - -function genConfig(options) { - // built-in vars - const vars = { ...defaultVars, ...defaultFeatures } - - const config = { - input: resolve(options.input || 'src/entry.ts'), - plugins: [nodeResolve({ extensions: ['.ts', '.js'] }), ...(options.plugins || [])], - output: { - banner, - file: resolve(options.dest), - format: options.format, - name: 'Fuse', - exports: options.exports || (options.format === 'cjs' ? 'default' : undefined) - } - } - - // build-specific env - if (options.env) { - vars['process.env.NODE_ENV'] = JSON.stringify(options.env) - } - - // feature flags - if (options.features) { - Object.keys(options.features).forEach((key) => { - vars[`process.env.${key}`] = JSON.stringify(options.features[key]) - }) - } - - config.plugins.push(replace(vars)) - - // Strip TypeScript, no downlevel transpilation - config.plugins.push(babel({ - babelHelpers: 'bundled', - extensions: ['.js', '.ts'], - configFile: false, - presets: ['@babel/preset-typescript'] - })) - - return config -} - -function mapValues(obj, fn) { - const res = {} - - Object.keys(obj).forEach((key) => { - res[key] = fn(obj[key], key) - }) - - return res -} - -if (process.env.TARGET) { - module.exports = genConfig(builds[process.env.TARGET]) -} else { - module.exports = mapValues(builds, genConfig) -} diff --git a/src/entry.ts b/src/entry.ts index 2c9353aa6..4b14f4ea9 100644 --- a/src/entry.ts +++ b/src/entry.ts @@ -7,7 +7,7 @@ import TokenSearch from './search/token' import register, { createSearcher } from './core/register' import * as ErrorMsg from './core/errorMessages' -Fuse.version = '__VERSION__' +Fuse.version = __VERSION__ Fuse.createIndex = createIndex Fuse.parseIndex = parseIndex Fuse.config = Config diff --git a/src/env.d.ts b/src/env.d.ts index d62b41637..ebed3a808 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -6,3 +6,13 @@ declare let process: { TOKEN_SEARCH_ENABLED?: string | boolean } } + +// Injected at build time by tsdown `define` (and vitest `define` for the tests +// that import `src/entry` directly). Replaced with the package.json version. +declare const __VERSION__: string + +// Injected per-format by tsdown in the worker-proxy build: `true` in the CJS +// output, `false` in ESM. Lets FuseWorker prefer the document-captured base in +// browser-CJS bundles, where import.meta.url's CJS rewrite (require('url')/ +// __filename) may be polyfilled to a virtual path. (vitest defines it `false`.) +declare const __WORKER_IS_CJS__: boolean diff --git a/src/workers/FuseWorker.ts b/src/workers/FuseWorker.ts index 57663d80d..428327eb7 100644 --- a/src/workers/FuseWorker.ts +++ b/src/workers/FuseWorker.ts @@ -34,6 +34,52 @@ function getDefaultWorkerCount(): number { return Math.min(hw, DEFAULT_MAX_WORKERS) } +// Capture the executing script's URL at MODULE-EVALUATION time. In browsers +// `document.currentScript` is only non-null while the script is synchronously +// running, so reading it later (inside the constructor, e.g. from an event +// handler) would miss it and resolve relative to the page instead. This mirrors +// the timing of the dual shim the old Rollup build emitted into the CJS output. +// Unused in ESM (the catch below is never reached there) and undefined in Node. +const browserWorkerBase: string | undefined = + typeof document !== 'undefined' + ? (document.currentScript as HTMLScriptElement | null)?.src || + new URL('fuse-worker.cjs', document.baseURI).href + : undefined + +// Resolve the bundled worker script's URL without depending on a specific +// bundler's `import.meta.url` codegen. The `new URL('./fuse.worker.mjs', +// import.meta.url)` literal is preserved verbatim so bundlers (Vite/webpack) +// statically detect and rewrite the worker asset for the ESM build; it is +// correct for node-ESM, browser-ESM, and node-CJS. Only browser-CJS — where the +// CJS rewrite's `require('url')`/`__filename` is unavailable and the expression +// throws — falls back to the module-eval-captured browser base. +function resolveDefaultWorkerUrl(): URL { + // In the CJS build, a browser bundle must resolve relative to the executing + // script, NOT via import.meta.url: its CJS rewrite (require('url')/__filename) + // can be polyfilled by browser bundlers (webpack/browserify) to a virtual path, + // which would silently win over the real script location. So when this is the + // CJS output and a document is present (browser-CJS), prefer the captured base. + // This matches the old Rollup CJS shim, which always preferred document in the + // browser. The ESM build keeps the import.meta.url literal below so bundlers + // detect + rewrite the worker asset (browser-ESM) and Node resolves it natively. + if (__WORKER_IS_CJS__ && browserWorkerBase !== undefined) { + return new URL('./fuse.worker.mjs', browserWorkerBase) + } + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore -- import.meta.url is provided by the bundler per output format + return new URL('./fuse.worker.mjs', import.meta.url) + } catch { + if (browserWorkerBase !== undefined) { + return new URL('./fuse.worker.mjs', browserWorkerBase) + } + throw new Error( + 'FuseWorker: unable to resolve the worker script URL automatically; ' + + 'pass workerOptions.workerUrl explicitly.' + ) + } +} + export default class FuseWorker { private _options: IFuseOptions private _workerOptions: FuseWorkerOptions @@ -62,11 +108,7 @@ export default class FuseWorker { if (this._options.useTokenSearch) { throw new Error(ErrorMsg.FUSE_WORKER_TOKEN_SEARCH_UNSUPPORTED) } - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore -- import.meta.url is resolved by Rollup at build time - this._workerUrl = - this._workerOptions.workerUrl || - new URL('./fuse.worker.mjs', import.meta.url) + this._workerUrl = this._workerOptions.workerUrl || resolveDefaultWorkerUrl() } private static _assertNoFunctionOptions(options: IFuseOptions): void { diff --git a/test/cjs-interop.test.js b/test/cjs-interop.test.js new file mode 100644 index 000000000..29538dd07 --- /dev/null +++ b/test/cjs-interop.test.js @@ -0,0 +1,53 @@ +// Runtime guard for the CommonJS export contract that the build must preserve: +// `require('fuse.js')` returns the Fuse *constructor* directly (not `{ default }`), +// and the worker proxy exposes a *named* `FuseWorker` export. The rest of the +// suite imports the ESM dist; nothing else exercises the .cjs builds at runtime, +// so a `{ default }` regression (the classic esbuild-interop trap) would ship +// uncaught without this. Runs against the BUILT dist — rebuild if you changed source. +import { describe, test, expect, beforeAll } from 'vitest' +import { createRequire } from 'node:module' +import { existsSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { join, dirname } from 'node:path' + +const require = createRequire(import.meta.url) +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..') +const pkg = require('../package.json') + +const LIB_CJS = [ + 'dist/fuse.cjs', + 'dist/fuse.min.cjs', + 'dist/fuse.basic.cjs', + 'dist/fuse.basic.min.cjs' +] + +describe('CommonJS export contract', () => { + beforeAll(() => { + for (const f of [...LIB_CJS, 'dist/fuse-worker.cjs']) { + if (!existsSync(join(repoRoot, f))) { + throw new Error(`${f} missing - run \`npm run build\` before this test`) + } + } + }) + + for (const rel of LIB_CJS) { + test(`require('${rel}') is the constructor, not { default }`, () => { + const Fuse = require(join(repoRoot, rel)) + expect(typeof Fuse).toBe('function') + // Not an interop object with a `.default` key. + expect(Fuse).not.toHaveProperty('default') + // Usable as a constructor. + const f = new Fuse([{ t: 'hello' }], { keys: ['t'] }) + expect(f.search('hello').length).toBeGreaterThan(0) + // Build-time `__VERSION__` was injected, not left as the literal token. + expect(Fuse.version).toBe(pkg.version) + }) + } + + test("require('dist/fuse-worker.cjs') exposes a named FuseWorker export", () => { + const mod = require(join(repoRoot, 'dist/fuse-worker.cjs')) + expect(typeof mod.FuseWorker).toBe('function') + // Named, not default: the worker proxy intentionally differs from the lib. + expect(mod).not.toHaveProperty('default') + }) +}) diff --git a/test/package-types.test.ts b/test/package-types.test.ts index 4375c88f0..7f74b603c 100644 --- a/test/package-types.test.ts +++ b/test/package-types.test.ts @@ -10,7 +10,14 @@ // committed dist is used, so rebuild if you changed source. import { describe, test, expect, beforeAll, afterAll } from 'vitest' import ts from 'typescript' -import { mkdtempSync, mkdirSync, symlinkSync, writeFileSync, rmSync, existsSync } from 'node:fs' +import { + mkdtempSync, + mkdirSync, + symlinkSync, + writeFileSync, + rmSync, + existsSync +} from 'node:fs' import { tmpdir } from 'node:os' import { join, dirname } from 'node:path' import { fileURLToPath } from 'node:url' @@ -25,9 +32,18 @@ const CASES = [ spec: 'fuse.js', code: `import Fuse from 'fuse.js'\nconst f = new Fuse<{ t: string }>([], { keys: ['t'] })\nvoid f.search('x')\n` }, - { spec: 'fuse.js/min', code: `import Fuse from 'fuse.js/min'\nvoid new Fuse([])\n` }, - { spec: 'fuse.js/basic', code: `import Fuse from 'fuse.js/basic'\nvoid new Fuse([])\n` }, - { spec: 'fuse.js/min-basic', code: `import Fuse from 'fuse.js/min-basic'\nvoid new Fuse([])\n` }, + { + spec: 'fuse.js/min', + code: `import Fuse from 'fuse.js/min'\nvoid new Fuse([])\n` + }, + { + spec: 'fuse.js/basic', + code: `import Fuse from 'fuse.js/basic'\nvoid new Fuse([])\n` + }, + { + spec: 'fuse.js/min-basic', + code: `import Fuse from 'fuse.js/min-basic'\nvoid new Fuse([])\n` + }, { // Exercise FuseWorkerOptions too (numWorkers + the `string | URL` // workerUrl branch), so a regression in those option types is caught. @@ -48,6 +64,31 @@ const CASES = [ `const notAny: IsAny extends true ? never : true = true\n` + `const s: string = workerUrl\n` + `void s\nvoid notAny\n` + }, + { + // Swapping the dts generator (rollup-plugin-dts -> tsdown's) could silently + // drop or rename a re-exported type. The cases above only exercise the + // default `Fuse`/`FuseWorker` values, so a missing *type* export would slip + // through. Import every public type from `fuse.js`; a dropped/renamed one + // surfaces as TS2305 on this consumer file. + spec: 'fuse.js (public type surface)', + code: + `import type {\n` + + ` IFuseOptions, FuseGetFunction, FuseOptionKey, FuseOptionKeyObject,\n` + + ` FuseResult, FuseResultMatch, FuseSearchOptions, FuseSortFunction,\n` + + ` FuseSortFunctionArg, FuseSortFunctionItem, FuseSortFunctionMatch,\n` + + ` FuseSortFunctionMatchList, FuseTokenizeFunction, FuseIndexOptions,\n` + + ` FuseIndexRecords, FuseIndexObjectRecord, FuseIndexStringRecord,\n` + + ` RecordEntry, RecordEntryObject, RecordEntryArrayItem, RangeTuple,\n` + + ` Expression, SearchResult, FuseIndex\n` + + `} from 'fuse.js'\n` + + `export {}\n` + }, + { + spec: 'fuse.js/worker (type surface)', + code: + `import type { FuseWorker, FuseWorkerOptions } from 'fuse.js/worker'\n` + + `export type { FuseWorker, FuseWorkerOptions }\n` } ] @@ -95,7 +136,9 @@ describe('published type declarations resolve for every export', () => { beforeAll(() => { for (const f of ['fuse.d.ts', 'fuse-worker.d.ts', 'fuse.worker.d.ts']) { if (!existsSync(join(repoRoot, 'dist', f))) { - throw new Error(`dist/${f} missing - run \`npm run build\` before this test`) + throw new Error( + `dist/${f} missing - run \`npm run build\` before this test` + ) } } tmp = mkdtempSync(join(tmpdir(), 'fusejs-types-')) @@ -124,11 +167,13 @@ describe('published type declarations resolve for every export', () => { for (const d of ts.getPreEmitDiagnostics(program)) { const fileName = d.file && d.file.fileName - if (fileName && fileName.includes('/node_modules/typescript/lib/')) continue + if (fileName && fileName.includes('/node_modules/typescript/lib/')) + continue const msg = ts.flattenDiagnosticMessageText(d.messageText, ' ') const key = fileName && fileToKey.get(fileName) if (key) errorsBySpec.get(key)!.push(msg) - else otherErrors.push(`[${mode.name}] ${fileName ?? '(no file)'}: ${msg}`) + else + otherErrors.push(`[${mode.name}] ${fileName ?? '(no file)'}: ${msg}`) } } }) diff --git a/test/worker-url.test.js b/test/worker-url.test.js new file mode 100644 index 000000000..a3e28a353 --- /dev/null +++ b/test/worker-url.test.js @@ -0,0 +1,164 @@ +// The FuseWorker auto-resolves its worker script URL via +// `new URL('./fuse.worker.mjs', import.meta.url)`. `import.meta.url` is ESM-only +// syntax, so each output format/environment resolves it differently. This gates +// all four target environments so a bundler/format change can't silently break +// worker-URL resolution (the migration's highest-risk behavior). Runs against +// the BUILT dist — rebuild if you changed source. +import { describe, test, expect, beforeAll } from 'vitest' +import { build } from 'vite' +import { createRequire } from 'node:module' +import vm from 'node:vm' +import { + existsSync, + readFileSync, + mkdtempSync, + writeFileSync, + rmSync +} from 'node:fs' +import { tmpdir } from 'node:os' +import { join, dirname } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' + +const require = createRequire(import.meta.url) +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..') +const WORKER_CJS = join(repoRoot, 'dist/fuse-worker.cjs') +const WORKER_MJS = join(repoRoot, 'dist/fuse-worker.mjs') + +describe('worker-URL auto-resolution', () => { + beforeAll(() => { + for (const f of [ + WORKER_CJS, + WORKER_MJS, + join(repoRoot, 'dist/fuse.worker.mjs') + ]) { + if (!existsSync(f)) { + throw new Error(`${f} missing - run \`npm run build\` before this test`) + } + } + }) + + test('node-CJS: resolves via __filename (require path)', () => { + const { FuseWorker } = require(WORKER_CJS) + const w = new FuseWorker([], { keys: ['t'] }) + const url = String(w._workerUrl) + expect(url.startsWith('file:')).toBe(true) + expect(url.endsWith('/dist/fuse.worker.mjs')).toBe(true) + }) + + test('node-ESM: resolves via native import.meta.url', async () => { + const { FuseWorker } = await import(pathToFileURL(WORKER_MJS).href) + const w = new FuseWorker([], { keys: ['t'] }) + const url = String(w._workerUrl) + expect(url.startsWith('file:')).toBe(true) + expect(url.endsWith('/dist/fuse.worker.mjs')).toBe(true) + }) + + test('browser-CJS: resolves via document base captured at module-eval (not page), currentScript cleared before construct', () => { + const code = readFileSync(WORKER_CJS, 'utf8') + const scriptSrc = 'https://cdn.example.com/libs/fuse-worker.cjs' + const doc = { + currentScript: { src: scriptSrc }, + baseURI: 'https://cdn.example.com/page/index.html' + } + // Browser-CJS: `document` exists, but `require`/`__filename` do not, so the + // CJS rewrite of import.meta.url throws and the document fallback is taken. + // CommonJS: `exports` and `module.exports` must be the same object. + const moduleObj = { exports: {} } + const sandbox = { + module: moduleObj, + exports: moduleObj.exports, + URL, + document: doc, + console + } + sandbox.globalThis = sandbox + vm.createContext(sandbox) + // Module evaluation: browserWorkerBase is captured from currentScript here. + vm.runInContext(code, sandbox) + // The browser clears currentScript after the script finishes evaluating; + // if resolution read it lazily (in the constructor) it would miss it. + doc.currentScript = null + + const { FuseWorker } = sandbox.module.exports + const w = new FuseWorker([], { keys: ['t'] }) + // Resolved relative to the captured *script* URL, not the page baseURI. + expect(String(w._workerUrl)).toBe( + 'https://cdn.example.com/libs/fuse.worker.mjs' + ) + }) + + test('browser-CJS with Node polyfills (require/__filename present): document base still wins', () => { + const code = readFileSync(WORKER_CJS, 'utf8') + const scriptSrc = 'https://cdn.example.com/libs/fuse-worker.cjs' + const doc = { + currentScript: { src: scriptSrc }, + baseURI: 'https://cdn.example.com/page/index.html' + } + // A browser bundler (webpack/browserify) may polyfill `require('url')` and + // `__filename` to a virtual path. The CJS build must NOT prefer that over the + // real script location — `__WORKER_IS_CJS__` routes browser-CJS to `document`. + const moduleObj = { exports: {} } + const sandbox = { + module: moduleObj, + exports: moduleObj.exports, + URL, + document: doc, + __filename: '/virtual/bundle/fuse-worker.cjs', + require(id) { + if (id === 'url') { + return { pathToFileURL: (p) => ({ href: 'file://' + p }) } + } + throw new Error('unexpected require ' + id) + }, + console + } + sandbox.globalThis = sandbox + vm.createContext(sandbox) + vm.runInContext(code, sandbox) + doc.currentScript = null + const { FuseWorker } = sandbox.module.exports + const w = new FuseWorker([], { keys: ['t'] }) + expect(String(w._workerUrl)).toBe( + 'https://cdn.example.com/libs/fuse.worker.mjs' + ) + }) + + test('browser-ESM: Vite statically detects + rewrites the worker asset', async () => { + const tmp = mkdtempSync(join(tmpdir(), 'fusejs-vite-')) + try { + const entry = join(tmp, 'app.mjs') + writeFileSync( + entry, + `import { FuseWorker } from ${JSON.stringify(WORKER_MJS)}\n` + + `new FuseWorker([], { keys: ['t'] })\n` + ) + const result = await build({ + root: tmp, + logLevel: 'silent', + build: { + write: false, + minify: false, + target: 'esnext', + rollupOptions: { input: entry, output: { format: 'es' } } + } + }) + const outputs = (Array.isArray(result) ? result : [result]).flatMap( + (r) => r.output ?? [] + ) + // Vite resolved `./fuse.worker.mjs` (sibling of the imported proxy) and + // emitted it as a worker asset. + const workerAsset = outputs.find((o) => /fuse\.worker/.test(o.fileName)) + expect(workerAsset, 'Vite should emit a fuse.worker asset').toBeTruthy() + // The literal `new URL('./fuse.worker.mjs', import.meta.url)` was rewritten + // to point at that emitted asset (not left verbatim, which would resolve + // against the app bundle's own location at runtime). + const entryChunk = outputs.find((o) => o.type === 'chunk' && o.isEntry) + expect(entryChunk.code).toMatch(/fuse\.worker/) + expect(entryChunk.code).not.toMatch( + /new URL\(\s*["']\.\/fuse\.worker\.mjs["']\s*,\s*import\.meta\.url\s*\)/ + ) + } finally { + rmSync(tmp, { recursive: true, force: true }) + } + }) +}) diff --git a/tsdown.config.ts b/tsdown.config.ts new file mode 100644 index 000000000..c032f8b21 --- /dev/null +++ b/tsdown.config.ts @@ -0,0 +1,146 @@ +import { defineConfig } from 'tsdown' +import { writeFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { createRequire } from 'node:module' + +// Build matrix (12 runtime files + 3 declaration files), grouped by +// feature-set x NODE_ENV x minify so each group can carry its own `define`. +// Replaces the old Rollup/Babel/Terser orchestrator (scripts/configs.cjs + +// config-types.cjs + build.main.cjs). See .plans/active/017-tsup-build-migration.md. + +const require = createRequire(import.meta.url) +const pkg = require('./package.json') +const VERSION = process.env.VERSION || pkg.version + +const banner = `/** + * Fuse.js v${VERSION} - ${pkg.description} (${pkg.homepage}) + * + * Copyright (c) ${new Date().getFullYear()} ${pkg.author.name} (${pkg.author.url}) + * All Rights Reserved. Apache Software License 2.0 + * + * http://www.apache.org/licenses/LICENSE-2.0 + */` + +const FULL = { LOGICAL: 'true', EXTENDED: 'true', TOKEN: 'true' } +const BASIC = { LOGICAL: 'false', EXTENDED: 'false', TOKEN: 'false' } + +function define(flags: typeof FULL, env: 'development' | 'production') { + return { + __VERSION__: JSON.stringify(VERSION), + 'process.env.NODE_ENV': JSON.stringify(env), + 'process.env.LOGICAL_SEARCH_ENABLED': flags.LOGICAL, + 'process.env.EXTENDED_SEARCH_ENABLED': flags.EXTENDED, + 'process.env.TOKEN_SEARCH_ENABLED': flags.TOKEN + } +} + +const shared = { + // Match the tsconfig baseline; avoids tsdown deriving target: node10 from + // package.json engines (which would downlevel modern syntax). + target: 'es2020', + platform: 'neutral', + treeshake: true, + sourcemap: false, + outDir: 'dist', + // The npm `build` script does `rm -rf dist` once; per-config clean would + // race-wipe sibling outputs. + clean: false, + dts: false, + // rolldown defaults ESM to `.js`; the package exports map uses `.mjs`/`.cjs`. + // Force a single `.d.ts` (not per-format `.d.cts`/`.d.mts`) — the types are + // format-agnostic and all exports map conditions point at one `.d.ts`. + outExtensions: ({ format }: { format: string }) => ({ + js: format === 'cjs' ? '.cjs' : '.mjs', + dts: '.d.ts' + }), + banner: { js: banner } +} as const + +// The `fuse.js/worker-script` entry is a side-effect Worker module with no +// exports; bundlers resolve the import to the script's asset URL (a string). +// rolldown-dts can't synthesize a string default from a no-export module, so +// emit this declaration literally (mirrors the old config-types.cjs hack). +function writeWorkerScriptDts() { + const tsVersion = require('typescript').version + const code = + `// Type definitions for Fuse.js v${VERSION}\n` + + `// TypeScript v${tsVersion}\n` + + '// `fuse.js/worker-script` resolves to the worker script URL, surfaced\n' + + '// by your bundler as a string.\n' + + 'declare const workerScriptUrl: string\n' + + 'export default workerScriptUrl\n' + writeFileSync(resolve('dist/fuse.worker.d.ts'), code) +} + +export default defineConfig([ + // full build (dev, non-min) — default package export; owns fuse.d.ts + { + ...shared, + entry: { fuse: 'src/entry.ts' }, + format: ['cjs', 'esm'], + minify: false, + define: define(FULL, 'development'), + dts: true + }, + // full build (prod, min) — ./min + { + ...shared, + entry: { 'fuse.min': 'src/entry.ts' }, + format: ['cjs', 'esm'], + minify: true, + define: define(FULL, 'production') + }, + // basic build (dev, non-min) — ./basic + { + ...shared, + entry: { 'fuse.basic': 'src/entry.ts' }, + format: ['cjs', 'esm'], + minify: false, + define: define(BASIC, 'development') + }, + // basic build (prod, min) — ./min-basic + { + ...shared, + entry: { 'fuse.basic.min': 'src/entry.ts' }, + format: ['cjs', 'esm'], + minify: true, + define: define(BASIC, 'production') + }, + // worker proxy (FuseWorker class; named CJS export) — ./worker. + // CJS and ESM are split so `__WORKER_IS_CJS__` can be defined per format: the + // CJS output prefers the document-captured base in browsers (import.meta.url's + // CJS rewrite may be polyfilled), while ESM keeps the static import.meta.url + // literal for bundler worker-asset detection. ESM owns the `.d.ts`. + { + ...shared, + entry: { 'fuse-worker': 'src/workers/index.ts' }, + format: ['cjs'], + minify: false, + define: { ...define(FULL, 'production'), __WORKER_IS_CJS__: 'true' } + }, + { + ...shared, + entry: { 'fuse-worker': 'src/workers/index.ts' }, + format: ['esm'], + minify: false, + define: { ...define(FULL, 'production'), __WORKER_IS_CJS__: 'false' }, + dts: true + }, + // self-contained worker script (ESM, side-effect) — ./worker-script; writes literal dts + { + ...shared, + entry: { 'fuse.worker': 'src/workers/worker.ts' }, + format: ['esm'], + minify: false, + define: define(FULL, 'production'), + onSuccess: writeWorkerScriptDts + }, + // self-contained worker script (ESM, min) + { + ...shared, + entry: { 'fuse.worker.min': 'src/workers/worker.ts' }, + format: ['esm'], + minify: true, + define: define(FULL, 'production') + } +]) diff --git a/vitest.config.ts b/vitest.config.ts index 50e92e7a7..4cb715759 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,6 +1,18 @@ import { defineConfig } from 'vitest/config' +import { createRequire } from 'node:module' + +// Tests that import `../src/entry` directly (e.g. fuse-worker, cache-invalidation) +// execute the source, where `Fuse.version = __VERSION__` is a build-time global. +// The production build injects it via tsdown `define`; mirror that here so the +// identifier isn't a `ReferenceError` under Vitest's esbuild transform. +const pkg = createRequire(import.meta.url)('./package.json') export default defineConfig({ + define: { + __VERSION__: JSON.stringify(pkg.version), + // Source-importing tests run ESM-like under vitest; never the CJS path. + __WORKER_IS_CJS__: 'false' + }, test: { globals: true },