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;
}
export function createMatches(score: undefined | FuzzyScore): Match[] {
if (typeof score === "undefined") {
return [];
}
function _createMatches(score: FuzzyScore, wordPos: number) {
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 last = res[res.length - 1];
if (last && last.end === pos) {
@ -525,9 +522,64 @@ export function createMatches(score: undefined | FuzzyScore): Match[] {
res.push({ start: pos, end: pos + 1 });
}
}
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.
* 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,
@ -23,8 +28,11 @@ export const fuzzySequentialMatch: FuzzySequentialMatcher = (
) => {
let topScore = Number.NEGATIVE_INFINITY;
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(
filter,
filter.toLowerCase(),
@ -36,7 +44,7 @@ export const fuzzySequentialMatch: FuzzySequentialMatcher = (
);
if (decorate) {
decoratedStrings.push(decorate(word, scores));
decoratedStrings.push(decorate(word, item, scores));
}
if (!scores) {
@ -81,6 +89,7 @@ export interface ScorableTextItem {
score?: number;
strings: string[];
decoratedStrings?: string[][];
treatArrayAsSingleString?: boolean;
}
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: (
left: string,
right: string
) => MatchDecorator = (left, right) => (word, scores) =>
_decorateMatch(word, [left, right], scores);
) => MatchDecorator = (left, right) => (word, item, scores) =>
_decorateMatch(word, [left, right], item, scores);
const _decorateMatch: (
word: string,
surroundWith: [string, string],
item: ScorableTextItem,
scores?: FuzzyScore
) => string[] = (word, surroundWith, scores) => {
) => string[] = (word, surroundWith, item, scores) => {
if (!scores) {
return [word];
}
const decoratedText: string[] = [];
const matches = createMatches(scores);
const matches = item.treatArrayAsSingleString
? createMatchesFragmented(scores, item.strings)
: [createMatches(scores)];
const [left, right] = surroundWith;
let pos = 0;
let actualWord = "";
for (const match of matches) {
actualWord +=
word.substring(pos, match.start) +
left +
word.substring(match.start, match.end) +
right;
pos = match.end;
for (let i = 0; i < matches.length; i++) {
const match = matches[i];
const _word = item.treatArrayAsSingleString ? item.strings[i] : word;
let pos = 0;
let actualWord = "";
for (const fragmentedMatch of match) {
actualWord +=
_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;
};

View File

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