Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoInstallSign in
Socket

@nexucis/fuzzy

Package Overview
Dependencies
Maintainers
1
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@nexucis/fuzzy - npm Package Compare versions

Comparing version
0.4.1
to
0.5.0
+1
.nvmrc
v18.16.0
"use strict";
// MIT License
//
// Copyright (c) 2020 Augustin Husson
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
Object.defineProperty(exports, "__esModule", { value: true });
exports.Fuzzy = void 0;
exports.score = score;
exports.filter = filter;
exports.match = match;
exports.render = render;
function escapeHTML(text) {
return text.replace(/[&<>"']/g, (m) => {
switch (m) {
case '&':
return '&amp;';
case '<':
return '&lt;';
case '>':
return '&gt;';
case '"':
return '&quot;';
default:
return '&#039;';
}
});
}
function intervalSize(interval) {
return interval.to - interval.from + 1;
}
function calculatePreviousNotMatchingInterval(intervals, idx) {
const currentInterval = intervals[idx];
let previousNotMatchingInterval = null;
if (idx === 0 && currentInterval.from !== 0) {
previousNotMatchingInterval = { from: 0, to: currentInterval.from - 1 };
}
if (idx > 0) {
previousNotMatchingInterval = { from: intervals[idx - 1].to + 1, to: currentInterval.from - 1 };
}
return previousNotMatchingInterval;
}
// score should be used to calculate the score based on the intervals created during the matching step.
// Here is how the score is determinate:
// 1. Consecutive characters should increase the score more than linearly
// 2. More there is a distance between the characters, higher it reduces the score.
// For example, for the pattern 'abc', the following strings are sorted by the highest score
// abcdef > defabc > abec > defabec
// Note: this function is exported only for testing purpose.
function score(intervals, strLength) {
let result = 0;
for (let i = 0; i < intervals.length; i++) {
const currentInterval = intervals[i];
const previousNotMatchingInterval = calculatePreviousNotMatchingInterval(intervals, i);
if (previousNotMatchingInterval !== null) {
result = result - intervalSize(previousNotMatchingInterval) / strLength;
}
result = result + intervalSize(currentInterval) ** 2;
}
return result;
}
class Fuzzy {
constructor(conf) {
this.conf = {
caseSensitive: (conf === null || conf === void 0 ? void 0 : conf.caseSensitive) === undefined ? false : conf.caseSensitive,
excludedChars: (conf === null || conf === void 0 ? void 0 : conf.excludedChars) === undefined ? [] : conf.excludedChars,
includeMatches: (conf === null || conf === void 0 ? void 0 : conf.includeMatches) === undefined ? false : conf.includeMatches,
shouldSort: (conf === null || conf === void 0 ? void 0 : conf.shouldSort) === undefined ? false : conf.shouldSort,
shouldRender: (conf === null || conf === void 0 ? void 0 : conf.shouldRender) === undefined ? true : conf.shouldRender,
escapeHTML: (conf === null || conf === void 0 ? void 0 : conf.escapeHTML) === undefined ? false : conf.escapeHTML,
pre: (conf === null || conf === void 0 ? void 0 : conf.pre) === undefined ? '' : conf.pre,
post: (conf === null || conf === void 0 ? void 0 : conf.post) === undefined ? '' : conf.post,
};
}
// filter is the method to use to filter a string list
// list of result return can be sort if parameter `shouldSort` is set.
filter(pattern, list, conf) {
const shouldSort = (conf === null || conf === void 0 ? void 0 : conf.shouldSort) !== undefined ? conf.shouldSort : this.conf.shouldSort;
let result = [];
for (let i = 0; i < list.length; i++) {
const matchedText = this.match(pattern, list[i], conf);
if (matchedText !== null) {
matchedText.index = i;
result.push(matchedText);
}
}
if (shouldSort) {
result = result.sort((a, b) => {
return b.score - a.score;
});
}
return result;
}
// match will return a result if `pattern` is matching `text`,
match(pattern, text, conf) {
let localPattern = pattern;
let localText = text;
const caseSensitive = (conf === null || conf === void 0 ? void 0 : conf.caseSensitive) !== undefined ? conf.caseSensitive : this.conf.caseSensitive;
const includeMatches = (conf === null || conf === void 0 ? void 0 : conf.includeMatches) !== undefined ? conf.includeMatches : this.conf.includeMatches;
const shouldRender = (conf === null || conf === void 0 ? void 0 : conf.shouldRender) !== undefined ? conf.shouldRender : this.conf.shouldRender;
if (!caseSensitive) {
localPattern = localPattern.toLowerCase();
localText = localText.toLowerCase();
}
// in case it's a perfect match, no need to loop to find which char is matching
if (localPattern === localText) {
const intervals = [{ from: 0, to: pattern.length - 1 }];
const result = {
original: text,
rendered: shouldRender ? this.render(text, intervals, conf) : text,
score: Infinity,
};
if (includeMatches) {
result.intervals = intervals;
}
return result;
}
// otherwise, let's calculate the different indices that will then be used to calculate the score
let intervals = [];
let score = 0;
for (let i = 0; i < localText.length - localPattern.length + 1; i++) {
// Each time a char is matching the first char of the pattern
// loop other the rest of the text to generate the different matching interval.
// Like that, we will be able to find the best matching possibility.
// For example, given the pattern `bac` and the text `babac`
// instead of matching `<ba>ba<c>, it will match ba<bac> which has a better score than the previous one.
if (localText[i] === localPattern[0]) {
const matchingResult = this.generateMatchingInterval(localPattern, localText, i, conf);
if (matchingResult === null) {
break;
}
if (matchingResult.score > score) {
score = matchingResult.score;
intervals = matchingResult.intervals;
}
}
}
if (intervals.length === 0) {
return null;
}
const result = {
original: text,
rendered: shouldRender ? this.render(text, intervals, conf) : text,
score: score,
};
if (includeMatches) {
result.intervals = intervals;
}
return result;
}
// render will modify the text according to the different parameter set in the conf.
// If nothing is set, then it will return the text not modified.
render(text, intervals, conf) {
if (intervals.length == 0) {
return text;
}
let rendered = '';
const pre = (conf === null || conf === void 0 ? void 0 : conf.pre) ? conf.pre : this.conf.pre;
const post = (conf === null || conf === void 0 ? void 0 : conf.post) ? conf.post : this.conf.post;
for (let i = 0; i < intervals.length; i++) {
const currentInterval = intervals[i];
const previousNotMatchingInterval = calculatePreviousNotMatchingInterval(intervals, i);
let previousStr = '';
if (previousNotMatchingInterval !== null) {
previousStr = this.extractSubString(text, previousNotMatchingInterval, conf);
}
const currentStr = this.extractSubString(text, currentInterval, conf);
rendered = `${rendered}${previousStr}${pre}${currentStr}${post}`;
}
// check if the last interval contains the end of the string. Otherwise, add it
const lastInterval = intervals[intervals.length - 1];
if (lastInterval.to < text.length - 1) {
rendered = rendered + this.extractSubString(text, { from: lastInterval.to + 1, to: text.length }, conf);
}
return rendered;
}
extractSubString(text, interval, conf) {
const shouldEscape = (conf === null || conf === void 0 ? void 0 : conf.escapeHTML) !== undefined ? conf.escapeHTML : this.conf.escapeHTML;
let str = text.substr(interval.from, intervalSize(interval));
if (shouldEscape) {
str = escapeHTML(str);
}
return str;
}
// generateMatchingInterval will iterate other the given text to find the different char that matched the given pattern
generateMatchingInterval(pattern, text, idxText, conf) {
let excludedChars = [];
if ((conf === null || conf === void 0 ? void 0 : conf.excludedChars) !== undefined) {
excludedChars = conf.excludedChars;
}
else if (this.conf.excludedChars !== undefined) {
excludedChars = this.conf.excludedChars;
}
let patternIdx = 0;
const intervals = [];
for (let i = idxText; i < text.length && patternIdx < pattern.length;) {
if (excludedChars.includes(text[i])) {
i++;
continue;
}
if (excludedChars.includes(pattern[patternIdx])) {
patternIdx++;
continue;
}
if (text[i] === pattern[patternIdx]) {
const interval = { from: i, to: i };
patternIdx++;
i++;
for (let j = i; j < text.length && patternIdx < pattern.length && text[j] === pattern[patternIdx]; j++) {
interval.to = j;
patternIdx++;
i = j;
}
intervals.push(interval);
}
i++;
}
if (intervals.length === 0 || patternIdx !== pattern.length) {
return null;
}
return { score: score(intervals, text.length), intervals: intervals };
}
}
exports.Fuzzy = Fuzzy;
const fuz = new Fuzzy();
function filter(pattern, list, conf) {
return fuz.filter(pattern, list, conf);
}
function match(pattern, text, conf) {
return fuz.match(pattern, text, conf);
}
function render(text, intervals, conf) {
return fuz.render(text, intervals, conf);
}
export declare function score(intervals: FuzzyMatchingInterval[], strLength: number): number;
export interface FuzzyConfiguration {
caseSensitive?: boolean;
excludedChars?: string[];
includeMatches?: boolean;
shouldSort?: boolean;
shouldRender?: boolean;
escapeHTML?: boolean;
pre?: string;
post?: string;
}
export interface FuzzyMatchingInterval {
from: number;
to: number;
}
export interface FuzzyResult {
rendered: string;
original: string;
index: number;
score: number;
intervals?: FuzzyMatchingInterval[];
}
export declare class Fuzzy {
private readonly conf;
constructor(conf?: FuzzyConfiguration);
filter(pattern: string, list: string[], conf?: FuzzyConfiguration): FuzzyResult[];
match(pattern: string, text: string, conf?: FuzzyConfiguration): FuzzyResult | null;
render(text: string, intervals: FuzzyMatchingInterval[], conf?: FuzzyConfiguration): string;
private extractSubString;
private generateMatchingInterval;
}
export declare function filter(pattern: string, list: string[], conf?: FuzzyConfiguration): FuzzyResult[];
export declare function match(pattern: string, text: string, conf?: FuzzyConfiguration): FuzzyResult | null;
export declare function render(text: string, intervals: FuzzyMatchingInterval[], conf?: FuzzyConfiguration): string;
// MIT License
//
// Copyright (c) 2020 Augustin Husson
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
function escapeHTML(text) {
return text.replace(/[&<>"']/g, (m) => {
switch (m) {
case '&':
return '&amp;';
case '<':
return '&lt;';
case '>':
return '&gt;';
case '"':
return '&quot;';
default:
return '&#039;';
}
});
}
function intervalSize(interval) {
return interval.to - interval.from + 1;
}
function calculatePreviousNotMatchingInterval(intervals, idx) {
const currentInterval = intervals[idx];
let previousNotMatchingInterval = null;
if (idx === 0 && currentInterval.from !== 0) {
previousNotMatchingInterval = { from: 0, to: currentInterval.from - 1 };
}
if (idx > 0) {
previousNotMatchingInterval = { from: intervals[idx - 1].to + 1, to: currentInterval.from - 1 };
}
return previousNotMatchingInterval;
}
// score should be used to calculate the score based on the intervals created during the matching step.
// Here is how the score is determinate:
// 1. Consecutive characters should increase the score more than linearly
// 2. More there is a distance between the characters, higher it reduces the score.
// For example, for the pattern 'abc', the following strings are sorted by the highest score
// abcdef > defabc > abec > defabec
// Note: this function is exported only for testing purpose.
export function score(intervals, strLength) {
let result = 0;
for (let i = 0; i < intervals.length; i++) {
const currentInterval = intervals[i];
const previousNotMatchingInterval = calculatePreviousNotMatchingInterval(intervals, i);
if (previousNotMatchingInterval !== null) {
result = result - intervalSize(previousNotMatchingInterval) / strLength;
}
result = result + intervalSize(currentInterval) ** 2;
}
return result;
}
export class Fuzzy {
constructor(conf) {
this.conf = {
caseSensitive: (conf === null || conf === void 0 ? void 0 : conf.caseSensitive) === undefined ? false : conf.caseSensitive,
excludedChars: (conf === null || conf === void 0 ? void 0 : conf.excludedChars) === undefined ? [] : conf.excludedChars,
includeMatches: (conf === null || conf === void 0 ? void 0 : conf.includeMatches) === undefined ? false : conf.includeMatches,
shouldSort: (conf === null || conf === void 0 ? void 0 : conf.shouldSort) === undefined ? false : conf.shouldSort,
shouldRender: (conf === null || conf === void 0 ? void 0 : conf.shouldRender) === undefined ? true : conf.shouldRender,
escapeHTML: (conf === null || conf === void 0 ? void 0 : conf.escapeHTML) === undefined ? false : conf.escapeHTML,
pre: (conf === null || conf === void 0 ? void 0 : conf.pre) === undefined ? '' : conf.pre,
post: (conf === null || conf === void 0 ? void 0 : conf.post) === undefined ? '' : conf.post,
};
}
// filter is the method to use to filter a string list
// list of result return can be sort if parameter `shouldSort` is set.
filter(pattern, list, conf) {
const shouldSort = (conf === null || conf === void 0 ? void 0 : conf.shouldSort) !== undefined ? conf.shouldSort : this.conf.shouldSort;
let result = [];
for (let i = 0; i < list.length; i++) {
const matchedText = this.match(pattern, list[i], conf);
if (matchedText !== null) {
matchedText.index = i;
result.push(matchedText);
}
}
if (shouldSort) {
result = result.sort((a, b) => {
return b.score - a.score;
});
}
return result;
}
// match will return a result if `pattern` is matching `text`,
match(pattern, text, conf) {
let localPattern = pattern;
let localText = text;
const caseSensitive = (conf === null || conf === void 0 ? void 0 : conf.caseSensitive) !== undefined ? conf.caseSensitive : this.conf.caseSensitive;
const includeMatches = (conf === null || conf === void 0 ? void 0 : conf.includeMatches) !== undefined ? conf.includeMatches : this.conf.includeMatches;
const shouldRender = (conf === null || conf === void 0 ? void 0 : conf.shouldRender) !== undefined ? conf.shouldRender : this.conf.shouldRender;
if (!caseSensitive) {
localPattern = localPattern.toLowerCase();
localText = localText.toLowerCase();
}
// in case it's a perfect match, no need to loop to find which char is matching
if (localPattern === localText) {
const intervals = [{ from: 0, to: pattern.length - 1 }];
const result = {
original: text,
rendered: shouldRender ? this.render(text, intervals, conf) : text,
score: Infinity,
};
if (includeMatches) {
result.intervals = intervals;
}
return result;
}
// otherwise, let's calculate the different indices that will then be used to calculate the score
let intervals = [];
let score = 0;
for (let i = 0; i < localText.length - localPattern.length + 1; i++) {
// Each time a char is matching the first char of the pattern
// loop other the rest of the text to generate the different matching interval.
// Like that, we will be able to find the best matching possibility.
// For example, given the pattern `bac` and the text `babac`
// instead of matching `<ba>ba<c>, it will match ba<bac> which has a better score than the previous one.
if (localText[i] === localPattern[0]) {
const matchingResult = this.generateMatchingInterval(localPattern, localText, i, conf);
if (matchingResult === null) {
break;
}
if (matchingResult.score > score) {
score = matchingResult.score;
intervals = matchingResult.intervals;
}
}
}
if (intervals.length === 0) {
return null;
}
const result = {
original: text,
rendered: shouldRender ? this.render(text, intervals, conf) : text,
score: score,
};
if (includeMatches) {
result.intervals = intervals;
}
return result;
}
// render will modify the text according to the different parameter set in the conf.
// If nothing is set, then it will return the text not modified.
render(text, intervals, conf) {
if (intervals.length == 0) {
return text;
}
let rendered = '';
const pre = (conf === null || conf === void 0 ? void 0 : conf.pre) ? conf.pre : this.conf.pre;
const post = (conf === null || conf === void 0 ? void 0 : conf.post) ? conf.post : this.conf.post;
for (let i = 0; i < intervals.length; i++) {
const currentInterval = intervals[i];
const previousNotMatchingInterval = calculatePreviousNotMatchingInterval(intervals, i);
let previousStr = '';
if (previousNotMatchingInterval !== null) {
previousStr = this.extractSubString(text, previousNotMatchingInterval, conf);
}
const currentStr = this.extractSubString(text, currentInterval, conf);
rendered = `${rendered}${previousStr}${pre}${currentStr}${post}`;
}
// check if the last interval contains the end of the string. Otherwise, add it
const lastInterval = intervals[intervals.length - 1];
if (lastInterval.to < text.length - 1) {
rendered = rendered + this.extractSubString(text, { from: lastInterval.to + 1, to: text.length }, conf);
}
return rendered;
}
extractSubString(text, interval, conf) {
const shouldEscape = (conf === null || conf === void 0 ? void 0 : conf.escapeHTML) !== undefined ? conf.escapeHTML : this.conf.escapeHTML;
let str = text.substr(interval.from, intervalSize(interval));
if (shouldEscape) {
str = escapeHTML(str);
}
return str;
}
// generateMatchingInterval will iterate other the given text to find the different char that matched the given pattern
generateMatchingInterval(pattern, text, idxText, conf) {
let excludedChars = [];
if ((conf === null || conf === void 0 ? void 0 : conf.excludedChars) !== undefined) {
excludedChars = conf.excludedChars;
}
else if (this.conf.excludedChars !== undefined) {
excludedChars = this.conf.excludedChars;
}
let patternIdx = 0;
const intervals = [];
for (let i = idxText; i < text.length && patternIdx < pattern.length;) {
if (excludedChars.includes(text[i])) {
i++;
continue;
}
if (excludedChars.includes(pattern[patternIdx])) {
patternIdx++;
continue;
}
if (text[i] === pattern[patternIdx]) {
const interval = { from: i, to: i };
patternIdx++;
i++;
for (let j = i; j < text.length && patternIdx < pattern.length && text[j] === pattern[patternIdx]; j++) {
interval.to = j;
patternIdx++;
i = j;
}
intervals.push(interval);
}
i++;
}
if (intervals.length === 0 || patternIdx !== pattern.length) {
return null;
}
return { score: score(intervals, text.length), intervals: intervals };
}
}
const fuz = new Fuzzy();
export function filter(pattern, list, conf) {
return fuz.filter(pattern, list, conf);
}
export function match(pattern, text, conf) {
return fuz.match(pattern, text, conf);
}
export function render(text, intervals, conf) {
return fuz.render(text, intervals, conf);
}
+13
-13
{
"name": "@nexucis/fuzzy",
"version": "0.4.1",
"version": "0.5.0",
"description": "small, standalone fuzzy search / fuzzy filter. browser or node",

@@ -33,17 +33,17 @@ "module": "dist/index.js",

"devDependencies": {
"@types/chai": "^4.3.1",
"@types/mocha": "^9.1.1",
"@typescript-eslint/eslint-plugin": "^5.25.0",
"@typescript-eslint/parser": "^5.25.0",
"chai": "^4.3.6",
"@types/chai": "^4.3.16",
"@types/mocha": "^10.0.7",
"@typescript-eslint/eslint-plugin": "^7.16.1",
"@typescript-eslint/parser": "^7.16.1",
"chai": "^4.4.1",
"codecov": "^3.8.3",
"eslint": "^8.15.0",
"eslint": "^8.57.0",
"eslint-plugin-flowtype": "^8.0.3",
"eslint-plugin-import": "^2.26.0",
"mocha": "^9.2.2",
"nyc": "^15.1.0",
"ts-mocha": "^9.0.2",
"ts-node": "^10.7.0",
"typescript": "^4.6.4"
"eslint-plugin-import": "^2.29.1",
"mocha": "^10.6.0",
"nyc": "^17.0.0",
"ts-mocha": "^10.0.0",
"ts-node": "^10.9.2",
"typescript": "^5.5.3"
}
}

@@ -88,2 +88,9 @@ Fuzzy

### excludedChars
* **Type**: `array of string`
* **Default**: `[]`
List of characters that should be ignored in the pattern or in the word used for matching
### includeMatches

@@ -104,2 +111,12 @@

### shouldRender
* **Type**: `boolean`
* **Default**: `true`
If true, then the strings matched will be automatically rendered using the config pre/post and escapeHTML.
In case you want to render it yourself, set it to false, and set `includeMatches` to true.
You will need the intervals to call the method render.
### escapeHTML

@@ -106,0 +123,0 @@

"use strict";
// MIT License
//
// Copyright (c) 2020 Augustin Husson
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
Object.defineProperty(exports, "__esModule", { value: true });
exports.render = exports.match = exports.filter = exports.Fuzzy = exports.score = void 0;
function escapeHTML(text) {
return text.replace(/[&<>"']/g, (m) => {
switch (m) {
case '&':
return '&amp;';
case '<':
return '&lt;';
case '>':
return '&gt;';
case '"':
return '&quot;';
default:
return '&#039;';
}
});
}
function intervalSize(interval) {
return interval.to - interval.from + 1;
}
function calculatePreviousNotMatchingInterval(intervals, idx) {
const currentInterval = intervals[idx];
let previousNotMatchingInterval = null;
if (idx === 0 && currentInterval.from !== 0) {
previousNotMatchingInterval = { from: 0, to: currentInterval.from - 1 };
}
if (idx > 0) {
previousNotMatchingInterval = { from: intervals[idx - 1].to + 1, to: currentInterval.from - 1 };
}
return previousNotMatchingInterval;
}
// score should be used to calculate the score based on the intervals created during the matching step.
// Here is how the score is determinated:
// 1. Consecutive characters should increase the score more than linearly
// 2. More there is a distance between the characters, higher it reduces the score
// For example, for the pattern 'abc', the following string are sorted by the highest score
// abcdef > defabc > abec > defabec
// Note: this function is exported only for testing purpose.
function score(intervals, strLength) {
let result = 0;
for (let i = 0; i < intervals.length; i++) {
const currentInterval = intervals[i];
const previousNotMatchingInterval = calculatePreviousNotMatchingInterval(intervals, i);
if (previousNotMatchingInterval !== null) {
result = result - intervalSize(previousNotMatchingInterval) / strLength;
}
result = result + intervalSize(currentInterval) ** 2;
}
return result;
}
exports.score = score;
// generateMatchingInterval will iterate other the given text to find the different char that matched the given pattern
function generateMatchingInterval(pattern, text, idxText) {
let patternIdx = 0;
const intervals = [];
for (let i = idxText; i < text.length && patternIdx < pattern.length;) {
if (text[i] === pattern[patternIdx]) {
const interval = { from: i, to: i };
patternIdx++;
i++;
for (let j = i; j < text.length && patternIdx < pattern.length && text[j] === pattern[patternIdx]; j++) {
interval.to = j;
patternIdx++;
i = j;
}
intervals.push(interval);
}
i++;
}
if (intervals.length === 0 || patternIdx !== pattern.length) {
return null;
}
return { score: score(intervals, text.length), intervals: intervals };
}
class Fuzzy {
constructor(conf) {
this.conf = {
caseSensitive: (conf === null || conf === void 0 ? void 0 : conf.caseSensitive) === undefined ? false : conf.caseSensitive,
includeMatches: (conf === null || conf === void 0 ? void 0 : conf.includeMatches) === undefined ? false : conf.includeMatches,
shouldSort: (conf === null || conf === void 0 ? void 0 : conf.shouldSort) === undefined ? false : conf.shouldSort,
escapeHTML: (conf === null || conf === void 0 ? void 0 : conf.escapeHTML) === undefined ? false : conf.escapeHTML,
pre: (conf === null || conf === void 0 ? void 0 : conf.pre) === undefined ? '' : conf.pre,
post: (conf === null || conf === void 0 ? void 0 : conf.post) === undefined ? '' : conf.post,
};
}
// filter is the method to use to filter a string list
// list of result return can be sort if parameter `shouldSort` is set.
filter(pattern, list, conf) {
const shouldSort = (conf === null || conf === void 0 ? void 0 : conf.shouldSort) !== undefined ? conf.shouldSort : this.conf.shouldSort;
let result = [];
for (let i = 0; i < list.length; i++) {
const matchedText = this.match(pattern, list[i], conf);
if (matchedText !== null) {
matchedText.index = i;
result.push(matchedText);
}
}
if (shouldSort) {
result = result.sort((a, b) => {
return b.score - a.score;
});
}
return result;
}
// match will return a result if `pattern` is matching `text`,
match(pattern, text, conf) {
let localPattern = pattern;
let localText = text;
const caseSensitive = (conf === null || conf === void 0 ? void 0 : conf.caseSensitive) !== undefined ? conf.caseSensitive : this.conf.caseSensitive;
const includeMatches = (conf === null || conf === void 0 ? void 0 : conf.includeMatches) !== undefined ? conf.includeMatches : this.conf.includeMatches;
if (!caseSensitive) {
localPattern = localPattern.toLowerCase();
localText = localText.toLowerCase();
}
// in case it's a perfect match, no need to loop to find which char is matching
if (localPattern === localText) {
const intervals = [{ from: 0, to: pattern.length - 1 }];
const result = {
original: text,
rendered: this.render(text, intervals, conf),
score: Infinity,
};
if (includeMatches) {
result.intervals = intervals;
}
return result;
}
// otherwise let's calculate the different indices that will then be used to calculate the score
let intervals = [];
let score = 0;
for (let i = 0; i < localText.length - localPattern.length + 1; i++) {
// Each time a char is matching the first char of the pattern
// loop other the rest of the text to generate the different matching interval.
// Like that we will be able to find the best matching possibility.
// For example: given the pattern `bac` and the text `babac`
// instead of matching `<ba>ba<c>, it will match ba<bac> which has a better score than the previous one.
if (localText[i] === localPattern[0]) {
const matchingResult = generateMatchingInterval(localPattern, localText, i);
if (matchingResult === null) {
break;
}
if (matchingResult.score > score) {
score = matchingResult.score;
intervals = matchingResult.intervals;
}
}
}
if (intervals.length === 0) {
return null;
}
const result = {
original: text,
rendered: this.render(text, intervals, conf),
score: score,
};
if (includeMatches) {
result.intervals = intervals;
}
return result;
}
// render will modify the text according to the different parameter set in the conf.
// If nothing is set, then it will return the text not modified.
render(text, intervals, conf) {
if (intervals.length == 0) {
return text;
}
let rendered = '';
const pre = (conf === null || conf === void 0 ? void 0 : conf.pre) ? conf.pre : this.conf.pre;
const post = (conf === null || conf === void 0 ? void 0 : conf.post) ? conf.post : this.conf.post;
for (let i = 0; i < intervals.length; i++) {
const currentInterval = intervals[i];
const previousNotMatchingInterval = calculatePreviousNotMatchingInterval(intervals, i);
let previousStr = '';
if (previousNotMatchingInterval !== null) {
previousStr = this.extractSubString(text, previousNotMatchingInterval, conf);
}
const currentStr = this.extractSubString(text, currentInterval, conf);
rendered = `${rendered}${previousStr}${pre}${currentStr}${post}`;
}
// check if the last interval contains the end of the string. Otherwise, add it
const lastInterval = intervals[intervals.length - 1];
if (lastInterval.to < text.length - 1) {
rendered = rendered + this.extractSubString(text, { from: lastInterval.to + 1, to: text.length }, conf);
}
return rendered;
}
extractSubString(text, interval, conf) {
const shouldEscape = (conf === null || conf === void 0 ? void 0 : conf.escapeHTML) !== undefined ? conf.escapeHTML : this.conf.escapeHTML;
let str = text.substr(interval.from, intervalSize(interval));
if (shouldEscape) {
str = escapeHTML(str);
}
return str;
}
}
exports.Fuzzy = Fuzzy;
const fuz = new Fuzzy();
function filter(pattern, list, conf) {
return fuz.filter(pattern, list, conf);
}
exports.filter = filter;
function match(pattern, text, conf) {
return fuz.match(pattern, text, conf);
}
exports.match = match;
function render(text, intervals, conf) {
return fuz.render(text, intervals, conf);
}
exports.render = render;
export declare function score(intervals: FuzzyMatchingInterval[], strLength: number): number;
export interface FuzzyConfiguration {
caseSensitive?: boolean;
includeMatches?: boolean;
shouldSort?: boolean;
escapeHTML?: boolean;
pre?: string;
post?: string;
}
export interface FuzzyMatchingInterval {
from: number;
to: number;
}
export interface FuzzyResult {
rendered: string;
original: string;
index: number;
score: number;
intervals?: FuzzyMatchingInterval[];
}
export declare class Fuzzy {
private readonly conf;
constructor(conf?: FuzzyConfiguration);
filter(pattern: string, list: string[], conf?: FuzzyConfiguration): FuzzyResult[];
match(pattern: string, text: string, conf?: FuzzyConfiguration): FuzzyResult | null;
render(text: string, intervals: FuzzyMatchingInterval[], conf?: FuzzyConfiguration): string;
private extractSubString;
}
export declare function filter(pattern: string, list: string[], conf?: FuzzyConfiguration): FuzzyResult[];
export declare function match(pattern: string, text: string, conf?: FuzzyConfiguration): FuzzyResult | null;
export declare function render(text: string, intervals: FuzzyMatchingInterval[], conf?: FuzzyConfiguration): string;
// MIT License
//
// Copyright (c) 2020 Augustin Husson
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
function escapeHTML(text) {
return text.replace(/[&<>"']/g, (m) => {
switch (m) {
case '&':
return '&amp;';
case '<':
return '&lt;';
case '>':
return '&gt;';
case '"':
return '&quot;';
default:
return '&#039;';
}
});
}
function intervalSize(interval) {
return interval.to - interval.from + 1;
}
function calculatePreviousNotMatchingInterval(intervals, idx) {
const currentInterval = intervals[idx];
let previousNotMatchingInterval = null;
if (idx === 0 && currentInterval.from !== 0) {
previousNotMatchingInterval = { from: 0, to: currentInterval.from - 1 };
}
if (idx > 0) {
previousNotMatchingInterval = { from: intervals[idx - 1].to + 1, to: currentInterval.from - 1 };
}
return previousNotMatchingInterval;
}
// score should be used to calculate the score based on the intervals created during the matching step.
// Here is how the score is determinated:
// 1. Consecutive characters should increase the score more than linearly
// 2. More there is a distance between the characters, higher it reduces the score
// For example, for the pattern 'abc', the following string are sorted by the highest score
// abcdef > defabc > abec > defabec
// Note: this function is exported only for testing purpose.
export function score(intervals, strLength) {
let result = 0;
for (let i = 0; i < intervals.length; i++) {
const currentInterval = intervals[i];
const previousNotMatchingInterval = calculatePreviousNotMatchingInterval(intervals, i);
if (previousNotMatchingInterval !== null) {
result = result - intervalSize(previousNotMatchingInterval) / strLength;
}
result = result + intervalSize(currentInterval) ** 2;
}
return result;
}
// generateMatchingInterval will iterate other the given text to find the different char that matched the given pattern
function generateMatchingInterval(pattern, text, idxText) {
let patternIdx = 0;
const intervals = [];
for (let i = idxText; i < text.length && patternIdx < pattern.length;) {
if (text[i] === pattern[patternIdx]) {
const interval = { from: i, to: i };
patternIdx++;
i++;
for (let j = i; j < text.length && patternIdx < pattern.length && text[j] === pattern[patternIdx]; j++) {
interval.to = j;
patternIdx++;
i = j;
}
intervals.push(interval);
}
i++;
}
if (intervals.length === 0 || patternIdx !== pattern.length) {
return null;
}
return { score: score(intervals, text.length), intervals: intervals };
}
export class Fuzzy {
constructor(conf) {
this.conf = {
caseSensitive: (conf === null || conf === void 0 ? void 0 : conf.caseSensitive) === undefined ? false : conf.caseSensitive,
includeMatches: (conf === null || conf === void 0 ? void 0 : conf.includeMatches) === undefined ? false : conf.includeMatches,
shouldSort: (conf === null || conf === void 0 ? void 0 : conf.shouldSort) === undefined ? false : conf.shouldSort,
escapeHTML: (conf === null || conf === void 0 ? void 0 : conf.escapeHTML) === undefined ? false : conf.escapeHTML,
pre: (conf === null || conf === void 0 ? void 0 : conf.pre) === undefined ? '' : conf.pre,
post: (conf === null || conf === void 0 ? void 0 : conf.post) === undefined ? '' : conf.post,
};
}
// filter is the method to use to filter a string list
// list of result return can be sort if parameter `shouldSort` is set.
filter(pattern, list, conf) {
const shouldSort = (conf === null || conf === void 0 ? void 0 : conf.shouldSort) !== undefined ? conf.shouldSort : this.conf.shouldSort;
let result = [];
for (let i = 0; i < list.length; i++) {
const matchedText = this.match(pattern, list[i], conf);
if (matchedText !== null) {
matchedText.index = i;
result.push(matchedText);
}
}
if (shouldSort) {
result = result.sort((a, b) => {
return b.score - a.score;
});
}
return result;
}
// match will return a result if `pattern` is matching `text`,
match(pattern, text, conf) {
let localPattern = pattern;
let localText = text;
const caseSensitive = (conf === null || conf === void 0 ? void 0 : conf.caseSensitive) !== undefined ? conf.caseSensitive : this.conf.caseSensitive;
const includeMatches = (conf === null || conf === void 0 ? void 0 : conf.includeMatches) !== undefined ? conf.includeMatches : this.conf.includeMatches;
if (!caseSensitive) {
localPattern = localPattern.toLowerCase();
localText = localText.toLowerCase();
}
// in case it's a perfect match, no need to loop to find which char is matching
if (localPattern === localText) {
const intervals = [{ from: 0, to: pattern.length - 1 }];
const result = {
original: text,
rendered: this.render(text, intervals, conf),
score: Infinity,
};
if (includeMatches) {
result.intervals = intervals;
}
return result;
}
// otherwise let's calculate the different indices that will then be used to calculate the score
let intervals = [];
let score = 0;
for (let i = 0; i < localText.length - localPattern.length + 1; i++) {
// Each time a char is matching the first char of the pattern
// loop other the rest of the text to generate the different matching interval.
// Like that we will be able to find the best matching possibility.
// For example: given the pattern `bac` and the text `babac`
// instead of matching `<ba>ba<c>, it will match ba<bac> which has a better score than the previous one.
if (localText[i] === localPattern[0]) {
const matchingResult = generateMatchingInterval(localPattern, localText, i);
if (matchingResult === null) {
break;
}
if (matchingResult.score > score) {
score = matchingResult.score;
intervals = matchingResult.intervals;
}
}
}
if (intervals.length === 0) {
return null;
}
const result = {
original: text,
rendered: this.render(text, intervals, conf),
score: score,
};
if (includeMatches) {
result.intervals = intervals;
}
return result;
}
// render will modify the text according to the different parameter set in the conf.
// If nothing is set, then it will return the text not modified.
render(text, intervals, conf) {
if (intervals.length == 0) {
return text;
}
let rendered = '';
const pre = (conf === null || conf === void 0 ? void 0 : conf.pre) ? conf.pre : this.conf.pre;
const post = (conf === null || conf === void 0 ? void 0 : conf.post) ? conf.post : this.conf.post;
for (let i = 0; i < intervals.length; i++) {
const currentInterval = intervals[i];
const previousNotMatchingInterval = calculatePreviousNotMatchingInterval(intervals, i);
let previousStr = '';
if (previousNotMatchingInterval !== null) {
previousStr = this.extractSubString(text, previousNotMatchingInterval, conf);
}
const currentStr = this.extractSubString(text, currentInterval, conf);
rendered = `${rendered}${previousStr}${pre}${currentStr}${post}`;
}
// check if the last interval contains the end of the string. Otherwise, add it
const lastInterval = intervals[intervals.length - 1];
if (lastInterval.to < text.length - 1) {
rendered = rendered + this.extractSubString(text, { from: lastInterval.to + 1, to: text.length }, conf);
}
return rendered;
}
extractSubString(text, interval, conf) {
const shouldEscape = (conf === null || conf === void 0 ? void 0 : conf.escapeHTML) !== undefined ? conf.escapeHTML : this.conf.escapeHTML;
let str = text.substr(interval.from, intervalSize(interval));
if (shouldEscape) {
str = escapeHTML(str);
}
return str;
}
}
const fuz = new Fuzzy();
export function filter(pattern, list, conf) {
return fuz.filter(pattern, list, conf);
}
export function match(pattern, text, conf) {
return fuz.match(pattern, text, conf);
}
export function render(text, intervals, conf) {
return fuz.render(text, intervals, conf);
}