javascript
39 lines · 7 steps
Safely highlighting search matches in text
Wrap query terms in <mark> tags while HTML-escaping everything, so highlighting never introduces an injection.
Explained by
highlit
1function escapeHtml(str) {
2 return str.replace(/[&<>"']/g, (ch) => ({
3 '&': '&',
4 '<': '<',
5 '>': '>',
6 '"': '"',
7 "'": ''',
8 }[ch]));
9}
10
11function escapeRegExp(str) {
12 return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
13}
14
15export function highlightMatches(text, query) {
16 const terms = query
17 .trim()
18 .split(/\s+/)
19 .filter(Boolean)
20 .map(escapeRegExp);
21
22 if (terms.length === 0) {
23 return escapeHtml(text);
24 }
25
26 const pattern = new RegExp(`(${terms.join('|')})`, 'gi');
27 let result = '';
28 let lastIndex = 0;
29
30 for (const match of text.matchAll(pattern)) {
31 const start = match.index;
32 result += escapeHtml(text.slice(lastIndex, start));
33 result += `<mark>${escapeHtml(match[0])}</mark>`;
34 lastIndex = start + match[0].length;
35 }
36
37 result += escapeHtml(text.slice(lastIndex));
38 return result;
39}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Escape user-controlled text before building HTML to avoid injecting markup.
- 2Escape query terms for regex so special characters are matched literally, not interpreted.
- 3Interleaving escaped plain text with escaped matches lets you inject safe markup at precise boundaries.
Related explainers
ruby
class UserAgentParser BROWSERS = [ [/Edg\/([\d.]+)/, "Edge"], [/OPR\/([\d.]+)/, "Opera"],
Parsing user-agent strings in Ruby
regex
pattern-matching
lookup-tables
Intermediate
8 steps
javascript
function evaluate(expression) { const tokens = tokenize(expression); let pos = 0;
Building a recursive descent calculator
parsing
recursion
operator-precedence
Intermediate
8 steps
javascript
import { useState, useEffect, useCallback, useRef } from 'react'; const cache = new Map(); const inflight = new Map();
Building a stale-while-revalidate hook in React
caching
request-deduplication
custom-hooks
Advanced
10 steps
javascript
import { useEffect, useRef, useState } from 'react'; export function useDelayedFlag(active, delay = 300) { const [visible, setVisible] = useState(false);
Delaying a loading spinner with a React hook
custom-hooks
debouncing
cleanup
Intermediate
8 steps
python
import re from functools import total_ordering from typing import Optional
Parsing and comparing semantic versions
regex
operator-overloading
sorting
Intermediate
7 steps
javascript
const SWIPE_THRESHOLD = 80; const MAX_TRANSLATE = 120; export function attachSwipeToDismiss(element, onDismiss) {
Building a swipe-to-dismiss gesture in JS
touch-events
gesture-detection
dom-manipulation
Intermediate
10 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/safely-highlighting-search-matches-in-text-explained-javascript-3c6c/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.