1/**
2 * Fuzzy match a pattern against a string.
3 *
4 * @param pattern The pattern to search for.
5 * @param text The string to search in.
6 *
7 * Returns a score greater than zero if all characters of `pattern` can be
8 * found in order in `string`. For lowercase characters in `pattern` match both
9 * lower and upper case, for uppercase only an exact match counts.
10 */
11export function fuzzytest(pattern: string, text: string): number {
12  const casesensitive = pattern === pattern.toLowerCase();
13  const exact = casesensitive
14    ? text.toLowerCase().indexOf(pattern)
15    : text.indexOf(pattern);
16  if (exact > -1) {
17    return pattern.length ** 2;
18  }
19  let score = 0;
20  let localScore = 0;
21  let pindex = 0;
22  for (let index = 0; index < text.length; index += 1) {
23    const char = text[index];
24    const search = pattern[pindex];
25    if (char === search || char.toLowerCase() === search) {
26      pindex += 1;
27      localScore += +1;
28    } else {
29      localScore = 0;
30    }
31    score += localScore;
32  }
33  return pindex === pattern.length ? score : 0;
34}
35
36/**
37 * Filter a list of possible suggestions to only those that match the pattern
38 */
39export function fuzzyfilter(pattern: string, suggestions: string[]): string[] {
40  if (!pattern) {
41    return suggestions;
42  }
43  return suggestions
44    .map((s): [string, number] => [s, fuzzytest(pattern, s)])
45    .filter(([, score]) => score)
46    .sort((a, b) => b[1] - a[1])
47    .map(([s]) => s);
48}
49
50/**
51 * Wrap fuzzy matched characters.
52 *
53 * Wrap all occurences of characters of `pattern` (in order) in `string` in
54 * <span> tags.
55 */
56export function fuzzywrap(pattern: string, text: string): string {
57  if (!pattern) {
58    return text;
59  }
60  const casesensitive = pattern === pattern.toLowerCase();
61  const exact = casesensitive
62    ? text.toLowerCase().indexOf(pattern)
63    : text.indexOf(pattern);
64  if (exact > -1) {
65    const before = text.slice(0, exact);
66    const match = text.slice(exact, exact + pattern.length);
67    const after = text.slice(exact + pattern.length);
68    return `${before}<span>${match}</span>${after}`;
69  }
70  let pindex = 0;
71  let inMatch = false;
72  const result = [];
73  for (let index = 0; index < text.length; index += 1) {
74    const char = text[index];
75    const search = pattern[pindex];
76    if (char === search || char.toLowerCase() === search) {
77      if (!inMatch) {
78        result.push("<span>");
79        inMatch = true;
80      }
81      result.push(char);
82      pindex += 1;
83    } else {
84      if (inMatch) {
85        result.push("</span>");
86        inMatch = false;
87      }
88      result.push(char);
89    }
90  }
91  if (inMatch) {
92    result.push("</span>");
93  }
94  return result.join("");
95}
96