Change matcher to accept a TemplateResult as well to avoid using unsafeHtml

This commit is contained in:
Donnie 2021-04-28 16:54:31 -07:00
parent 7ddafdd45f
commit 448f5d7be1
4 changed files with 59 additions and 38 deletions

View File

@ -537,9 +537,6 @@ export function createMatches(score: undefined | FuzzyScore): Match[] {
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);
@ -554,7 +551,12 @@ export function createMatchesFragmented(
const matches: Match[][] = [];
const wordPos = score[1];
let lengthCounter = 0;
// The first and second elements in score represent total score, and the offset at which
// matching started. For this method, we only care about the rest of the score array
// which represents matched position indexes.
const _score = score.splice(2);
const fragmentedScores: FuzzyScore[] = [];
for (const string of strings) {

View File

@ -1,3 +1,4 @@
import { TemplateResult } from "lit-html";
import {
createMatches,
createMatchesFragmented,
@ -24,10 +25,10 @@ type FuzzySequentialMatcher = (
export const fuzzySequentialMatch: FuzzySequentialMatcher = (
filter,
item,
decorate = createMatchDecorator("[", "]")
decorate = createMatchDecorator((letter) => `[${letter}]`)
) => {
let topScore = Number.NEGATIVE_INFINITY;
const decoratedStrings: string[][] = [];
const decoratedStrings: Decoration[][][] = [];
const strings = item.treatArrayAsSingleString
? [item.strings.join("")]
: item.strings;
@ -88,7 +89,7 @@ export const fuzzySequentialMatch: FuzzySequentialMatcher = (
export interface ScorableTextItem {
score?: number;
strings: string[];
decoratedStrings?: string[][];
decoratedStrings?: Decoration[][][];
treatArrayAsSingleString?: boolean;
}
@ -101,7 +102,7 @@ type FuzzyFilterSort = <T extends ScorableTextItem>(
export const fuzzyFilterSort: FuzzyFilterSort = (
filter,
items,
decorate = createMatchDecorator("[", "]")
decorate = createMatchDecorator((letter) => `[${letter}]`)
) => {
return items
.map((item) => {
@ -118,48 +119,56 @@ export const fuzzyFilterSort: FuzzyFilterSort = (
);
};
type Decoration = string | TemplateResult;
export type Surrounder = (matchedChunk: Decoration) => Decoration;
type MatchDecorator = (
word: string,
item: ScorableTextItem,
scores?: FuzzyScore
) => string[];
) => Decoration[][];
export const createMatchDecorator: (
left: string,
right: string
) => MatchDecorator = (left, right) => (word, item, scores) =>
_decorateMatch(word, [left, right], item, scores);
surrounder: Surrounder
) => MatchDecorator = (surrounder) => (word, item, scores) =>
_decorateMatch(word, surrounder, item, scores);
const _decorateMatch: (
word: string,
surroundWith: [string, string],
surrounder: Surrounder,
item: ScorableTextItem,
scores?: FuzzyScore
) => string[] = (word, surroundWith, item, scores) => {
) => Decoration[][] = (word, surrounder, item, scores) => {
if (!scores) {
return [word];
return [[word]];
}
const decoratedText: string[] = [];
const decoratedText: Decoration[][] = [];
const matches = item.treatArrayAsSingleString
? createMatchesFragmented(scores, item.strings)
: [createMatches(scores)];
const [left, right] = surroundWith;
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 = "";
const actualWord: Decoration[] = [];
for (const fragmentedMatch of match) {
actualWord +=
_word.substring(pos, fragmentedMatch.start) +
left +
_word.substring(fragmentedMatch.start, fragmentedMatch.end) +
right;
const unmatchedChunk = _word.substring(pos, fragmentedMatch.start);
const matchedChunk = _word.substring(
fragmentedMatch.start,
fragmentedMatch.end
);
actualWord.push(unmatchedChunk);
actualWord.push(surrounder(matchedChunk));
pos = fragmentedMatch.end;
}
actualWord += _word.substring(pos);
actualWord.push(_word.substring(pos));
decoratedText.push(actualWord);
}
return decoratedText;

View File

@ -55,7 +55,6 @@ import {
import { QuickBarParams } from "./show-dialog-quick-bar";
import "../../components/ha-chip";
import { toTitleCase } from "../../common/string/casing";
import { unsafeHTML } from "lit-html/directives/unsafe-html";
interface QuickBarItem extends ScorableTextItem {
primaryText: string;
@ -121,6 +120,7 @@ export class QuickBar extends LitElement {
this._focusSet = false;
this._filter = "";
this._search = "";
this._resetDecorations();
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@ -258,7 +258,7 @@ export class QuickBar extends LitElement {
></ha-icon>`}
<span class="item-text primary"
>${item.decoratedStrings
? unsafeHTML(item.decoratedStrings[0])
? item.decoratedStrings[0]
: item.primaryText}</span
>
${item.altText
@ -266,7 +266,7 @@ export class QuickBar extends LitElement {
<span slot="secondary" class="item-text secondary">
<span
>${item.decoratedStrings
? unsafeHTML(item.decoratedStrings[1])
? item.decoratedStrings[1]
: item.altText}</span
></span
>
@ -298,16 +298,12 @@ export class QuickBar extends LitElement {
slot="icon"
></ha-svg-icon>`
: ""}
${decoratedItem
? unsafeHTML(decoratedItem[0])
: item.categoryText}</ha-chip
${decoratedItem ? decoratedItem[0] : item.categoryText}</ha-chip
>
</span>
<span class="command-text"
>${decoratedItem
? unsafeHTML(decoratedItem[1])
: item.primaryText}</span
>${decoratedItem ? decoratedItem[1] : item.primaryText}</span
>
</mwc-list-item>
`;
@ -629,7 +625,10 @@ export class QuickBar extends LitElement {
return fuzzyFilterSort<QuickBarItem>(
filter.trimLeft(),
items,
createMatchDecorator("<span class='highlight-letter'>", "</span>")
createMatchDecorator(
(matchedChunk) =>
html`<span class="highlight-letter">${matchedChunk}</span>`
)
);
}
);

View File

@ -77,8 +77,13 @@ describe("fuzzySequentialMatch", () => {
it(`decorates '${expectation.pattern}' as '${expectation.expected?.decoratedString}'`, () => {
const res = fuzzySequentialMatch(expectation.pattern, item);
assert.includeDeepMembers(res!.decoratedStrings!, [
[expectation.expected!.decoratedString!],
const allDecoratedStrings = [
res!.decoratedStrings![0][0].join(""),
res!.decoratedStrings![1][0].join(""),
];
assert.includeDeepMembers(allDecoratedStrings, [
expectation.expected!.decoratedString!,
]);
});
}
@ -114,7 +119,7 @@ describe("fuzzyFilterSort", () => {
strings: ["light.chandelier", "Chandelier"],
score: 0,
};
const itemsBeforeFilter = [
const itemsBeforeFilter: ScorableTextItem[] = [
automationTicker,
sensorTicker,
timerCheckRouter,
@ -149,7 +154,13 @@ describe("fuzzyFilterSort", () => {
},
];
const res = fuzzyFilterSort(filter, itemsBeforeFilter);
const res = fuzzyFilterSort(filter, itemsBeforeFilter).map((item) => ({
...item,
decoratedStrings: [
[item.decoratedStrings![0][0].join("")],
[item.decoratedStrings![1][0].join("")],
],
}));
assert.deepEqual(res, expectedItemsAfterFilter);
});