|
1 | 1 | import type { FuseResult, FuseResultMatch } from 'fuse.js' |
2 | 2 | import type { GetItemKeys } from '../types/utils' |
3 | 3 | |
| 4 | +const htmlEscapes: Record<string, string> = { |
| 5 | +'&': '&', |
| 6 | +'<': '<', |
| 7 | +'>': '>', |
| 8 | +'"': '"', |
| 9 | +'\'': ''' |
| 10 | +} |
| 11 | + |
| 12 | +function escapeHTML(str: string): string { |
| 13 | +return str.replace(/[&<>"']/g, char => htmlEscapes[char]!) |
| 14 | +} |
| 15 | + |
| 16 | +// Check if string is already HTML-escaped to avoid double-escaping |
| 17 | +function isAlreadyEscaped(str: string): boolean { |
| 18 | +return /&(?:amp|lt|gt|quot|#39);/.test(str) |
| 19 | +} |
| 20 | + |
| 21 | +function sanitize(str: string): string { |
| 22 | +if (isAlreadyEscaped(str)) { |
| 23 | +return str |
| 24 | +} |
| 25 | +return escapeHTML(str) |
| 26 | +} |
| 27 | + |
4 | 28 | function truncateHTMLFromStart(html: string, maxLength: number) { |
5 | 29 | let truncated = '' |
6 | 30 | let totalLength = 0 |
@@ -49,16 +73,16 @@ export function highlight<T>(item: T & { matches?: FuseResult<T>['matches'] }, s
|
49 | 73 | const isMatched = (lastIndiceNextIndex - region[0]) >= searchTerm.length |
50 | 74 | |
51 | 75 | content += [ |
52 | | -value.substring(nextUnhighlightedRegionStartingIndex, region[0]), |
| 76 | +sanitize(value.substring(nextUnhighlightedRegionStartingIndex, region[0])), |
53 | 77 | isMatched && `<mark>`, |
54 | | -value.substring(region[0], lastIndiceNextIndex), |
| 78 | +sanitize(value.substring(region[0], lastIndiceNextIndex)), |
55 | 79 | isMatched && '</mark>' |
56 | 80 | ].filter(Boolean).join('') |
57 | 81 | |
58 | 82 | nextUnhighlightedRegionStartingIndex = lastIndiceNextIndex |
59 | 83 | }) |
60 | 84 | |
61 | | -content += value.substring(nextUnhighlightedRegionStartingIndex) |
| 85 | +content += sanitize(value.substring(nextUnhighlightedRegionStartingIndex)) |
62 | 86 | |
63 | 87 | const markIndex = content.indexOf('<mark>') |
64 | 88 | if (markIndex !== -1) { |
|