* Highlight matching text in a string
(text: string, query: string)
| 134 | * Highlight matching text in a string |
| 135 | */ |
| 136 | highlight(text: string, query: string): string { |
| 137 | if (!query || !text) return escapeHtml(text || ""); |
| 138 | |
| 139 | const normalizedQuery = query.toLowerCase().trim(); |
| 140 | const words = normalizedQuery.split(/\s+/); |
| 141 | let result = escapeHtml(text); |
| 142 | |
| 143 | for (const word of words) { |
| 144 | if (word.length < 2) continue; |
| 145 | const regex = new RegExp( |
| 146 | `(${word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, |
| 147 | "gi" |
| 148 | ); |
| 149 | const parts = result.split(/(<[^>]+>)/g); |
| 150 | let inMark = false; |
| 151 | result = parts |
| 152 | .map((part) => { |
| 153 | if (part.startsWith("<")) { |
| 154 | if (part.toLowerCase() === "<mark>") inMark = true; |
| 155 | if (part.toLowerCase() === "</mark>") inMark = false; |
| 156 | return part; |
| 157 | } |
| 158 | |
| 159 | if (inMark) { |
| 160 | return part; |
| 161 | } |
| 162 | |
| 163 | return part.replace(regex, "<mark>$1</mark>"); |
| 164 | }) |
| 165 | .join(""); |
| 166 | } |
| 167 | |
| 168 | return result; |
| 169 | } |
| 170 | } |
| 171 | |
| 172 | // Global search instance (uses SearchItem for the global search index) |
no test coverage detected