Algorithm

Text Highlighting Algorithm in TypeScript

I needed to highlight search matches in a string. Not with regex replacement — I had specific character index ranges that needed wrapping with tags.

25 Apr 2024

Text Highlighting Algorithm in TypeScript

I needed to highlight search matches in a string. Not with regex replacement — I had specific character index ranges that needed wrapping with tags.

Sounds simple. It gets tricky when ranges overlap or when you need to preserve the original string structure.

The Problem

Given a string and an array of [start, end] index pairs, wrap the highlighted portions in bold tags. Non-highlighted portions stay plain.

The Code

Typescript
function highlight(str: string, ranges: [number, number][]): string {
  if (!ranges.length) return str;

  // merge overlapping ranges
  const sorted = [...ranges].sort((a, b) => a[0] - b[0]);
  const merged: [number, number][] = [sorted[0]];

  for (let i = 1; i < sorted.length; i++) {
    const last = merged[merged.length - 1];
    if (sorted[i][0] <= last[1] + 1) {
      last[1] = Math.max(last[1], sorted[i][1]);
    } else {
      merged.push(sorted[i]);
    }
  }

  let result = '';
  let pos = 0;

  for (const [start, end] of merged) {
    result += str.slice(pos, start);
    result += `<b>${str.slice(start, end + 1)}</b>`;
    pos = end + 1;
  }

  result += str.slice(pos);
  return result;
}

console.log(highlight("abcdefgh", [[0, 2], [4, 6]]));
// "<b>abc</b>d<b>efg</b>h"

console.log(highlight("hello world", [[0, 4], [3, 7]]));
// "<b>hello wo</b>rld"  (overlapping ranges merged)

How It Works

  1. Sort the ranges by start index.
  2. Merge overlapping or adjacent ranges. If range B starts before range A ends, combine them.
  3. Build the result string: plain text between ranges, bold text inside ranges.

Complexity

  • Time: O(n log n) for sorting the ranges, plus O(m) for building the string (where m is the string length).
  • Space: O(n + m) for the merged ranges and output string.

The Trade-off

This approach handles overlapping ranges correctly by merging them first. The alternative — processing ranges without merging — breaks when ranges overlap and produces nested or malformed tags.

For real-world search highlighting, you'd also want to handle case-insensitive matching, HTML entity escaping, and potentially React-friendly output (returning arrays of JSX elements instead of raw HTML strings).