Mirror of https://github.com/roostorg/osprey
github.com/roostorg/osprey
1export function pluralize(word: string, val: number, pluralization: string = 's'): string {
2 if (val === 1) return word;
3
4 return `${word}${pluralization}`;
5}
6
7/**
8 * Splits a string into an array of objects containing substrings and their match status.
9 * Supports both exact string matching and regex patterns. Matched substrings are included in the result.
10 * Useful for tokenizing a string so that substrings matching a search query can be highlighted.
11 *
12 * @param str - The string to split
13 * @param pattern - The substring or regex pattern to match against
14 * @param useRegex - Whether to treat the pattern as a regex (default: false)
15 * @returns Array of objects with substring and matched properties
16 * @throws {Error} When useRegex is true and pattern is an invalid regex
17 *
18 * @example
19 * ```ts
20 * // Exact string matching
21 * const result1 = splitStringIncludingMatches("foo boor", "oo");
22 * // returns [
23 * // { substring: "f", matched: false },
24 * // { substring: "oo", matched: true },
25 * // { substring: " b", matched: false },
26 * // { substring: "oo", matched: true },
27 * // { substring: "r", matched: false }
28 * // ]
29 *
30 * // Regex matching
31 * const result2 = splitStringIncludingMatches("foo123bar456", "\\d+", true);
32 * // returns [
33 * // { substring: "foo", matched: false },
34 * // { substring: "123", matched: true },
35 * // { substring: "bar", matched: false },
36 * // { substring: "456", matched: true }
37 * // ]
38 * ```
39 */
40export function splitStringIncludingMatches(
41 str: string,
42 pattern: string,
43 useRegex: boolean = false
44): Array<{ substring: string; matched: boolean }> {
45 // Early returns for edge cases
46 if (!str) return [];
47 if (!pattern) return [{ substring: str, matched: false }];
48
49 const result = useRegex ? splitStringRegexMatch(str, pattern) : splitStringExactMatch(str, pattern);
50
51 // Filter out empty substrings (except for edge cases where they might be meaningful)
52 return result.filter((item) => item.substring.length > 0);
53}
54
55export function highlightMatchedText(text: Array<{ substring: string; matched: boolean }>, highlightClass?: string) {
56 const highlightText = (content: string) => {
57 return (
58 <span className={highlightClass ?? ''} style={{ backgroundColor: highlightClass ? 'inherit' : 'yellow' }}>
59 {content}
60 </span>
61 );
62 };
63 const highlighted = text.map((item) => (item.matched ? highlightText(item.substring) : item.substring));
64 return <>{highlighted}</>;
65}
66
67/**
68 * Helper function to split string using exact text matching
69 */
70function splitStringExactMatch(str: string, pattern: string): Array<{ substring: string; matched: boolean }> {
71 const result: Array<{ substring: string; matched: boolean }> = [];
72 let remaining = str;
73 let index = remaining.indexOf(pattern);
74
75 while (index !== -1) {
76 // Add non-matching substring before the match
77 const beforeMatch = remaining.substring(0, index);
78 if (beforeMatch) {
79 result.push({ substring: beforeMatch, matched: false });
80 }
81
82 // Add the matched substring
83 result.push({ substring: pattern, matched: true });
84
85 remaining = remaining.substring(index + pattern.length);
86 index = remaining.indexOf(pattern);
87 }
88
89 // Add any remaining non-matching substring
90 if (remaining) {
91 result.push({ substring: remaining, matched: false });
92 }
93
94 return result;
95}
96
97/**
98 * Helper function to split string using regex pattern matching
99 */
100function splitStringRegexMatch(str: string, pattern: string): Array<{ substring: string; matched: boolean }> {
101 const result: Array<{ substring: string; matched: boolean }> = [];
102 let regex: RegExp;
103
104 try {
105 regex = new RegExp(pattern, 'g');
106 } catch (error) {
107 // invalid regex can't match in the string, so no need to force error handling upstream.
108 return [{ substring: str, matched: false }];
109 }
110
111 let lastIndex = 0;
112 let match: RegExpExecArray | null;
113
114 while ((match = regex.exec(str)) !== null) {
115 // Add non-matching substring before the match
116 if (match.index > lastIndex) {
117 const nonMatchSubstring = str.substring(lastIndex, match.index);
118 if (nonMatchSubstring) {
119 result.push({ substring: nonMatchSubstring, matched: false });
120 }
121 }
122
123 // Add the matched substring
124 result.push({ substring: match[0], matched: true });
125 lastIndex = regex.lastIndex;
126
127 // Prevent infinite loop for zero-length matches
128 if (match[0].length === 0) {
129 regex.lastIndex++;
130 }
131 }
132
133 // Add any remaining non-matching substring
134 if (lastIndex < str.length) {
135 result.push({ substring: str.substring(lastIndex), matched: false });
136 }
137
138 return result;
139}