When you build HTML as a string, some characters in it are instructions to the browser, not text: < starts a tag, and & starts a character reference. A character reference is a short code that stands for one character, such as < for < and & for &; they're often called HTML entities. The browser shows the character a reference stands for as text, so < appears on the page as < but never starts a tag. HTML escaping means replacing the special characters in a piece of text with references, so the browser shows the text exactly as it was written.
It matters most when the text comes from someone else: a user's name, a comment, a search query, data from an API. Put into HTML as it is, any markup in it becomes part of your page, and markup can run JavaScript. That's called cross-site scripting (XSS): an attacker's code runs in your page with the same powers as your own, so it can read what the page shows and send requests as the signed-in user. Say a user sets their name to <img src=x onerror=alert(1)>, and the page greets them with "<p>Hello, " + name + "</p>":
| The HTML the page builds | What the browser does |
|---|---|
<p>Hello, <img src=x onerror=alert(1)></p> | Creates an <img> element. x isn't a real image, so loading it fails, and the browser runs the code in onerror. An attacker would put something worse than alert(1) there. |
<p>Hello, <img src=x onerror=alert(1)></p> | The name was escaped. The page shows the text Hello, <img src=x onerror=alert(1)>, as the user typed it, and nothing runs. |
A <script> element in the name would run too, when the HTML is built on the server and sent as the page. Scripts added with innerHTML don't run, which is why attacks there use event attributes such as onerror instead.
Which characters matter depends on where the text goes:
| Character | Where it matters | What it does there |
|---|---|---|
< | In an element's content | Starts a tag, such as <img, or an end tag, such as </p>. |
& | In content and in attribute values | Starts a character reference, so a user who typed < would see < instead. |
" | In an attribute value wrapped in " | Ends the value early, and the rest becomes new attributes: a title of x" onmouseover="alert(1) adds an onmouseover handler. |
' | In an attribute value wrapped in ' | Ends the value early, in the same way. |
> | Nowhere in content or in quoted attribute values | It only ends a tag that's already open. Escaping it anyway is the convention, and it does no harm. |
Escaping protects text in an element's content and in a quoted attribute value. Other places, such as a URL in an href or code inside a <script>, need different protection. Day to day, you often don't escape by hand: setting textContent puts text into the page as text, and React and Vue treat values you put in with {name} and {{ name }} as text, unless you opt out with dangerouslySetInnerHTML or v-html. You need escaping when you build an HTML string yourself, for innerHTML or in a server-side template.
Write highlightMatches(text, query). It returns an HTML string: text with every match of query wrapped in <mark> and </mark>, and everything else escaped so the browser shows it as text.
Escape &, <, >, ", and ' as &, <, >, ", and ', both outside and inside the marks.
query holds one or more terms separated by whitespace. A part of text matches when it equals a term, ignoring case; the mark keeps the casing of text. Every character of a term stands for itself: ., *, (, and the like have no special meaning. Search the original text, not its escaped form, so a search for amp never matches inside &.
Matches don't overlap. Scan from the start: at each position, the longest term that matches there wins, and the search continues after it. Each match gets its own <mark>, even when two are next to each other. If query has no terms (it's empty or only whitespace), return the escaped text.
highlightMatches("Tom & Jerry & tom", "tom");
// "<mark>Tom</mark> & Jerry & <mark>tom</mark>"
highlightMatches("JavaScript and Java", "java javascript");
// "<mark>JavaScript</mark> and <mark>Java</mark>"
expect(highlightMatches("Hello world", "world")).toBe("Hello <mark>world</mark>");
expect(highlightMatches("Apple, apple, APPLE", "apple")).toBe("<mark>Apple</mark>, <mark>apple</mark>, <mark>APPLE</mark>");
expect(highlightMatches('<b>"Tom" & \'Jerry\'</b>', "tom")).toBe("<b>"<mark>Tom</mark>" & 'Jerry'</b>");
expect(highlightMatches("a <i> b", "<i>")).toBe("a <mark><i></mark> b");
expect(highlightMatches("Tom & Jerry", "amp")).toBe("Tom & Jerry");
expect(highlightMatches("x < y", "lt")).toBe("x < y");
expect(highlightMatches("x<y", "lt")).toBe("x&<mark>lt</mark>;y");
expect(highlightMatches("1+1=2, 11", "1+1")).toBe("<mark>1+1</mark>=2, 11");
expect(highlightMatches("a.b axb", "a.b")).toBe("<mark>a.b</mark> axb");
expect(highlightMatches("cost: $5 (approx.)", "(approx.)")).toBe("cost: $5 <mark>(approx.)</mark>");
expect(highlightMatches("C:\\dir\\d", "\\d")).toBe("C:<mark>\\d</mark>ir<mark>\\d</mark>");
expect(highlightMatches("[x] or x", "[x]")).toBe("<mark>[x]</mark> or x");
expect(highlightMatches("JavaScript and Java", "java javascript")).toBe("<mark>JavaScript</mark> and <mark>Java</mark>");
expect(highlightMatches("red shoes, blue socks", " socks red ")).toBe("<mark>red</mark> shoes, blue <mark>socks</mark>");
expect(highlightMatches("aaaa", "aa")).toBe("<mark>aa</mark><mark>aa</mark>");
expect(highlightMatches("aaa", "aa")).toBe("<mark>aa</mark>a");
expect(highlightMatches("ab", "a b")).toBe("<mark>a</mark><mark>b</mark>");
expect(highlightMatches("a < b", "")).toBe("a < b");
expect(highlightMatches("a < b", " ")).toBe("a < b");
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with Highlight matches. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.