Skip to the code
cf.

Highlight matches

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 &amp;, &lt;, &gt;, &quot;, and &#39;, 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 &amp;.

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> &amp; Jerry &amp; <mark>tom</mark>"
highlightMatches("JavaScript and Java", "java javascript");
// "<mark>JavaScript</mark> and <mark>Java</mark>"

What the tests check

  1. wraps a match in <mark>
    expect(highlightMatches("Hello world", "world")).toBe("Hello <mark>world</mark>");
    
  2. marks every match, ignoring case but keeping the text's case
    expect(highlightMatches("Apple, apple, APPLE", "apple")).toBe("<mark>Apple</mark>, <mark>apple</mark>, <mark>APPLE</mark>");
    
  3. escapes the text outside and inside the marks
    expect(highlightMatches('<b>"Tom" & \'Jerry\'</b>', "tom")).toBe("&lt;b&gt;&quot;<mark>Tom</mark>&quot; &amp; &#39;Jerry&#39;&lt;/b&gt;");
    expect(highlightMatches("a <i> b", "<i>")).toBe("a <mark>&lt;i&gt;</mark> b");
    
  4. searches the original text, not the escaped text
    expect(highlightMatches("Tom & Jerry", "amp")).toBe("Tom &amp; Jerry");
    expect(highlightMatches("x < y", "lt")).toBe("x &lt; y");
    expect(highlightMatches("x&lt;y", "lt")).toBe("x&amp;<mark>lt</mark>;y");
    
  5. treats special regex characters in the query literally
    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");
    
  6. highlights each term of the query, the longest one first
    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>");
    
  7. gives each match its own mark, without overlaps
    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>");
    
  8. returns the escaped text when the query has no terms
    expect(highlightMatches("a < b", "")).toBe("a &lt; b");
    expect(highlightMatches("a < b", "   ")).toBe("a &lt; b");
    

Learn more

solution.jsSaved in this browser

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).