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:
const element = document.querySelector('#text');
const keywords = [
{style: 'yellow-marker', patterns: ['text'] },
{style: 'green-marker', patterns: ['underline', 'ital']}
];
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:
// ONLINE-RUNNER:browser;
<!doctype html>
<html>
<head>
<style>
.yellow-marker {
background: yellow;
}
.green-marker {
background: #00db00;
}
.blue-marker {
background: #5ccbff;
}
</style>
</head>
<body>
<p id="text">
This is example text with nested <b>bold text</b>.
<span>Other examples: <u>underline</u> or <i>italic</i></span>
</p>
<script>
// Finds text nodes that contain some text.
//
const findNodes = node => {
const result = [];
const filter = /^(\s|\n)+$/i;
const execute = node => {
let child = node.firstChild;
while (child) {
switch (child.nodeType) {
case Node.TEXT_NODE:
if (!filter.test(child.data)) {
result.push({
handle: child,
spacers: [child.data],
matchings: [],
});
}
break;
case Node.ELEMENT_NODE:
execute(child);
break;
}
child = child.nextSibling;
}
}
if (node) {
execute(node);
}
return result;
}
// Finds next text part that matches indicated expression.
//
const findPart = (expression, text) => {
const matching = expression.exec(text);
if (matching) {
const part = matching[0];
return {
index: matching.index,
length: part.length,
text: part
};
}
return null;
};
// Finds matching and not matching text parts using indicated expression.
//
const findParts = (expression, text, style) => {
expression.lastIndex = 0;
const spacers = [];
const matchings = [];
let index = 0;
while(true) {
const part = findPart(expression, text);
if (part == null) {
break;
}
const spacer = text.substring(index, part.index);
const matching = {
style: style,
text: part.text,
};
spacers.push(spacer);
matchings.push(matching);
index = part.index + part.length;
}
spacers.push(text.substring(index));
return {
spacers: spacers,
matchings: matchings
};
};
// Splits text nodes into marked and not marked groups.
//
const splitNodes = (nodes, keywords) => {
for (let i = 0; i < keywords.length; ++i) {
const keyword = keywords[i];
const style = keyword.style;
const patterns = keyword.patterns;
for (let m = 0; m < patterns.length; ++m) {
const pattern = patterns[m];
const expression = new RegExp(pattern, 'gi');
for (let j = 0; j < nodes.length; ++j) {
const node = nodes[j];
const spacers = node.spacers;
const matchings = node.matchings;
for (let k = 0; k < spacers.length;) {
const parts = findParts(expression, spacers[k], style);
spacers.splice(k, 1, ...parts.spacers);
matchings.splice(k, 0, ...parts.matchings);
k += parts.spacers.length;
}
}
}
}
};
// Wraps matched text parts into marked nodes.
//
const wrapNodes = (nodes) => {
for (let i = 0; i < nodes.length; ++i) {
const node = nodes[i];
const handle = node.handle, parent = handle.parentNode;
const spacers = node.spacers, matchings = node.matchings;
for (let j = 0; j < matchings.length; ++j) {
const spacer = spacers[j];
const matching = matchings[j];
if (spacer) {
const text = document.createTextNode(spacer);
parent.insertBefore(text, handle);
}
if (matching) {
const wrapper = document.createElement('span');
wrapper.className = matching.style;
wrapper.innerText = matching.text;
parent.insertBefore(wrapper, handle);
}
}
const spacer = spacers[spacers.length - 1];
if (spacer) {
const text = document.createTextNode(spacer);
parent.insertBefore(text, handle);
}
parent.removeChild(handle);
}
};
const markKeywords = (element, keywords) => {
const nodes = findNodes(element);
splitNodes(nodes, keywords);
wrapNodes(nodes);
};
// Usage example:
const element = document.querySelector('#text');
const keywords = [
{style: 'yellow-marker', patterns: ['text'] },
{style: 'green-marker', patterns: ['underline', 'ital']},
{style: 'blue-marker', patterns: ['example'] }
];
markKeywords(element, keywords);
</script>
</body>
</html>