Add ability to receive string array, treat as one string for matching, but return same string array decorated

This commit is contained in:
Donnie 2021-04-28 15:08:20 -07:00
parent fdf1eae882
commit 7ddafdd45f
3 changed files with 104 additions and 34 deletions

View File

@ -510,13 +510,10 @@ export interface FuzzyScorer {
): FuzzyScore | undefined; ): FuzzyScore | undefined;
} }
export function createMatches(score: undefined | FuzzyScore): Match[] { function _createMatches(score: FuzzyScore, wordPos: number) {
if (typeof score === "undefined") {
return [];
}
const res: Match[] = []; const res: Match[] = [];
const wordPos = score[1];
for (let i = score.length - 1; i > 1; i--) { for (let i = score.length - 1; i >= 0; i--) {
const pos = score[i] + wordPos; const pos = score[i] + wordPos;
const last = res[res.length - 1]; const last = res[res.length - 1];
if (last && last.end === pos) { if (last && last.end === pos) {
@ -525,9 +522,64 @@ export function createMatches(score: undefined | FuzzyScore): Match[] {
res.push({ start: pos, end: pos + 1 }); res.push({ start: pos, end: pos + 1 });
} }
} }
return res; return res;
} }
export function createMatches(score: undefined | FuzzyScore): Match[] {
if (typeof score === "undefined") {
return [];
}
const wordPos = score[1];
const _score = score.splice(2);
return _createMatches(_score, wordPos);
}
// The first and second elements in score represent total score, and the offset at which
// matching started. For this method, we only care about match positions, not the score
// or offset.
const findFirstOutOfRangeElement = (number, score: FuzzyScore) =>
score.findIndex((num) => num < number);
export function createMatchesFragmented(
score: undefined | FuzzyScore,
strings: string[]
): Match[][] {
if (typeof score === "undefined") {
return [];
}
const matches: Match[][] = [];
const wordPos = score[1];
let lengthCounter = 0;
const _score = score.splice(2);
const fragmentedScores: FuzzyScore[] = [];
for (const string of strings) {
const prevLengthCounter = lengthCounter;
lengthCounter += string.length;
const lastIndex = findFirstOutOfRangeElement(lengthCounter, _score);
if (lastIndex < 0) {
fragmentedScores.push([]);
continue;
}
fragmentedScores.push(
_score.splice(lastIndex).map((pos) => pos - prevLengthCounter)
);
}
for (const fragmentedScore of fragmentedScores) {
const res = _createMatches(fragmentedScore, wordPos);
matches.push(res);
}
return matches;
}
/** /**
* A fast function (therefore imprecise) to check if code points are emojis. * A fast function (therefore imprecise) to check if code points are emojis.
* Generated using https://github.com/alexdima/unicode-utils/blob/master/generate-emoji-test.js * Generated using https://github.com/alexdima/unicode-utils/blob/master/generate-emoji-test.js

View File

@ -1,4 +1,9 @@
import { createMatches, FuzzyScore, fuzzyScore } from "./filter"; import {
createMatches,
createMatchesFragmented,
FuzzyScore,
fuzzyScore,
} from "./filter";
/** /**
* Determine whether a sequence of letters exists in another string, * Determine whether a sequence of letters exists in another string,
@ -23,8 +28,11 @@ export const fuzzySequentialMatch: FuzzySequentialMatcher = (
) => { ) => {
let topScore = Number.NEGATIVE_INFINITY; let topScore = Number.NEGATIVE_INFINITY;
const decoratedStrings: string[][] = []; const decoratedStrings: string[][] = [];
const strings = item.treatArrayAsSingleString
? [item.strings.join("")]
: item.strings;
for (const word of item.strings) { for (const word of strings) {
const scores = fuzzyScore( const scores = fuzzyScore(
filter, filter,
filter.toLowerCase(), filter.toLowerCase(),
@ -36,7 +44,7 @@ export const fuzzySequentialMatch: FuzzySequentialMatcher = (
); );
if (decorate) { if (decorate) {
decoratedStrings.push(decorate(word, scores)); decoratedStrings.push(decorate(word, item, scores));
} }
if (!scores) { if (!scores) {
@ -81,6 +89,7 @@ export interface ScorableTextItem {
score?: number; score?: number;
strings: string[]; strings: string[];
decoratedStrings?: string[][]; decoratedStrings?: string[][];
treatArrayAsSingleString?: boolean;
} }
type FuzzyFilterSort = <T extends ScorableTextItem>( type FuzzyFilterSort = <T extends ScorableTextItem>(
@ -109,43 +118,49 @@ export const fuzzyFilterSort: FuzzyFilterSort = (
); );
}; };
type MatchDecorator = (word: string, scores?: FuzzyScore) => string[]; type MatchDecorator = (
word: string,
item: ScorableTextItem,
scores?: FuzzyScore
) => string[];
export const createMatchDecorator: ( export const createMatchDecorator: (
left: string, left: string,
right: string right: string
) => MatchDecorator = (left, right) => (word, scores) => ) => MatchDecorator = (left, right) => (word, item, scores) =>
_decorateMatch(word, [left, right], scores); _decorateMatch(word, [left, right], item, scores);
const _decorateMatch: ( const _decorateMatch: (
word: string, word: string,
surroundWith: [string, string], surroundWith: [string, string],
item: ScorableTextItem,
scores?: FuzzyScore scores?: FuzzyScore
) => string[] = (word, surroundWith, scores) => { ) => string[] = (word, surroundWith, item, scores) => {
if (!scores) { if (!scores) {
return [word]; return [word];
} }
const decoratedText: string[] = []; const decoratedText: string[] = [];
const matches = createMatches(scores); const matches = item.treatArrayAsSingleString
? createMatchesFragmented(scores, item.strings)
: [createMatches(scores)];
const [left, right] = surroundWith; const [left, right] = surroundWith;
let pos = 0;
let actualWord = ""; for (let i = 0; i < matches.length; i++) {
for (const match of matches) { const match = matches[i];
actualWord += const _word = item.treatArrayAsSingleString ? item.strings[i] : word;
word.substring(pos, match.start) + let pos = 0;
left + let actualWord = "";
word.substring(match.start, match.end) + for (const fragmentedMatch of match) {
right; actualWord +=
pos = match.end; _word.substring(pos, fragmentedMatch.start) +
left +
_word.substring(fragmentedMatch.start, fragmentedMatch.end) +
right;
pos = fragmentedMatch.end;
}
actualWord += _word.substring(pos);
decoratedText.push(actualWord);
} }
actualWord += word.substring(pos);
const fragments = actualWord.split("::");
for (const fragment of fragments) {
decoratedText.push(fragment);
}
return decoratedText; return decoratedText;
}; };

View File

@ -476,7 +476,8 @@ export class QuickBar extends LitElement {
return { return {
...commandItem, ...commandItem,
categoryKey: "reload", categoryKey: "reload",
strings: [`${commandItem.categoryText}::${commandItem.primaryText}`], strings: [`${commandItem.categoryText} `, commandItem.primaryText],
treatArrayAsSingleString: true,
}; };
}); });
} }
@ -510,7 +511,8 @@ export class QuickBar extends LitElement {
return this._generateConfirmationCommand( return this._generateConfirmationCommand(
{ {
...item, ...item,
strings: [`${item.categoryText}::${item.primaryText}`], strings: [`${item.categoryText} `, item.primaryText],
treatArrayAsSingleString: true,
}, },
this.hass.localize("ui.dialogs.generic.ok") this.hass.localize("ui.dialogs.generic.ok")
); );
@ -611,7 +613,8 @@ export class QuickBar extends LitElement {
return { return {
...navItem, ...navItem,
strings: [`${navItem.categoryText}::${navItem.primaryText}`], strings: [`${navItem.categoryText} `, navItem.primaryText],
treatArrayAsSingleString: true,
categoryKey, categoryKey,
}; };
}); });