Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/common/string/casing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const toTitleCase = (str: string) => {
return str.replace(/\w\S*/g, (txt) => {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
});
};
66 changes: 60 additions & 6 deletions src/common/string/filter/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -525,9 +522,66 @@ 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);
}

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;

// 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) {
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
Expand Down
107 changes: 99 additions & 8 deletions src/common/string/filter/sequence-matching.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { fuzzyScore } from "./filter";
import { TemplateResult } from "lit-html";
import {
createMatches,
createMatchesFragmented,
FuzzyScore,
fuzzyScore,
} from "./filter";

/**
* Determine whether a sequence of letters exists in another string,
Expand All @@ -10,13 +16,24 @@ import { fuzzyScore } from "./filter";
* @return {number} Score representing how well the word matches the filter. Return of 0 means no match.
*/

export const fuzzySequentialMatch = (
type FuzzySequentialMatcher = (
filter: string,
item: ScorableTextItem
item: ScorableTextItem,
decorate?: MatchDecorator
) => ScorableTextItem | undefined;

export const fuzzySequentialMatch: FuzzySequentialMatcher = (
filter,
item,
decorate = createMatchDecorator((letter) => `[${letter}]`)
) => {
let topScore = Number.NEGATIVE_INFINITY;
const decoratedStrings: Decoration[][][] = [];
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(),
Expand All @@ -27,6 +44,10 @@ export const fuzzySequentialMatch = (
true
);

if (decorate) {
decoratedStrings.push(decorate(word, item, scores));
}

if (!scores) {
continue;
}
Expand All @@ -45,7 +66,11 @@ export const fuzzySequentialMatch = (
return undefined;
}

return topScore;
return {
score: topScore,
strings: item.strings,
decoratedStrings,
};
};

/**
Expand All @@ -64,21 +89,87 @@ export const fuzzySequentialMatch = (
export interface ScorableTextItem {
score?: number;
strings: string[];
decoratedStrings?: Decoration[][][];
treatArrayAsSingleString?: boolean;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this to an options object param on FuzzyFilterSort?

}

type FuzzyFilterSort = <T extends ScorableTextItem>(
filter: string,
items: T[]
items: T[],
decorate?: MatchDecorator
) => T[];

export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => {
export const fuzzyFilterSort: FuzzyFilterSort = (
filter,
items,
decorate = createMatchDecorator((letter) => `[${letter}]`)
) => {
return items
.map((item) => {
item.score = fuzzySequentialMatch(filter, item);
const match = fuzzySequentialMatch(filter, item, decorate);

item.score = match?.score;
item.decoratedStrings = match?.decoratedStrings;

return item;
})
.filter((item) => item.score !== undefined)
.sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) =>
scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0
);
};

type Decoration = string | TemplateResult;

export type Surrounder = (matchedChunk: Decoration) => Decoration;

type MatchDecorator = (
word: string,
item: ScorableTextItem,
scores?: FuzzyScore
) => Decoration[][];

export const createMatchDecorator: (
surrounder: Surrounder
) => MatchDecorator = (surrounder) => (word, item, scores) =>
_decorateMatch(word, surrounder, item, scores);

const _decorateMatch: (
word: string,
surrounder: Surrounder,
item: ScorableTextItem,
scores?: FuzzyScore
) => Decoration[][] = (word, surrounder, item, scores) => {
if (!scores) {
return [[word]];
}

const decoratedText: Decoration[][] = [];
const matches = item.treatArrayAsSingleString
? createMatchesFragmented(scores, item.strings)
: [createMatches(scores)];

for (let i = 0; i < matches.length; i++) {
const match = matches[i];
const _word = item.treatArrayAsSingleString ? item.strings[i] : word;
let pos = 0;
const actualWord: Decoration[] = [];

for (const fragmentedMatch of match) {
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.push(_word.substring(pos));
decoratedText.push(actualWord);
}
return decoratedText;
};
Loading