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

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
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
- Sort the ranges by start index.
- Merge overlapping or adjacent ranges. If range B starts before range A ends, combine them.
- 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).
Production-Ready Systems with LLMs and Agents
A live Maven cohort, 5 October to 2 November: eight 90-minute sessions where you build LLM and agent systems that survive real traffic, real cost and real failure. Tuesdays and Thursdays, 7:30 to 9:00pm London.
Cohort 2 starts 5 October. Eight live sessions, $1,500.
View the live cohortKeep reading
- The Algorithms Worth Knowing in Code Review: Cutting Big O Without Being Clever
- Tracking Down the Mystery: Finding the Unique Delivery ID
- Elusive Least Occurring Number in an Array
- Solving the Height of a Binary Tree Puzzle
- Harry Potter Discounts: A Code Challenge Algorithm
- Implementing a Task Heap Service in TypeScript