Skip to content

Instantly share code, notes, and snippets.

@HeimMatthias
Created May 31, 2023 12:04
Show Gist options
  • Select an option

  • Save HeimMatthias/0b4d9852bcbc39d843942d5a4f5279d6 to your computer and use it in GitHub Desktop.

Select an option

Save HeimMatthias/0b4d9852bcbc39d843942d5a4f5279d6 to your computer and use it in GitHub Desktop.
Find and wrap text in DOM, even across multiple nodes
/*
* will search for a string or a regular expression and wrap resulting textnodes in <span class="selection">-elements
* even if the result spans multiple text nodes.
* returns the new span-nodes as a two-dimensional array, where each entry corresponds to all the wrapped text nodes for a result of the search, loop over these to add ids
* (as an added goodie, the returned element contains the 'text' of each result, as well as 'context.before' and 'context.after', a trimmed 100-character string of the surrounding content)
*/
function findText(root, regex) {
if (regex instanceof RegExp) {
if (!regex.flags.includes("g")) regex = new RegExp(regex.source,"g"+regex.flags);
} else {
regex = new RegExp("\\b"+regex.toString().replace(/[\^$\\.*+?()[\]{}|]/g,"\\$&")+"\\b","g")
}
let textIterator = document.createNodeIterator(root, NodeFilter.SHOW_TEXT, (node) => node.nodeValue.length>0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT) ;
let textNodes=[];
let node;
let text="";
while (node = textIterator.nextNode()) {
if (node.nodeValue.length>0) {
textNodes.push({node:node, index:text.length});
text+=node.nodeValue;
}
}
const results = [...text.matchAll(regex)];
if (results.length===0) return [];
let resultNodes=new Array();
let result=results.pop();
resultNodes[0]=new Array();
for (let i=textNodes.length-1; i>=0; i--) {
if (textNodes[i].index < result.index + result[0].length) {
resultNodes[0].text = result[0];
resultNodes[0].context = {
before: text.substring(result.index - 100,result.index),
after: text.substring(result.index + result[0].length, result.index + result[0].length + 100)
}
while (textNodes[i].index > result.index) {
let sliceLength = result.index + result[0].length - textNodes[i].index;
let span = document.createElement('span');
span.classList.add("selection");
let range=new Range();
range.setStart(textNodes[i].node, 0);
range.setEnd(textNodes[i].node, sliceLength);
range.surroundContents(span);
resultNodes[0].unshift(span);
result[0]=result[0].slice(0,-sliceLength);
i--;
}
let span = document.createElement('span');
span.classList.add("selection");
span.id="selection-"+(results.length+1)
let range=new Range();
range.setStart(textNodes[i].node, result.index - textNodes[i].index);
range.setEnd(textNodes[i].node, (result.index - textNodes[i].index)+result[0].length);
range.surroundContents(span);
resultNodes[0].unshift(span);
if (results.length===0) return resultNodes;
result=results.pop();
resultNodes.unshift(new Array());
i++;
}
}
return resultNodes;
}
// test
findText(document.body, "text to find").forEach((el,index) => {
el[0].id="selection-"+index;
console.log(index+": "+el.context.before+"๐Ÿ‘‰"+el.text+"๐Ÿ‘ˆ"+el.context.after);
});
// to unwrap:
document.querySelectorAll(".selection").forEach(el => el.replacewith(el.firstChild));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment