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); 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) => const findFirstOutOfRangeElement = (number, score: FuzzyScore) =>
score.findIndex((num) => num < number); score.findIndex((num) => num < number);
@ -554,7 +551,12 @@ export function createMatchesFragmented(
const matches: Match[][] = []; const matches: Match[][] = [];
const wordPos = score[1]; const wordPos = score[1];
let lengthCounter = 0; 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 _score = score.splice(2);
const fragmentedScores: FuzzyScore[] = []; const fragmentedScores: FuzzyScore[] = [];
for (const string of strings) { for (const string of strings) {

View File

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

View File

@ -55,7 +55,6 @@ import {
import { QuickBarParams } from "./show-dialog-quick-bar"; import { QuickBarParams } from "./show-dialog-quick-bar";
import "../../components/ha-chip"; import "../../components/ha-chip";
import { toTitleCase } from "../../common/string/casing"; import { toTitleCase } from "../../common/string/casing";
import { unsafeHTML } from "lit-html/directives/unsafe-html";
interface QuickBarItem extends ScorableTextItem { interface QuickBarItem extends ScorableTextItem {
primaryText: string; primaryText: string;
@ -121,6 +120,7 @@ export class QuickBar extends LitElement {
this._focusSet = false; this._focusSet = false;
this._filter = ""; this._filter = "";
this._search = ""; this._search = "";
this._resetDecorations();
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
@ -258,7 +258,7 @@ export class QuickBar extends LitElement {
></ha-icon>`} ></ha-icon>`}
<span class="item-text primary" <span class="item-text primary"
>${item.decoratedStrings >${item.decoratedStrings
? unsafeHTML(item.decoratedStrings[0]) ? item.decoratedStrings[0]
: item.primaryText}</span : item.primaryText}</span
> >
${item.altText ${item.altText
@ -266,7 +266,7 @@ export class QuickBar extends LitElement {
<span slot="secondary" class="item-text secondary"> <span slot="secondary" class="item-text secondary">
<span <span
>${item.decoratedStrings >${item.decoratedStrings
? unsafeHTML(item.decoratedStrings[1]) ? item.decoratedStrings[1]
: item.altText}</span : item.altText}</span
></span ></span
> >
@ -298,16 +298,12 @@ export class QuickBar extends LitElement {
slot="icon" slot="icon"
></ha-svg-icon>` ></ha-svg-icon>`
: ""} : ""}
${decoratedItem ${decoratedItem ? decoratedItem[0] : item.categoryText}</ha-chip
? unsafeHTML(decoratedItem[0])
: item.categoryText}</ha-chip
> >
</span> </span>
<span class="command-text" <span class="command-text"
>${decoratedItem >${decoratedItem ? decoratedItem[1] : item.primaryText}</span
? unsafeHTML(decoratedItem[1])
: item.primaryText}</span
> >
</mwc-list-item> </mwc-list-item>
`; `;
@ -629,7 +625,10 @@ export class QuickBar extends LitElement {
return fuzzyFilterSort<QuickBarItem>( return fuzzyFilterSort<QuickBarItem>(
filter.trimLeft(), filter.trimLeft(),
items, 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}'`, () => { it(`decorates '${expectation.pattern}' as '${expectation.expected?.decoratedString}'`, () => {
const res = fuzzySequentialMatch(expectation.pattern, item); const res = fuzzySequentialMatch(expectation.pattern, item);
assert.includeDeepMembers(res!.decoratedStrings!, [ const allDecoratedStrings = [
[expectation.expected!.decoratedString!], 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"], strings: ["light.chandelier", "Chandelier"],
score: 0, score: 0,
}; };
const itemsBeforeFilter = [ const itemsBeforeFilter: ScorableTextItem[] = [
automationTicker, automationTicker,
sensorTicker, sensorTicker,
timerCheckRouter, 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); assert.deepEqual(res, expectedItemsAfterFilter);
}); });