-
-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
- Loading branch information
Showing
11 changed files
with
215 additions
and
165 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
112 changes: 112 additions & 0 deletions
112
packages/@cz-git/plugin-inquirer/__tests__/utils.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import { test, expect, describe } from "vitest"; | ||
import { fuzzyFilter, fuzzyMatch } from "../src"; | ||
|
||
/** | ||
* @description: fuzzyMatch Test | ||
*/ | ||
describe("fuzzyMatch", () => { | ||
test("function should be check param fit", () => { | ||
expect(fuzzyMatch(null, null)).toBe(null); | ||
expect(fuzzyMatch(undefined, null)).toBe(null); | ||
expect(fuzzyMatch(undefined, undefined)).toBe(null); | ||
// @ts-ignore | ||
expect(fuzzyMatch([], [])).toBe(null); | ||
// @ts-ignore | ||
expect(fuzzyMatch({}, {})).toBe(null); | ||
}); | ||
|
||
test("match char should be return right score", () => { | ||
expect(fuzzyMatch("a", "Apple")).toEqual(1); | ||
expect(fuzzyMatch("ae", "Apple")).toEqual(2); | ||
expect(fuzzyMatch("ap", "Apple")).toEqual(4); | ||
expect(fuzzyMatch("app", "Apple")).toEqual(11); | ||
expect(fuzzyMatch("ban", "banana")).toEqual(11); | ||
expect(fuzzyMatch("bna", "banana")).toEqual(5); | ||
expect(fuzzyMatch("baaa", "banana")).toEqual(6); | ||
}); | ||
|
||
test("consistent case should be return same score", () => { | ||
expect(fuzzyMatch("sz", "shenzhen")).toEqual(fuzzyMatch("sz", "ShenZhen")); | ||
}); | ||
|
||
test("case sensitive should be return diff score", () => { | ||
expect(fuzzyMatch("sz", "shenzhen", true)).toEqual(2); | ||
expect(fuzzyMatch("sz", "ShenZhen", true)).toEqual(null); | ||
}); | ||
|
||
test("not match char should be return null", () => { | ||
expect(fuzzyMatch("k", "banana")).toEqual(null); | ||
expect(fuzzyMatch("kkkkkk", "banana")).toEqual(null); | ||
expect(fuzzyMatch("bne", "banana")).toEqual(null); | ||
expect(fuzzyMatch("bnae", "banana")).toEqual(null); | ||
}); | ||
|
||
test("all match should be return Infinity", () => { | ||
expect(fuzzyMatch("apple", "Apple")).toEqual(Infinity); | ||
expect(fuzzyMatch("Apple", "Apple")).toEqual(Infinity); | ||
}); | ||
}); | ||
|
||
/** | ||
* @description: fuzzyFilter Test | ||
*/ | ||
describe("fuzzyFilter", () => { | ||
const testArr = [ | ||
{ name: "cz-git", value: "cz-git" }, | ||
{ name: "plugin-inquirer", value: "plugin-inquirer" }, | ||
{ name: "plugin-loader", value: "plugin-loader" }, | ||
{ type: "separator", line: "\x1B[2m──────────────\x1B[22m" }, | ||
{ name: "custom", value: "___CUSTOM__" }, | ||
{ name: "empty", value: false } | ||
]; | ||
|
||
test("function should be check param fit", () => { | ||
expect(fuzzyFilter("", [])).toEqual([]); | ||
expect(fuzzyFilter("", undefined)).toEqual([]); | ||
expect(fuzzyFilter(undefined, undefined)).toEqual([]); | ||
expect(fuzzyFilter("", null)).toEqual([]); | ||
}); | ||
|
||
test("empty input should be return origin array", () => { | ||
expect(fuzzyFilter("", testArr)).toBe(testArr); | ||
}); | ||
|
||
test("normal match should be return right array", () => { | ||
expect(fuzzyFilter("cz-git", testArr)).toEqual([ | ||
{ name: "cz-git", value: "cz-git", index: 0, score: Infinity } | ||
]); | ||
expect(fuzzyFilter("ty", testArr)).toEqual([ | ||
{ name: "empty", value: false, index: 5, score: 4 } | ||
]); | ||
expect(fuzzyFilter("inq", testArr)).toEqual([ | ||
{ name: "plugin-inquirer", value: "plugin-inquirer", index: 1, score: 5 } | ||
]); | ||
expect(fuzzyFilter("ii", testArr)).toEqual([ | ||
{ name: "plugin-inquirer", value: "plugin-inquirer", index: 1, score: 2 } | ||
]); | ||
}); | ||
|
||
test("same score shoule be return sort by index", () => { | ||
expect(fuzzyFilter("plu", testArr)).toEqual([ | ||
{ name: "plugin-inquirer", value: "plugin-inquirer", index: 1, score: 11 }, | ||
{ name: "plugin-loader", value: "plugin-loader", index: 2, score: 11 } | ||
]); | ||
}); | ||
|
||
test("diff score shoule be return sort by score", () => { | ||
const testArr = [ | ||
{ name: "anapple", value: "apple" }, | ||
{ name: "aapple", value: "apple" }, | ||
{ name: "apple", value: "apple" } | ||
]; | ||
expect(fuzzyFilter("ap", testArr)).toEqual([ | ||
{ name: "apple", value: "apple", index: 2, score: 4 }, | ||
{ name: "anapple", value: "apple", index: 0, score: 2 }, | ||
{ name: "aapple", value: "apple", index: 1, score: 2 } | ||
]); | ||
expect(fuzzyFilter("aap", testArr)).toEqual([ | ||
{ name: "aapple", value: "apple", index: 1, score: 11 }, | ||
{ name: "anapple", value: "apple", index: 0, score: 5 } | ||
]); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,5 +4,4 @@ | |
* @license: MIT | ||
*/ | ||
|
||
console.log("hello world"); | ||
export const hello = "hello world"; |
41 changes: 0 additions & 41 deletions
41
packages/@cz-git/plugin-inquirer/src/shared/types/fuzzy.ts
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,2 @@ | ||
export * from "./fuzzy"; | ||
export * from "./util"; | ||
export * from "./checkbox"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export interface FilterArrayItemType { | ||
value: string; | ||
name: string; | ||
emoji?: string; | ||
index?: number; | ||
score?: number; | ||
} |
155 changes: 58 additions & 97 deletions
155
packages/@cz-git/plugin-inquirer/src/shared/utils/fuzzy.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,127 +1,88 @@ | ||
/** | ||
* Powered by Fuzzy | ||
* https://github.com/myork/fuzzy | ||
* | ||
* Copyright (c) 2012 Matt York | ||
* Licensed under the MIT license. | ||
* | ||
* @description: A standalone fuzzy search / fuzzy filter. provide inquirer usage | ||
* @description: provide list and checkBox fuzzy search | ||
* @author: @Zhengqbbb ([email protected]) | ||
* @license: MIT | ||
*/ | ||
|
||
import { FilterFucType, MatchOptions, MatchResult } from "../types"; | ||
import type { FilterArrayItemType } from "../types"; | ||
|
||
/** | ||
* @description: If `pattern` matches `inputString`, wrap each matching character in `opts.pre` | ||
* and `opts.post`. If no match, return null. | ||
* @param {string} pattern inputString | ||
* @param {string} str targetString | ||
* @description: inputString match targetString return match score | ||
* @param {string} input input string | ||
* @param {string} target target string | ||
* @param {boolean} caseSensitive isCaseSensitive, default: false | ||
* @return {number | null} match score. if not match return null | ||
*/ | ||
export const fuzzyMatch = ( | ||
pattern: string, | ||
str: string, | ||
opts?: MatchOptions | ||
): MatchResult | null => { | ||
opts = opts || {}; | ||
const result = []; | ||
const len = str.length; | ||
// prefix | ||
const pre = opts.pre || ""; | ||
// suffix | ||
const post = opts.post || ""; | ||
// String to compare against. This might be a lowercase version of the | ||
// raw string | ||
const compareString = (opts.caseSensitive && str) || str.toLowerCase(); | ||
let patternIdx = 0, | ||
input: string, | ||
target: string, | ||
caseSensitive?: boolean | ||
): number | null => { | ||
if (typeof input !== "string" || typeof target !== "string") return null; | ||
const matchResult = []; | ||
const len = target.length; | ||
const shimTarget = (caseSensitive && target) || target.toLowerCase(); | ||
input = (caseSensitive && input) || input.toLowerCase(); | ||
let inputIndex = 0, | ||
totalScore = 0, | ||
currScore = 0, | ||
ch; | ||
|
||
pattern = (opts.caseSensitive && pattern) || pattern.toLowerCase(); | ||
|
||
// For each character in the string, either add it to the result | ||
// or wrap in template if it's the next string in the pattern | ||
currentScore = 0, | ||
currentChar; | ||
for (let idx = 0; idx < len; idx++) { | ||
ch = str[idx]; | ||
if (compareString[idx] === pattern[patternIdx]) { | ||
ch = pre + ch + post; | ||
patternIdx += 1; | ||
|
||
// consecutive characters should increase the score more than linearly | ||
currScore += 1 + currScore; | ||
currentChar = input[idx]; | ||
if (shimTarget[idx] === input[inputIndex]) { | ||
// consecutive matches will score higher | ||
inputIndex += 1; | ||
currentScore += 1 + currentScore; | ||
} else { | ||
currScore = 0; | ||
currentScore = 0; | ||
} | ||
totalScore += currScore; | ||
result[result.length] = ch; | ||
totalScore += currentScore; | ||
matchResult[matchResult.length] = currentChar; | ||
} | ||
|
||
// return rendered string if we have a match for every char | ||
if (patternIdx === pattern.length) { | ||
// if the string is an exact match with pattern, totalScore should be maxed | ||
totalScore = compareString === pattern ? Infinity : totalScore; | ||
return { rendered: result.join(""), score: totalScore }; | ||
if (inputIndex === input.length) { | ||
totalScore = shimTarget === input ? Infinity : totalScore; | ||
return totalScore; | ||
} | ||
|
||
return null; | ||
}; | ||
|
||
/** | ||
* @description: Does `pattern` fuzzy match `inputString`? | ||
* @param {string} pattern inputString | ||
* @param {string} str targetString | ||
* @return {boolean} isMatch | ||
*/ | ||
export const fuzzyTest = (pattern: string, str: string): boolean => { | ||
return fuzzyMatch(pattern, str) !== null; | ||
}; | ||
|
||
/** | ||
* @description: The normal entry point. Filters `arr` for matches against `pattern`. | ||
* @param {*} pattern inputString | ||
* @param {*} arr targetArray | ||
* @param {*} opts FilterOptions | ||
* @description: Array fuzzy filter | ||
* @param {string} input input string | ||
* @param {Array<FilterArrayItemType | unknown>} arr target Array | ||
* @return {Array<FilterArrayItemType>} filtered array | ||
*/ | ||
export const fuzzyFilter: FilterFucType<any> = (pattern, arr, opts?) => { | ||
if (!arr || arr.length === 0) { | ||
export const fuzzyFilter = ( | ||
input: string, | ||
arr: Array<FilterArrayItemType | unknown>, | ||
targetKey: "name" | "value" = "name" | ||
): Array<FilterArrayItemType> => { | ||
if (!arr || !Array.isArray(arr) || arr.length === 0) { | ||
return []; | ||
} | ||
if (typeof pattern !== "string") { | ||
} else if (typeof input !== "string" || input === "") { | ||
return arr; | ||
} | ||
opts = opts || {}; | ||
|
||
return arr | ||
.reduce(function (prev, element, idx) { | ||
let str = element; | ||
if (opts?.extract) { | ||
str = opts?.extract(element); | ||
.reduce((preVal: Array<FilterArrayItemType>, curItem: FilterArrayItemType, index) => { | ||
if (!curItem || !curItem[targetKey]) return preVal; | ||
const score = fuzzyMatch(input, curItem[targetKey]); | ||
if (score !== null) { | ||
preVal.push({ | ||
score, | ||
index, | ||
...curItem | ||
}); | ||
} | ||
const rendered = fuzzyMatch(pattern, str, opts); | ||
if (rendered != null) { | ||
prev[prev.length] = { | ||
string: rendered.rendered, | ||
score: rendered.score, | ||
index: idx, | ||
original: element | ||
}; | ||
} | ||
return prev; | ||
return preVal; | ||
}, []) | ||
.sort(function (a: any, b: any) { | ||
.sort((a: any, b: any) => { | ||
const compare = b.score - a.score; | ||
if (compare) return compare; | ||
return a.index - b.index; | ||
if (compare) { | ||
return compare; | ||
} else { | ||
return a.index - b.index; | ||
} | ||
}); | ||
}; | ||
|
||
/** | ||
* @description: Return all elements of `array` that have a fuzzy match against `pattern`. | ||
* @param {string} pattern inputString | ||
* @param {Array<string>} array targetArray | ||
*/ | ||
export const fuzzySimpleFilter = (pattern: string, array: string[]): string[] => { | ||
return array.filter(function (str) { | ||
return fuzzyTest(pattern, str); | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.