mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-31 21:17:47 +00:00
Change matcher to accept a TemplateResult as well to avoid using unsafeHtml
This commit is contained in:
parent
7ddafdd45f
commit
448f5d7be1
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
@ -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>`
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user