From 448f5d7be17f0811db32fe629605b0d9b11c62d7 Mon Sep 17 00:00:00 2001 From: Donnie Date: Wed, 28 Apr 2021 16:54:31 -0700 Subject: [PATCH] Change matcher to accept a TemplateResult as well to avoid using unsafeHtml --- src/common/string/filter/filter.ts | 8 +-- src/common/string/filter/sequence-matching.ts | 51 +++++++++++-------- src/dialogs/quick-bar/ha-quick-bar.ts | 19 ++++--- .../common/string/sequence_matching.test.ts | 19 +++++-- 4 files changed, 59 insertions(+), 38 deletions(-) diff --git a/src/common/string/filter/filter.ts b/src/common/string/filter/filter.ts index a592a8e81e..9cb553d410 100644 --- a/src/common/string/filter/filter.ts +++ b/src/common/string/filter/filter.ts @@ -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) { diff --git a/src/common/string/filter/sequence-matching.ts b/src/common/string/filter/sequence-matching.ts index 9278bf724c..67f8cf0a56 100644 --- a/src/common/string/filter/sequence-matching.ts +++ b/src/common/string/filter/sequence-matching.ts @@ -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 = ( 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; diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts index 61e2ee1ea5..dff502450d 100644 --- a/src/dialogs/quick-bar/ha-quick-bar.ts +++ b/src/dialogs/quick-bar/ha-quick-bar.ts @@ -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 { >`} ${item.decoratedStrings - ? unsafeHTML(item.decoratedStrings[0]) + ? item.decoratedStrings[0] : item.primaryText} ${item.altText @@ -266,7 +266,7 @@ export class QuickBar extends LitElement { ${item.decoratedStrings - ? unsafeHTML(item.decoratedStrings[1]) + ? item.decoratedStrings[1] : item.altText} @@ -298,16 +298,12 @@ export class QuickBar extends LitElement { slot="icon" >` : ""} - ${decoratedItem - ? unsafeHTML(decoratedItem[0]) - : item.categoryText} ${decoratedItem - ? unsafeHTML(decoratedItem[1]) - : item.primaryText}${decoratedItem ? decoratedItem[1] : item.primaryText} `; @@ -629,7 +625,10 @@ export class QuickBar extends LitElement { return fuzzyFilterSort( filter.trimLeft(), items, - createMatchDecorator("", "") + createMatchDecorator( + (matchedChunk) => + html`${matchedChunk}` + ) ); } ); diff --git a/test-mocha/common/string/sequence_matching.test.ts b/test-mocha/common/string/sequence_matching.test.ts index f8eb947470..85d29f3799 100644 --- a/test-mocha/common/string/sequence_matching.test.ts +++ b/test-mocha/common/string/sequence_matching.test.ts @@ -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); });