EN
JavaScript - mark matching text in HTML document with specific marker
5 points
In this article, we want to show how to write in JavaScript simple logic that finds in indicated element (it can be the whole document) matching text and highlights it using indicated styles (indicated marker).
That logic lets us to group highlighted texts and use different markers.

Note: to see a simpler version check this article.
Below code usage example:
xxxxxxxxxx
1
const element = document.querySelector('#text');
2
const keywords = [
3
{style: 'yellow-marker', patterns: ['text'] },
4
{style: 'green-marker', patterns: ['underline', 'ital']}
5
];
6
7
markKeywords(element, keywords);
The solution doesn't change document formatting - just wraps the matching test with an additional neutral element that marks matching text.
Keywords can be described as a regular expression that makes below logic very elastic.
Practical example:
xxxxxxxxxx
1
2
<html>
3
<head>
4
<style>
5
6
.yellow-marker {
7
background: yellow;
8
}
9
10
.green-marker {
11
background: #00db00;
12
}
13
14
.blue-marker {
15
background: #5ccbff;
16
}
17
18
</style>
19
</head>
20
<body>
21
<p id="text">
22
This is example text with nested <b>bold text</b>.
23
<span>Other examples: <u>underline</u> or <i>italic</i></span>
24
</p>
25
<script>
26
27
// Finds text nodes that contain some text.
28
//
29
const findNodes = node => {
30
const result = [];
31
const filter = /^(\s|\n)+$/i;
32
const execute = node => {
33
let child = node.firstChild;
34
while (child) {
35
switch (child.nodeType) {
36
case Node.TEXT_NODE:
37
if (!filter.test(child.data)) {
38
result.push({
39
handle: child,
40
spacers: [child.data],
41
matchings: [],
42
});
43
}
44
break;
45
case Node.ELEMENT_NODE:
46
execute(child);
47
break;
48
}
49
child = child.nextSibling;
50
}
51
}
52
if (node) {
53
execute(node);
54
}
55
return result;
56
}
57
58
// Finds next text part that matches indicated expression.
59
//
60
const findPart = (expression, text) => {
61
const matching = expression.exec(text);
62
if (matching) {
63
const part = matching[0];
64
return {
65
index: matching.index,
66
length: part.length,
67
text: part
68
};
69
}
70
return null;
71
};
72
73
// Finds matching and not matching text parts using indicated expression.
74
//
75
const findParts = (expression, text, style) => {
76
expression.lastIndex = 0;
77
const spacers = [];
78
const matchings = [];
79
let index = 0;
80
while(true) {
81
const part = findPart(expression, text);
82
if (part == null) {
83
break;
84
}
85
const spacer = text.substring(index, part.index);
86
const matching = {
87
style: style,
88
text: part.text,
89
};
90
spacers.push(spacer);
91
matchings.push(matching);
92
index = part.index + part.length;
93
}
94
spacers.push(text.substring(index));
95
return {
96
spacers: spacers,
97
matchings: matchings
98
};
99
};
100
101
// Splits text nodes into marked and not marked groups.
102
//
103
const splitNodes = (nodes, keywords) => {
104
for (let i = 0; i < keywords.length; ++i) {
105
const keyword = keywords[i];
106
const style = keyword.style;
107
const patterns = keyword.patterns;
108
for (let m = 0; m < patterns.length; ++m) {
109
const pattern = patterns[m];
110
const expression = new RegExp(pattern, 'gi');
111
for (let j = 0; j < nodes.length; ++j) {
112
const node = nodes[j];
113
const spacers = node.spacers;
114
const matchings = node.matchings;
115
for (let k = 0; k < spacers.length;) {
116
const parts = findParts(expression, spacers[k], style);
117
spacers.splice(k, 1, parts.spacers);
118
matchings.splice(k, 0, parts.matchings);
119
k += parts.spacers.length;
120
}
121
}
122
}
123
}
124
};
125
126
// Wraps matched text parts into marked nodes.
127
//
128
const wrapNodes = (nodes) => {
129
for (let i = 0; i < nodes.length; ++i) {
130
const node = nodes[i];
131
const handle = node.handle, parent = handle.parentNode;
132
const spacers = node.spacers, matchings = node.matchings;
133
for (let j = 0; j < matchings.length; ++j) {
134
const spacer = spacers[j];
135
const matching = matchings[j];
136
if (spacer) {
137
const text = document.createTextNode(spacer);
138
parent.insertBefore(text, handle);
139
}
140
if (matching) {
141
const wrapper = document.createElement('span');
142
wrapper.className = matching.style;
143
wrapper.innerText = matching.text;
144
parent.insertBefore(wrapper, handle);
145
}
146
}
147
const spacer = spacers[spacers.length - 1];
148
if (spacer) {
149
const text = document.createTextNode(spacer);
150
parent.insertBefore(text, handle);
151
}
152
parent.removeChild(handle);
153
}
154
};
155
156
const markKeywords = (element, keywords) => {
157
const nodes = findNodes(element);
158
splitNodes(nodes, keywords);
159
wrapNodes(nodes);
160
};
161
162
163
// Usage example:
164
165
const element = document.querySelector('#text');
166
const keywords = [
167
{style: 'yellow-marker', patterns: ['text'] },
168
{style: 'green-marker', patterns: ['underline', 'ital']},
169
{style: 'blue-marker', patterns: ['example'] }
170
];
171
172
markKeywords(element, keywords);
173
174
</script>
175
</body>
176
</html>