Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

fuzzyjs

Package Overview
Dependencies
Maintainers
1
Versions
22
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

fuzzyjs - npm Package Compare versions

Comparing version 4.0.4 to 5.0.0

.cspell.json

96

benchmark/index.js

@@ -1,60 +0,70 @@

const Benchmark = require('benchmark')
const Benchmark = require("benchmark");
const { test, match, sort: fuzzySort, filter: fuzzyFilter } = require('../dist/index.umd')
const {
test,
match,
sort: fuzzySort,
filter: fuzzyFilter,
} = require("../build/cjs");
process.stdout.write('Setting up benmarks...')
process.stdout.write("Setting up benmarks...");
let cities = require('./cities.json')
let cities = require("./cities.json");
cities = cities.map((city, i) => ({ id: i, ...city }))
cities = cities.map((city, i) => ({ id: i, ...city }));
console.log(' Done.')
console.log(" Done.");
const benchmarkUnoptimizedSort = new Benchmark('unoptimized sort', () => {
cities.sort(fuzzySort('ah', { sourcePath: 'city' }))
})
const benchmarkUnoptimizedSort = new Benchmark("unoptimized sort", () => {
cities.sort(fuzzySort("ah", { sourceAccessor: (city) => city.city }));
});
const benchmarkOptimizedSort = new Benchmark('optimized sort', () => {
cities.sort(fuzzySort('ah', { sourcePath: 'city', idPath: 'id' }))
})
const benchmarkOptimizedSort = new Benchmark("optimized sort", () => {
cities.sort(
fuzzySort("ah", {
sourceAccessor: (city) => city.city,
idAccessor: (city) => city.id,
})
);
});
const benchmarkFilter = new Benchmark('filter', () => {
cities.filter(fuzzyFilter('ah', { sourcePath: 'city' }))
})
const benchmarkFilter = new Benchmark("filter", () => {
cities.filter(fuzzyFilter("ah", { sourceAccessor: (city) => city.city }));
});
const benchmarkMatch = new Benchmark('match', () => {
match('av', cities[0].city, { withRanges: true, withScore: true })
})
const benchmarkMatch = new Benchmark("match", () => {
match("av", cities[0].city, { withRanges: true, withScore: true });
});
const benchmarkTest = new Benchmark('test', () => {
test('av', cities[0].city)
})
const benchmarkTest = new Benchmark("test", () => {
test("av", cities[0].city);
});
process.stdout.write(`Running unoptimized sort on ${cities.length} objects...`)
benchmarkUnoptimizedSort.run()
console.log(' Done.')
process.stdout.write(`Running unoptimized sort on ${cities.length} objects...`);
benchmarkUnoptimizedSort.run();
console.log(" Done.");
process.stdout.write(`Running optimized sort on ${cities.length} objects...`)
benchmarkOptimizedSort.run()
console.log(' Done.')
process.stdout.write(`Running optimized sort on ${cities.length} objects...`);
benchmarkOptimizedSort.run();
console.log(" Done.");
process.stdout.write(`Running filter on ${cities.length} objects...`)
benchmarkFilter.run()
console.log(' Done.')
process.stdout.write(`Running filter on ${cities.length} objects...`);
benchmarkFilter.run();
console.log(" Done.");
process.stdout.write(`Running match...`)
benchmarkMatch.run()
console.log(' Done.')
process.stdout.write(`Running match...`);
benchmarkMatch.run();
console.log(" Done.");
process.stdout.write(`Running test...`)
benchmarkTest.run()
console.log(' Done.')
process.stdout.write(`Running test...`);
benchmarkTest.run();
console.log(" Done.");
console.log('')
console.log("");
console.log('Results:')
console.log(benchmarkUnoptimizedSort.toString())
console.log(benchmarkOptimizedSort.toString())
console.log(benchmarkFilter.toString())
console.log(benchmarkMatch.toString())
console.log(benchmarkTest.toString())
console.log("Results:");
console.log(benchmarkUnoptimizedSort.toString());
console.log(benchmarkOptimizedSort.toString());
console.log(benchmarkFilter.toString());
console.log(benchmarkMatch.toString());
console.log(benchmarkTest.toString());
{
"name": "fuzzyjs",
"version": "4.0.4",
"description": "Fuzzy matching in Javascript",
"version": "5.0.0",
"description": "Simple fuzzy matching",
"keywords": [

@@ -14,10 +14,9 @@ "fuzzy",

],
"main": "dist/index.umd.js",
"module": "dist/index.es5.js",
"typings": "dist/types/index.d.ts",
"homepage": "https://github.com/gjuchault/fuzzyjs",
"bugs": "https://github.com/gjuchault/fuzzyjs/issues",
"author": "Gabriel Juchault <gabriel.juchault@gmail.com>",
"repository": {
"type": "git",
"url": "git@github.com:gjuchault/fuzzyjs.git"
},
"repository": "gjuchault/fuzzyjs",
"main": "./build/cjs/index.js",
"module": "./build/esm/index.js",
"types": "./build/index.d.ts",
"license": "MIT",

@@ -27,98 +26,52 @@ "engines": {

},
"publishConfig": {
"access": "public"
},
"scripts": {
"lint": "prettier --write \"{src,test}/**/*.ts\"",
"prebuild": "rimraf dist",
"build": "tsc --module commonjs && rollup -c rollup.config.ts && typedoc --out docs --target es6 --theme minimal --mode file src",
"start": "rollup -c rollup.config.ts -w",
"test": "jest --coverage",
"test:watch": "jest --coverage --watch",
"test:prod": "yarn lint && yarn test --no-cache",
"report-coverage": "cat ./coverage/lcov.info | coveralls",
"semantic-release": "semantic-release",
"benchmark": "node benchmark",
"benchmark:brk": "node --inspect --debug-brk benchmark/withoutTimers",
"travis-deploy-once": "travis-deploy-once"
"build": "yarn clean && yarn type:dts && yarn type:build",
"clean": "node -r esbuild-register ./scripts/clean",
"type:dts": "tsc --emitDeclarationOnly",
"type:check": "tsc --noEmit",
"type:build": "node -r esbuild-register ./scripts/build",
"format": "prettier \"src/**/*.ts\" --write",
"format:check": "prettier \"src/**/*.ts\" --check",
"lint": "eslint src --ext .ts --fix",
"lint:check": "eslint src --ext .ts",
"test": "ava",
"test:coverage": "nyc ava && nyc report --reporter=html",
"spell:check": "cspell \"{README.md,CODE_OF_CONDUCT.md,CONTRIBUTING.md,.github/*.md,src/**/*.ts}\"",
"cz": "cz",
"semantic-release": "semantic-release"
},
"lint-staged": {
"{src,test}/**/*.ts": [
"prettier --write",
"git add"
]
},
"config": {
"commitizen": {
"path": "node_modules/cz-conventional-changelog"
}
},
"jest": {
"transform": {
".(ts|tsx)": "ts-jest"
},
"testEnvironment": "node",
"testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$",
"moduleFileExtensions": [
"ts",
"tsx",
"js"
],
"coveragePathIgnorePatterns": [
"/node_modules/",
"/test/"
],
"coverageThreshold": {
"global": {
"branches": 90,
"functions": 95,
"lines": 95,
"statements": 95
}
},
"collectCoverageFrom": [
"src/**/*.{js,ts}"
]
},
"prettier": {
"semi": false,
"singleQuote": true,
"parser": "typescript"
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
},
"devDependencies": {
"@commitlint/cli": "^8.0.0",
"@commitlint/config-conventional": "^8.0.0",
"@types/benchmark": "^1.0.31",
"@types/jest": "^25.1.4",
"@types/node": "^13.1.1",
"@semantic-release/changelog": "^5.0.1",
"@semantic-release/commit-analyzer": "^8.0.1",
"@semantic-release/github": "^7.2.3",
"@semantic-release/npm": "^7.1.3",
"@semantic-release/release-notes-generator": "^9.0.3",
"@types/node": "^16.0.0",
"@types/prompts": "^2.0.13",
"@typescript-eslint/eslint-plugin": "^4.28.2",
"@typescript-eslint/parser": "^4.28.2",
"ava": "^3.15.0",
"benchmark": "^2.1.4",
"colors": "^1.3.2",
"coveralls": "^3.0.2",
"cz-conventional-changelog": "^3.1.0",
"husky": "^4.2.3",
"jest": "^25.1.0",
"jest-config": "^25.1.0",
"lint-staged": "^10.0.8",
"lodash.camelcase": "^4.3.0",
"prettier": "^1.14.3",
"rimraf": "^3.0.0",
"rollup": "^2.0.6",
"rollup-plugin-commonjs": "^10.0.2",
"rollup-plugin-json": "^4.0.0",
"rollup-plugin-node-resolve": "^5.0.2",
"rollup-plugin-sourcemaps": "^0.5.0",
"rollup-plugin-typescript2": "^0.26.0",
"ts-jest": "^25.2.1",
"ts-node": "^8.3.0",
"typedoc": "^0.16.11",
"typescript": "^3.8.3"
"commitizen": "^4.2.4",
"cspell": "^5.6.6",
"cz-conventional-changelog": "^3.3.0",
"esbuild": "^0.12.15",
"esbuild-register": "^2.6.0",
"eslint": "^7.30.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-import": "^2.23.4",
"nyc": "^15.1.0",
"prettier": "^2.3.2",
"semantic-release": "^17.4.4",
"typescript": "^4.3.5"
},
"dependencies": {},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
"volta": {
"node": "16.4.1",
"yarn": "1.22.10",
"npm": "7.19.1"
}
}
# fuzzyjs
[![Build Status](https://travis-ci.org/gjuchault/fuzzyjs.svg?branch=master)](https://travis-ci.org/gjuchault/fuzzyjs)
[![Coverage Status](https://coveralls.io/repos/github/gjuchault/fuzzyjs/badge.svg?branch=master)](https://coveralls.io/github/gjuchault/fuzzyjs?branch=master)
![NPM](https://img.shields.io/npm/l/@gjuchault/typescript-library-starter)
![NPM](https://img.shields.io/npm/v/@gjuchault/typescript-library-starter)
![GitHub Workflow Status](https://github.com/gjuchault/typescript-library-starter/actions/workflows/typescript-library-starter.yml/badge.svg?branch=master)

@@ -15,14 +16,14 @@ fuzzyjs is a fuzzy search algorithm in javascript.

```ts
import { test } from 'fuzzyjs'
import { test } from "fuzzyjs";
test('ssjs', 'Set Syntax: JavaScript')
true
test("ssjs", "Set Syntax: JavaScript");
true;
```
```ts
const test: (query: string, source: string, opts?: TestOptions) => boolean
function test(query: string, source: string, opts?: TestOptions): boolean;
type TestOptions = {
caseSensitive: boolean // (default: false)
}
caseSensitive?: boolean; // (default: false)
};
```

@@ -37,8 +38,6 @@

match('ssjs', 'Set Syntax: JavaScript')
{ match: true, score: 22 }
match('ssjav', 'Set Syntax: JavaScript', { withScore: false, withRanges: true })
match('ssjav', 'Set Syntax: JavaScript')
{
match: true,
score: 22,
ranges: [

@@ -53,15 +52,15 @@ { start: 0, stop: 1 },

```ts
const match: (query: string, source: string, opts?: MatchOptions) => MatchResult
function match(query: string, source: string, opts?: MatchOptions): MatchResult;
type MatchOptions = TestOptions & {
strategy?: ScoreStrategy // (default: see below)
withRanges?: boolean // (default: false)
withScore?: boolean // (default: true)
}
type MatchOptions = {
caseSensitive?: boolean;
strategy?: ScoreStrategy; // (default: defaultStrategy, see below)
withScore?: boolean; // (default: true)
};
type MatchResult = {
match: boolean
score?: number
ranges?: MatchRange[]
}
match: boolean;
score: number; // only if `withScore` is true, else undefined
ranges: MatchRange[];
};
```

@@ -76,27 +75,24 @@

```ts
import { match, surround } from 'fuzzyjs'
import { match, surround } from "fuzzyjs";
const result = match('ssjav', 'Set Syntax: JavaScript', { withRanges: true })
const result = match("ssjav", "Set Syntax: JavaScript");
surround(
'Set Syntax: JavaScript',
{
result,
prefix: '<strong>',
suffix: '</strong>'
}
)
'<strong>S</strong>et <strong>S</strong>yntax: <strong>Jav</strong>aScript'
surround("Set Syntax: JavaScript", {
result,
prefix: "<strong>",
suffix: "</strong>",
});
("<strong>S</strong>et <strong>S</strong>yntax: <strong>Jav</strong>aScript");
```
```ts
const surround: (source: string, options: SurroundOptions) => string
function surround(source: string, options: SurroundOptions): string;
type SurroundOptions = {
result: {
ranges: MatchRange[]
}
prefix?: string // (default: '')
suffix?: string // (default: '')
}
ranges: MatchRange[];
};
prefix?: string; // (default: '')
suffix?: string; // (default: '')
};
```

@@ -107,28 +103,35 @@

Can be used as a [Array.prototype.filter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) callback.
You can use the `sourceAccessor` option if you pass an array of objects that contains the string you want to match.
```ts
import { filter as fuzzy } from 'fuzzyjs'
import { filter as fuzzy } from "fuzzyjs";
const sources = ['Set Syntax: JavaScript', 'Set Syntax: CSS', 'Set Syntax: HTML']
const sources = [
"Set Syntax: JavaScript",
"Set Syntax: CSS",
"Set Syntax: HTML",
];
sources.filter(fuzzy('ssjs'))
[ 'Set Syntax: JavaScript' ]
sources.filter(fuzzy("ssjs", { iterator: (item) => item }));
["Set Syntax: JavaScript"];
const sources = [
{ name: { foo: 'Set Syntax: JavaScript' } },
{ name: { foo: 'Set Syntax: CSS' } },
{ name: { foo: 'Set Syntax: HTML' } }
]
{ name: { foo: "Set Syntax: JavaScript" } },
{ name: { foo: "Set Syntax: CSS" } },
{ name: { foo: "Set Syntax: HTML" } },
];
sources.filter(fuzzy('ssjs', { sourceAccessor: source => source.name.foo }))
[ { name: { foo: 'Set Syntax: JavaScript' } } ]
sources.filter(fuzzy("ssjs", { iterator: (source) => source.name.foo }));
[{ name: { foo: "Set Syntax: JavaScript" } }];
```
```ts
const filter: (query: string, options?: FilterOptions) => (source: any) => boolean
function filter<TItem>(
query: string,
options: FilterOptions<TItem>
): (source: TItem) => boolean;
type FilterOptions = TestOptions & {
sourceAccessor?: (source: any) => string
}
type FilterOptions<TItem> = {
caseSensitive?: boolean;
iterator: (source: TItem) => string;
};
```

@@ -142,38 +145,37 @@

```ts
import { sort as fuzzy } from 'fuzzyjs'
import { sort as fuzzy } from "fuzzyjs";
const sources = ['Set Syntax: CSS', 'Set Syntax: HTML', 'Set Syntax: JavaScript']
const sources = [
"Set Syntax: CSS",
"Set Syntax: HTML",
"Set Syntax: JavaScript",
];
sources.sort(fuzzy('ssjs'))
[ 'Set Syntax: JavaScript', 'Set Syntax: CSS', 'Set Syntax: HTML' ]
sources.sort(fuzzy("ssjs", { iterator: (item) => item }));
[("Set Syntax: JavaScript", "Set Syntax: CSS", "Set Syntax: HTML")];
const sources = [
{ name: { id: 0, foo: 'Set Syntax: CSS' } },
{ name: { id: 1, foo: 'Set Syntax: HTML' } },
{ name: { id: 2, foo: 'Set Syntax: JavaScript' } }
]
{ name: { id: 0, foo: "Set Syntax: CSS" } },
{ name: { id: 1, foo: "Set Syntax: HTML" } },
{ name: { id: 2, foo: "Set Syntax: JavaScript" } },
];
sources.sort(fuzzy('ssjs', { sourceAccessor: source => source.name.foo }))
sources.sort(fuzzy("ssjs", { iterator: (source) => source.name.foo }));
[
{ name: { id: 2, foo: 'Set Syntax: JavaScript' } },
{ name: { id: 0, foo: 'Set Syntax: CSS' } },
{ name: { id: 1, foo: 'Set Syntax: HTML' } }
]
// same, but will be faster thanks to memoization
sources.sort(fuzzy('ssjs', { sourceAccessor: source => source.name.foo, idAccessor: source => source.name.id }))
[
{ name: { id: 2, foo: 'Set Syntax: JavaScript' } },
{ name: { id: 0, foo: 'Set Syntax: CSS' } },
{ name: { id: 1, foo: 'Set Syntax: HTML' } }
]
{ name: { id: 2, foo: "Set Syntax: JavaScript" } },
{ name: { id: 0, foo: "Set Syntax: CSS" } },
{ name: { id: 1, foo: "Set Syntax: HTML" } },
];
```
```ts
const sort: (query: string, options?: SortOptions) => (leftSource: any, rightSource: any) => 0 | 1 | -1
function sort<TItem>(
query: string,
options?: SortOptions<TItem>
): (leftSource: TItem, rightSource: TItem) => 0 | 1 | -1;
type SortOptions = MatchOptions & {
sourceAccessor?: Accessor // used as an accessor if array is made of objects
idAccessor?: Accessor // used as an accessor if you want fuzzy to be memoized
}
type SortOptions<TItem> = {
caseSensitive?: boolean;
iterator: (item: TItem) => string;
};
```

@@ -186,13 +188,16 @@

A leading character is a character that matters more than others.
These are made of capitals and letters follwoing `-_ ./\`.
These are made of capitals and letters following `-_ ./\`.
```ts
const pushScore: (previousContext: ScoreContext, context: ScoreContext) => number
function pushScore(
previousContext: ScoreContext | undefined,
context: ScoreContext
): number;
type ScoreContext = {
currentScore: number // the current match score
character: string // the current character
match: boolean // is the character matching the source string
leading: boolean // is the character leading
}
currentScore: number; // the current match score
character: string; // the current character
match: boolean; // is the character matching the source string
leading: boolean; // is the character leading
};
```

@@ -204,2 +209,2 @@

fuzzyjs is licensed under MIT License.
fuzzyjs is licensed under MIT License.

@@ -1,105 +0,76 @@

import { test, TestOptions } from './test'
import { match, MatchOptions } from './match'
import { test, TestOptions } from "./test";
import { match } from "./match";
/**
* This represents an accessor as used with `sourceAccessor` and `idAccessor`.
*/
export type Accessor<T> = (source: T) => string
export type ItemIterator<TItem> = (source: TItem) => string;
/**
* This represents filter util options. It is based on [[TestOptions]] (as
* filter is using test) and extends it with `sourceAccessor` when you filter
* over an object array.
*/
export interface FilterOptions<T> extends TestOptions {
sourceAccessor?: Accessor<T>
export interface FilterOptions<TItem> extends TestOptions {
iterator: ItemIterator<TItem>;
}
/**
* This represents sort util options. It is based on [[MatchOptions]] (as sort
* is using match) and extends it with `sourceAccessor` when you filter over an
* object array and `idAccessor` when you want to optimize the sort with
* memoization.
*/
export interface SortOptions<T> extends MatchOptions {
sourceAccessor?: Accessor<T>
idAccessor?: Accessor<T>
export interface SortOptions<TItem> extends TestOptions {
iterator: ItemIterator<TItem>;
}
/**
* @ignore
*/
const get = <T>(source: T, accessor?: Accessor<T>): string => {
if (typeof accessor === 'function') return accessor(source)
if (typeof source === 'string') return source
type FilterIterator<TItem> = (item: TItem) => boolean;
type SortIterator<TItem> = (leftItem: TItem, rightItem: TItem) => number;
throw new TypeError(`Unexpected array of ${typeof source}. Use an accessor to return a string`)
}
/**
* This array helper can be used as an `Array.prototype.filter` callback as it
* will return true or false when passing it a source string. The only
* difference with `test` is that you can use it with an object, as long as you
* give a `sourceAccessor` in options.
*
* @param query The input query
* @param options The options as defined in [[FilterOptions]]
* @returns A function that you can pass into `Array.prototype.filter`
* will return true or false when passing it a source string.
*/
export const filter = <T>(query: string, options: FilterOptions<T> = {}) => (source: T) =>
test(query, get(source, options.sourceAccessor), options)
export function filter<TItem>(
query: string,
options: FilterOptions<TItem>
): FilterIterator<TItem> {
return function (item) {
const source = options.iterator(item);
return test(query, source, options);
};
}
/**
* This array helper can be used as an `Array.prototype.sort` callback as it
* will return `-1`/`0`/`1` when passing it two source strings. It is based on
* match with default options set (`withRanges` to false and `withScore` to
* true). You can also use `sourceAccessor` if you intend to sort over an array
* of objects and `idAccessor` to optimize the performances.
*
* @param query The input query
* @param options The options as defined in [[SortOptions]]
* @returns A function that you can pass into `Array.prototype.sort`
* will return `-1`/`0`/`1` when passing it two source strings.
*/
export const sort = <T>(query: string, options: SortOptions<T> = {}) => {
const matchOptions: MatchOptions = {
...options,
withRanges: false,
withScore: true
}
export function sort<TItem>(
query: string,
options: SortOptions<TItem>
): SortIterator<TItem> {
const cacheMap: Map<string, number> = new Map();
if (!options.idAccessor) {
return (leftSource: T, rightSource: T) => {
const leftScore = match(query, get(leftSource, options.sourceAccessor), matchOptions).score!
const rightScore = match(query, get(rightSource, options.sourceAccessor), matchOptions).score!
return (leftItem, rightItem) => {
const leftSource = options.iterator(leftItem);
const rightSource = options.iterator(rightItem);
if (rightScore === leftScore) return 0
return rightScore > leftScore ? 1 : -1
}
}
const cachedLeftMatch = cacheMap.get(leftSource);
const cachedRightMatch = cacheMap.get(rightSource);
const memo: { [key: string]: number } = {}
const leftScore = cachedLeftMatch
? cachedLeftMatch
: match(query, leftSource, {
withScore: true,
caseSensitive: options.caseSensitive,
}).score;
return (leftSource: any, rightSource: any) => {
const leftId = get(leftSource, options.idAccessor)
const rightId = get(rightSource, options.idAccessor)
const rightScore = cachedRightMatch
? cachedRightMatch
: match(query, rightSource, {
withScore: true,
caseSensitive: options.caseSensitive,
}).score;
const leftScore: number = memo.hasOwnProperty(leftId)
? memo[leftId]
: match(query, get(leftSource, options.sourceAccessor), matchOptions).score!
if (!cacheMap.has(leftSource)) {
cacheMap.set(leftSource, leftScore);
}
const rightScore: number = memo.hasOwnProperty(rightId)
? memo[rightId]
: match(query, get(rightSource, options.sourceAccessor), matchOptions).score!
if (!memo.hasOwnProperty(leftId)) {
memo[leftId] = leftScore
if (!cacheMap.has(rightSource)) {
cacheMap.set(rightSource, rightScore);
}
if (!memo.hasOwnProperty(rightId)) {
memo[rightId] = rightScore
if (rightScore === leftScore) {
return 0;
}
if (rightScore === leftScore) return 0
return rightScore > leftScore ? 1 : -1
}
return rightScore > leftScore ? 1 : -1;
};
}

@@ -1,4 +0,4 @@

export { test, TestOptions } from './test'
export { match, MatchOptions, MatchResult, MatchRange, ScoreContext, ScoreStrategy } from './match'
export { filter, sort, FilterOptions, SortOptions, Accessor } from './array'
export { surround, SurroundOptions } from './surround'
export { test, TestOptions } from "./test";
export { match, MatchRange, ScoreContext, ScoreStrategy } from "./match";
export { filter, sort, FilterOptions, SortOptions } from "./array";
export { surround, SurroundOptions } from "./surround";

@@ -1,31 +0,7 @@

import { TestOptions } from './test'
import { prepare } from './utils/prepare'
import { pushRange } from './utils/range'
import { pushScore } from './score/defaultStrategy'
import { isLeading } from './utils/isLeading'
import { reshapeInput } from "./utils/prepare";
import { pushRange } from "./utils/range";
import { pushScore } from "./score/defaultStrategy";
import { isLeading } from "./utils/isLeading";
/**
* This represents match options. It is based on [[TestOptions]] but allows
* scores and ranges to be returned as well.
*/
export interface MatchOptions extends TestOptions {
strategy?: ScoreStrategy
withRanges?: boolean
withScore?: boolean
}
/**
* This represents match result as returned by the match function. It will
* always contains a `match` boolean (which would be equivalent to `test`
* function). If you set `withRanges` to true, match will return a `ranges`
* array, and if you set `withScore` to true, match will return a `score`
* number.
*/
export interface MatchResult {
match: boolean
score?: number
ranges?: MatchRange[]
}
/**
* This represents a Range that you can get if you call match with `withRanges`

@@ -36,4 +12,4 @@ * set to true. It is composed of indexes of the source string that are matched

export interface MatchRange {
start: number
stop: number
start: number;
stop: number;
}

@@ -45,3 +21,3 @@

* - `currentScore` the actual score (ie. the result of the last `pushScore` call or 0)
* - `character` the actual source character. It must not be reshaped (ie. lowercased or normalized)
* - `character` the actual source character. It must not be reshaped (ie. lower-cased or normalized)
* - `match` wether or not the actual source character is matched by the query

@@ -51,6 +27,6 @@ * - `leading` wether or not the actual source character is a leading character (as returned by the `isLeading` function)

export interface ScoreContext {
currentScore: number
character: string
match: boolean
leading: boolean
currentScore: number;
character: string;
match: boolean;
leading: boolean;
}

@@ -66,31 +42,49 @@

*/
export type ScoreStrategy = (previousContext: ScoreContext | null, context: ScoreContext) => number
export type ScoreStrategy = (
previousContext: ScoreContext | null,
context: ScoreContext
) => number;
/**
* Returns wether or not the query fuzzy matches the source. Called without
* options would be strictly equivalent as `{ match: test(query, source) }` but
* less optimized. You should use `withScore` or `withRanges` options to get
* additional informations about the match.
*
* @param query The input query
* @param source The input source
* @param opts Options as defined by [[MatchOptions]]
* @returns An object as defined by [[MatchResult]]
* Returns wether or not the query fuzzy matches the source
*/
export const match = (
export function match(
query: string,
source: string
): { match: boolean; ranges: MatchRange[]; score: number };
export function match(
query: string,
source: string,
opts: MatchOptions = { withScore: true, strategy: pushScore }
): MatchResult => {
const [reshapedQuery, reshapedSource] = prepare(query, source, opts)
opts: { caseSensitive?: boolean }
): { match: boolean; ranges: MatchRange[]; score: number };
export function match(
query: string,
source: string,
opts: { withScore?: true; caseSensitive?: boolean }
): { match: boolean; ranges: MatchRange[]; score: number };
export function match(
query: string,
source: string,
opts: { withScore?: false; caseSensitive?: boolean }
): { match: boolean; ranges: MatchRange[] };
export function match(
query: string,
source: string,
opts: {
withScore?: boolean;
caseSensitive?: boolean;
} = { withScore: true }
): { match: boolean; score?: number; ranges: MatchRange[] } {
const [reshapedQuery, reshapedSource] = reshapeInput(query, source, opts);
const withScore = !(opts?.withScore === false);
// if no source, then only return true if query is also empty
if (!reshapedSource.length || !reshapedQuery.length) {
const result: MatchResult = { match: !query.length }
if (opts.withRanges)
result.ranges = !query.length ? [{ start: 0, stop: reshapedSource.length }] : []
if (opts.withScore) result.score = Number(!query.length)
return result
if (reshapedSource.length === 0 || reshapedQuery.length === 0) {
return {
match: query.length === 0,
ranges:
query.length === 0 ? [{ start: 0, stop: reshapedSource.length }] : [],
score: withScore ? (query.length === 0 ? 1 : 0) : undefined,
};
}

@@ -100,25 +94,20 @@

if (reshapedQuery.length > reshapedSource.length) {
const result: MatchResult = { match: false, score: 0 }
if (opts.withRanges) result.ranges = []
if (opts.withScore) result.score = 0
return result
return { match: false, ranges: [], score: withScore ? 0 : undefined };
}
let queryPos = 0
let sourcePos = 0
let score = 0
let lastContext: ScoreContext | null = null
let ranges: MatchRange[] = []
let queryPos = 0;
let sourcePos = 0;
let score = 0;
let lastContext: ScoreContext | undefined;
let ranges: MatchRange[] = [];
// loop on source string
while (sourcePos < source.length) {
const actualSourceCharacter = reshapedSource[sourcePos]
const queryCharacterWaitingForMatch = reshapedQuery[queryPos]
const match = actualSourceCharacter === queryCharacterWaitingForMatch
const actualSourceCharacter = reshapedSource[sourcePos];
const queryCharacterWaitingForMatch = reshapedQuery[queryPos];
const match = actualSourceCharacter === queryCharacterWaitingForMatch;
if (opts.withScore) {
if (withScore) {
// context does not use reshaped as uppercase changes score
const previousCharacter = sourcePos > 0 ? source[sourcePos - 1] : ''
const previousCharacter = sourcePos > 0 ? source[sourcePos - 1] : "";

@@ -129,8 +118,8 @@ const newContext: ScoreContext = {

match,
leading: isLeading(previousCharacter, source[sourcePos])
}
leading: isLeading(previousCharacter, source[sourcePos]),
};
score = pushScore(lastContext, newContext)
score = pushScore(lastContext, newContext);
lastContext = newContext
lastContext = newContext;
}

@@ -141,28 +130,24 @@

// push range to result
if (opts.withRanges) {
ranges = pushRange(ranges, sourcePos)
}
ranges = pushRange(ranges, sourcePos);
// move query pos
queryPos += 1
queryPos += 1;
}
sourcePos += 1
sourcePos += 1;
}
if (queryPos === reshapedQuery.length) {
const result: MatchResult = { match: true }
if (opts.withRanges) result.ranges = ranges
if (opts.withScore) result.score = score
return result
return {
match: true,
ranges,
score: withScore ? score : undefined,
};
}
const result: MatchResult = { match: false }
if (opts.withRanges) result.ranges = []
if (opts.withScore) result.score = 0
return result
return {
match: false,
ranges: [],
score: withScore ? 0 : undefined,
};
}

@@ -1,2 +0,2 @@

import { ScoreContext } from '../match'
import { ScoreContext } from "../match";

@@ -11,26 +11,29 @@ /**

*
* @param previousContext The last context given to pushScore
* @param previousContext The last context given to pushScore. undefined if first match
* @param context The actual context
* @returns The new score
*/
export const pushScore = (previousContext: ScoreContext | null, context: ScoreContext) => {
export function pushScore(
previousContext: ScoreContext | undefined,
context: ScoreContext
): number {
if (!context) {
throw new TypeError('Expecting context to be defined')
throw new TypeError("Expecting context to be defined");
}
if (!context.match) {
return context.currentScore - 1
return context.currentScore - 1;
}
let increment = 0
let increment = 0;
if (previousContext && previousContext.match) {
increment += 5
increment += 5;
}
if (context.leading) {
increment += 10
increment += 10;
}
return context.currentScore + increment
return context.currentScore + increment;
}

@@ -1,58 +0,46 @@

import { MatchRange } from './match'
import { MatchRange } from "./match";
/**
* This represents the options you can pass to surround.
* It requires at least `result` (that you'll get in `match` with the
* `withRanges` option set to true). You can give a prefix and a suffix, which
* will surround every part of the source string that matched the query.
*/
export interface SurroundOptions {
result: {
ranges: MatchRange[]
}
prefix?: string
suffix?: string
ranges: MatchRange[];
};
prefix?: string;
suffix?: string;
}
/**
* @ignore
*/
const insertAt = (input: string, index: number, patch: string = '') =>
input.slice(0, index) + patch + input.slice(index)
/**
* Surround parts of the string that matched with prefix and suffix.
* Useful to emphasize the parts that matched.
*
* @param source The input source
* @param options Options as defined by [[SurroundOptions]]
* @returns The input source with matching ranges surrounded by prefix and suffix
*/
export const surround = (source: string, options: SurroundOptions) => {
if (typeof source !== 'string') {
throw new TypeError('Expecting source to be a string')
export function surround(source: string, options: SurroundOptions): string {
if (typeof source !== "string") {
throw new TypeError("Expecting source to be a string");
}
if (!source.length) {
return ''
if (source.length === 0) {
return "";
}
if (!options || !options.result || !options.result.ranges || !options.result.ranges.length) {
return source
if (!options?.result?.ranges?.length) {
return source;
}
let result = source
let accumulator = 0
let result = source;
let accumulator = 0;
for (let range of options.result.ranges) {
result = insertAt(result, range.start + accumulator, options.prefix)
for (const range of options.result.ranges) {
result = insertAt(result, range.start + accumulator, options.prefix);
accumulator += (options.prefix || '').length
accumulator += (options.prefix ?? "").length;
result = insertAt(result, range.stop + accumulator, options.suffix)
result = insertAt(result, range.stop + accumulator, options.suffix);
accumulator += (options.suffix || '').length
accumulator += (options.suffix ?? "").length;
}
return result
return result;
}
function insertAt(input: string, index: number, patch = ""): string {
return input.slice(0, index) + patch + input.slice(index);
}

@@ -1,2 +0,2 @@

import { prepare } from './utils/prepare'
import { reshapeInput } from "./utils/prepare";

@@ -8,3 +8,3 @@ /**

export interface TestOptions {
caseSensitive?: boolean
caseSensitive?: boolean;
}

@@ -21,12 +21,16 @@

*/
export const test = (query: string, source: string, opts: TestOptions = {}) => {
const [reshapedQuery, reshapedSource] = prepare(query, source, opts)
export function test(
query: string,
source: string,
opts: TestOptions = {}
): boolean {
const [reshapedQuery, reshapedSource] = reshapeInput(query, source, opts);
// if no source, then only return true if query is also empty
if (!reshapedSource.length) {
return !query.length
return !query.length;
}
if (!reshapedQuery.length) {
return true
return true;
}

@@ -36,12 +40,12 @@

if (reshapedQuery.length > reshapedSource.length) {
return false
return false;
}
let queryPos = 0
let sourcePos = 0
let queryPos = 0;
let sourcePos = 0;
// loop on source string
while (sourcePos < source.length) {
const actualSourceCharacter = reshapedSource[sourcePos]
const queryCharacterWaitingForMatch = reshapedQuery[queryPos]
const actualSourceCharacter = reshapedSource[sourcePos];
const queryCharacterWaitingForMatch = reshapedQuery[queryPos];

@@ -51,9 +55,9 @@ // if actual query character matches source character

// move query pos
queryPos += 1
queryPos += 1;
}
sourcePos += 1
sourcePos += 1;
}
return queryPos === reshapedQuery.length
return queryPos === reshapedQuery.length;
}

@@ -1,2 +0,2 @@

import { toLatin } from './toLatin'
import { toLatin } from "./toLatin";

@@ -13,14 +13,14 @@ /**

*/
export const isLeading = (prevChar: string, char: string) => {
export function isLeading(prevChar: string, char: string): boolean {
const precededBySeparator =
prevChar === '-' ||
prevChar === '_' ||
prevChar === ' ' ||
prevChar === '.' ||
prevChar === '/' ||
prevChar === '\\'
prevChar === "-" ||
prevChar === "_" ||
prevChar === " " ||
prevChar === "." ||
prevChar === "/" ||
prevChar === "\\";
const isCharLeading = char.toUpperCase() === char && /\w/.test(toLatin(char))
const isCharLeading = char.toUpperCase() === char && /\w/.test(toLatin(char));
return precededBySeparator || isCharLeading
return precededBySeparator || isCharLeading;
}

@@ -1,2 +0,2 @@

import { TestOptions } from '../test'
import { TestOptions } from "../test";

@@ -12,20 +12,24 @@ /**

*/
export const prepare = (query: string, source: string, opts: TestOptions) => {
if (typeof query !== 'string') {
throw new TypeError('Expecting query to be a string')
export function reshapeInput(
query: string,
source: string,
opts: TestOptions
): [string, string] {
if (typeof query !== "string") {
throw new TypeError("Expecting query to be a string");
}
if (typeof source !== 'string') {
throw new TypeError('Expecting source to be a string')
if (typeof source !== "string") {
throw new TypeError("Expecting source to be a string");
}
let reshapedQuery = query.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
let reshapedSource = source.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
let reshapedQuery = query.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
let reshapedSource = source.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
if (!opts.caseSensitive) {
reshapedQuery = reshapedQuery.toLowerCase()
reshapedSource = reshapedSource.toLowerCase()
reshapedQuery = reshapedQuery.toLowerCase();
reshapedSource = reshapedSource.toLowerCase();
}
return [reshapedQuery, reshapedSource]
return [reshapedQuery, reshapedSource];
}

@@ -1,2 +0,2 @@

import { MatchRange } from '../match'
import { MatchRange } from "../match";

@@ -12,4 +12,7 @@ /**

*/
export const pushRange = (ranges: MatchRange[], sourcePos: number): MatchRange[] => {
const lastRange = ranges[ranges.length - 1]
export function pushRange(
ranges: MatchRange[],
sourcePos: number
): MatchRange[] {
const lastRange = ranges[ranges.length - 1];

@@ -21,8 +24,8 @@ if (lastRange && lastRange.stop === sourcePos) {

start: lastRange.start,
stop: sourcePos + 1
}
]
stop: sourcePos + 1,
},
];
} else {
return [...ranges, { start: sourcePos, stop: sourcePos + 1 }]
return [...ranges, { start: sourcePos, stop: sourcePos + 1 }];
}
}

@@ -9,2 +9,4 @@ /**

*/
export const toLatin = (str: string) => str.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
export function toLatin(str: string): string {
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
{
"include": ["./src/**/*.ts"],
"exclude": ["./src/**/__tests__"],
"compilerOptions": {
"moduleResolution": "node",
"target": "es5",
"module":"es2015",
"lib": ["es2015", "es2016", "es2017", "dom"],
"lib": ["es2020"],
"module": "commonjs",
"target": "es2020",
"rootDir": "./src",
"outDir": "build",
"strict": true,
"sourceMap": true,
"declaration": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"declarationDir": "dist/types",
"outDir": "dist/lib",
"esModuleInterop": true,
"typeRoots": [
"node_modules/@types"
]
},
"include": [
"src"
]
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true
}
}
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc