EN
JavaScript - mark matching text in HTML document
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.
Note: to see the solution that groups and marks detected keywords with different markers check this article.
Below code usage example:
const element = document.querySelector('#text');
const keywords = ['text', 'underline', 'ital'];
markKeywords(element, 'yellow-marker', 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;
}
</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 == null) {
return null;
}
const part = matching[0];
return {
index: matching.index,
length: part.length,
text: part
};
};
// Finds in text parts that matches and not indicated expression.
//
const findParts = (expression, text) => {
expression.lastIndex = 0;
const spacers = [];
const matchings = [];
let index = 0;
while(true) {
const part = findPart(expression, text);
if (part == null) {
break;
}
spacers.push(text.substring(index, part.index));
matchings.push(part.text);
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 expression = new RegExp(`${keywords[i]}`, '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]);
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, style) => {
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 = style;
wrapper.innerText = matching;
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, style, keywords) => {
const nodes = findNodes(element);
splitNodes(nodes, keywords);
wrapNodes(nodes, style);
};
// Usage example:
const element = document.querySelector('#text');
const keywords = ['text', 'underline', 'ital'];
markKeywords(element, 'yellow-marker', keywords);
</script>
</body>
</html>