This question already has an answer here:
Replace each word in webpage's paragraphs with a button containing that text
(1 answer)
Closed 5 years ago.
I have two wrappers:
function wrapSentences(str, tmpl) {
return str.replace(/[^\.!\?]+[\.!\?]+/g, tmpl || "<sentence>$&</sentence>")
}
and
function wrapWords(str, tmpl) {
return str.replace(/\w+/g, tmpl || "<word>$&</word>");
}
I use these in our extension to wrap every word and sentence on any webpage the user visits for TTS and settings purposes.
document.body is the most atomic element on every website, but doing body.innerHTML = wrapWords(body.innerText) will (obviously) replace any element that was in between the different text nodes, thus breaking (the visual part of) the website. I'm looking for a way to find any closest element around any text without knowing anything specific about that element, so I can replace it with a wrapped equivalent without altering the website in any way.
I found several examples that go to the deepest child, but they all rely on passing something (node or id) the extension has no way of knowing about. We will use rangy for highlighting, but have the same issue... I always end up having to pass a node or id that the extension is unable to be aware of when visiting random sites.
One of the examples that needs a node passed:
function replaceTextNodes(node, newText) {
if (node.nodeType === 3) {
//Filter out text nodes that contain only whitespace
if (!/^\s*$/.test(node.data)) {
node.data = newText;
}
} else if (node.hasChildNodes()) {
for (let i = 0, len = node.childNodes.length; i < len; ++i) {
replaceTextNodes(node.childNodes[i], newText);
}
}
}
I'll be happy to explain it better if needed. I fear my wording may not always be the best, I'm aware of that.
It looks like what you want is all the text nodes on the page... This question might have your answer.
Using the function from the first answer:
Edit: now wrapping text in <word> nodes, not just their textContent
function textNodesUnder(el){
var n, a=[], walk=document.createTreeWalker(el,NodeFilter.SHOW_TEXT,null,false);
while(n=walk.nextNode()) a.push(n);
return a;
}
exp = /(?:(\W+)|(\w+))/g
textNodesUnder(document.body)
.filter(t => !/^\s*$/.test(t.textContent))
.forEach(t => {
let s = t.textContent, match
while(match = exp.exec(s)) {
let el
if(match[1] !== undefined) {
el = document.createTextNode(match[1])
}
else {
el = document.createElement("word")
el.textContent = match[2]
}
t.parentNode.insertBefore(el, t)
}
t.parentElement.removeChild(t)
})
Related
How can I find DIV with certain text? For example:
<div>
SomeText, text continues.
</div>
Trying to use something like this:
var text = document.querySelector('div[SomeText*]').innerTEXT;
alert(text);
But ofcourse it will not work. How can I do it?
OP's question is about plain JavaScript and not jQuery.
Although there are plenty of answers and I like #Pawan Nogariya answer, please check this alternative out.
You can use XPATH in JavaScript. More info on the MDN article here.
The document.evaluate() method evaluates an XPATH query/expression. So you can pass XPATH expressions there, traverse into the HTML document and locate the desired element.
In XPATH you can select an element, by the text node like the following, whch gets the div that has the following text node.
//div[text()="Hello World"]
To get an element that contains some text use the following:
//div[contains(., 'Hello')]
The contains() method in XPATH takes a node as first parameter and the text to search for as second parameter.
Check this plunk here, this is an example use of XPATH in JavaScript
Here is a code snippet:
var headings = document.evaluate("//h1[contains(., 'Hello')]", document, null, XPathResult.ANY_TYPE, null );
var thisHeading = headings.iterateNext();
console.log(thisHeading); // Prints the html element in console
console.log(thisHeading.textContent); // prints the text content in console
thisHeading.innerHTML += "<br />Modified contents";
As you can see, I can grab the HTML element and modify it as I like.
You could use this pretty simple solution:
Array.from(document.querySelectorAll('div'))
.find(el => el.textContent === 'SomeText, text continues.');
The Array.from will convert the NodeList to an array (there are multiple methods to do this like the spread operator or slice)
The result now being an array allows for using the Array.find method, you can then put in any predicate. You could also check the textContent with a regex or whatever you like.
Note that Array.from and Array.find are ES2015 features. Te be compatible with older browsers like IE10 without a transpiler:
Array.prototype.slice.call(document.querySelectorAll('div'))
.filter(function (el) {
return el.textContent === 'SomeText, text continues.'
})[0];
Since you have asked it in javascript so you can have something like this
function contains(selector, text) {
var elements = document.querySelectorAll(selector);
return Array.prototype.filter.call(elements, function(element){
return RegExp(text).test(element.textContent);
});
}
And then call it like this
contains('div', 'sometext'); // find "div" that contain "sometext"
contains('div', /^sometext/); // find "div" that start with "sometext"
contains('div', /sometext$/i); // find "div" that end with "sometext", case-insensitive
This solution does the following:
Uses the ES6 spread operator to convert the NodeList of all divs to an array.
Provides output if the div contains the query string, not just if it exactly equals the query string (which happens for some of the other answers). e.g. It should provide output not just for 'SomeText' but also for 'SomeText, text continues'.
Outputs the entire div contents, not just the query string. e.g. For 'SomeText, text continues' it should output that whole string, not just 'SomeText'.
Allows for multiple divs to contain the string, not just a single div.
[...document.querySelectorAll('div')] // get all the divs in an array
.map(div => div.innerHTML) // get their contents
.filter(txt => txt.includes('SomeText')) // keep only those containing the query
.forEach(txt => console.log(txt)); // output the entire contents of those
<div>SomeText, text continues.</div>
<div>Not in this div.</div>
<div>Here is more SomeText.</div>
Coming across this in 2021, I found using XPATH too complicated (need to learn something else) for something that should be rather simple.
Came up with this:
function querySelectorIncludesText (selector, text){
return Array.from(document.querySelectorAll(selector))
.find(el => el.textContent.includes(text));
}
Usage:
querySelectorIncludesText('button', 'Send')
Note that I decided to use includes and not a strict comparison, because that's what I really needed, feel free to adapt.
You might need those polyfills if you want to support all browsers:
/**
* String.prototype.includes() polyfill
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes#Polyfill
* #see https://vanillajstoolkit.com/polyfills/stringincludes/
*/
if (!String.prototype.includes) {
String.prototype.includes = function (search, start) {
'use strict';
if (search instanceof RegExp) {
throw TypeError('first argument must not be a RegExp');
}
if (start === undefined) {
start = 0;
}
return this.indexOf(search, start) !== -1;
};
}
You best see if you have a parent element of the div you are querying. If so get the parent element and perform an element.querySelectorAll("div"). Once you get the nodeList apply a filter on it over the innerText property. Assume that a parent element of the div that we are querying has an id of container. You can normally access container directly from the id but let's do it the proper way.
var conty = document.getElementById("container"),
divs = conty.querySelectorAll("div"),
myDiv = [...divs].filter(e => e.innerText == "SomeText");
So that's it.
If you don't want to use jquery or something like that then you can try this:
function findByText(rootElement, text){
var filter = {
acceptNode: function(node){
// look for nodes that are text_nodes and include the following string.
if(node.nodeType === document.TEXT_NODE && node.nodeValue.includes(text)){
return NodeFilter.FILTER_ACCEPT;
}
return NodeFilter.FILTER_REJECT;
}
}
var nodes = [];
var walker = document.createTreeWalker(rootElement, NodeFilter.SHOW_TEXT, filter, false);
while(walker.nextNode()){
//give me the element containing the node
nodes.push(walker.currentNode.parentNode);
}
return nodes;
}
//call it like
var nodes = findByText(document.body,'SomeText');
//then do what you will with nodes[];
for(var i = 0; i < nodes.length; i++){
//do something with nodes[i]
}
Once you have the nodes in an array that contain the text you can do something with them. Like alert each one or print to console. One caveat is that this may not necessarily grab divs per se, this will grab the parent of the textnode that has the text you are looking for.
Google has this as a top result for For those who need to find a node with certain text.
By way of update, a nodelist is now iterable in modern browsers without having to convert it to an array.
The solution can use forEach like so.
var elList = document.querySelectorAll(".some .selector");
elList.forEach(function(el) {
if (el.innerHTML.indexOf("needle") !== -1) {
// Do what you like with el
// The needle is case sensitive
}
});
This worked for me to do a find/replace text inside a nodelist when a normal selector could not choose just one node so I had to filter each node one by one to check it for the needle.
Use XPath and document.evaluate(), and make sure to use text() and not . for the contains() argument, or else you will have the entire HTML, or outermost div element matched.
var headings = document.evaluate("//h1[contains(text(), 'Hello')]", document, null, XPathResult.ANY_TYPE, null );
or ignore leading and trailing whitespace
var headings = document.evaluate("//h1[contains(normalize-space(text()), 'Hello')]", document, null, XPathResult.ANY_TYPE, null );
or match all tag types (div, h1, p, etc.)
var headings = document.evaluate("//*[contains(text(), 'Hello')]", document, null, XPathResult.ANY_TYPE, null );
Then iterate
let thisHeading;
while(thisHeading = headings.iterateNext()){
// thisHeading contains matched node
}
Here's the XPath approach but with a minimum of XPath jargon.
Regular selection based on element attribute values (for comparison):
// for matching <element class="foo bar baz">...</element> by 'bar'
var things = document.querySelectorAll('[class*="bar"]');
for (var i = 0; i < things.length; i++) {
things[i].style.outline = '1px solid red';
}
XPath selection based on text within element.
// for matching <element>foo bar baz</element> by 'bar'
var things = document.evaluate('//*[contains(text(),"bar")]',document,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);
for (var i = 0; i < things.snapshotLength; i++) {
things.snapshotItem(i).style.outline = '1px solid red';
}
And here's with case-insensitivity since text is more volatile:
// for matching <element>foo bar baz</element> by 'bar' case-insensitively
var things = document.evaluate('//*[contains(translate(text(),"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz"),"bar")]',document,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);
for (var i = 0; i < things.snapshotLength; i++) {
things.snapshotItem(i).style.outline = '1px solid red';
}
There are lots of great solutions here already. However, to provide a more streamlined solution and one more in keeping with the idea of a querySelector behavior and syntax, I opted for a solution that extends Object with a couple prototype functions. Both of these functions use regular expressions for matching text, however, a string can be provided as a loose search parameter.
Simply implement the following functions:
// find all elements with inner text matching a given regular expression
// args:
// selector: string query selector to use for identifying elements on which we
// should check innerText
// regex: A regular expression for matching innerText; if a string is provided,
// a case-insensitive search is performed for any element containing the string.
Object.prototype.queryInnerTextAll = function(selector, regex) {
if (typeof(regex) === 'string') regex = new RegExp(regex, 'i');
const elements = [...this.querySelectorAll(selector)];
const rtn = elements.filter((e)=>{
return e.innerText.match(regex);
});
return rtn.length === 0 ? null : rtn
}
// find the first element with inner text matching a given regular expression
// args:
// selector: string query selector to use for identifying elements on which we
// should check innerText
// regex: A regular expression for matching innerText; if a string is provided,
// a case-insensitive search is performed for any element containing the string.
Object.prototype.queryInnerText = function(selector, text){
return this.queryInnerTextAll(selector, text)[0];
}
With these functions implemented, you can now make calls as follows:
document.queryInnerTextAll('div.link', 'go');
This would find all divs containing the link class with the word go in the innerText (eg. Go Left or GO down or go right or It's Good)
document.queryInnerText('div.link', 'go');
This would work exactly as the example above except it would return only the first matching element.
document.queryInnerTextAll('a', /^Next$/);
Find all links with the exact text Next (case-sensitive). This will exclude links that contain the word Next along with other text.
document.queryInnerText('a', /next/i);
Find the first link that contains the word next, regardless of case (eg. Next Page or Go to next)
e = document.querySelector('#page');
e.queryInnerText('button', /Continue/);
This performs a search within a container element for a button containing the text, Continue (case-sensitive). (eg. Continue or Continue to Next but not continue)
I had similar problem.
Function that return all element which include text from arg.
This works for me:
function getElementsByText(document, str, tag = '*') {
return [...document.querySelectorAll(tag)]
.filter(
el => (el.text && el.text.includes(str))
|| (el.children.length === 0 && el.outerText && el.outerText.includes(str)))
}
Since there are no limits to the length of text in a data attribute, use data attributes! And then you can use regular css selectors to select your element(s) like the OP wants.
for (const element of document.querySelectorAll("*")) {
element.dataset.myInnerText = element.innerText;
}
document.querySelector("*[data-my-inner-text='Different text.']").style.color="blue";
<div>SomeText, text continues.</div>
<div>Different text.</div>
Ideally you do the data attribute setting part on document load and narrow down the querySelectorAll selector a bit for performance.
I was looking for a way to do something similar using a Regex, and decided to build something of my own that I wanted to share if others are looking for a similar solution.
function getElementsByTextContent(tag, regex) {
const results = Array.from(document.querySelectorAll(tag))
.reduce((acc, el) => {
if (el.textContent && el.textContent.match(regex) !== null) {
acc.push(el);
}
return acc;
}, []);
return results;
}
selectedContentWrap: HTML nodes.
htmlVarTag: is an string.
How do I check if the HTML element exists in the nodes?
The htmlVarTag is a string and don't understand how to convert it so it check again if there is a tag like that so that if there is I can remove it?
here is output of my nodes that is stored in selectedContentWrap
var checkingElement = $scope.checkIfHTMLinside(selectedContentWrap,htmlVarTag );
$scope.checkIfHTMLinside = function(selectedContentWrap,htmlVarTag){
var node = htmlVarTag.parentNode;
while (node != null) {
if (node == selectedContentWrap) {
return true;
}
node = node.parentNode;
}
return false;
}
Well if you could paste the content of selectedContentWrap I would be able to test this code, but I think this would work
// Code goes here
var checkIfHTMLinside = function(selectedContentWrap,htmlVarTag){
for (item of selectedContentWrap) {
if (item.nodeName.toLowerCase() == htmlVarTag.toLowerCase()){
return true;
}
}
return false;
}
Simplest is use angular.element which is a subset of jQuery compatible methods
$scope.checkIfHTMLinside = function(selectedContentWrap,htmlVarTag){
// use filter() on array and return filtered array length as boolean
return selectedContentWrap.filter(function(str){
// return length of tag collection found as boolean
return angular.element('<div>').append(str).find(htmlVarTag).length
}).length;
});
Still not 100% clear if objective is only to look for a specific tag or any tags (ie differentiate from text only)
Or as casually mentioned to actually remove the tag
If you want to remove the tag it's not clear if you simply want to unwrap it or remove it's content also ... both easily achieved using angular.element
Try using: node.innerHTML and checking against that
is it me or post a question on stackoverflow and 20min after test testing I figure it.,...
the answer is that in the selectedContentWrap I already got list of nodes, all I need to do i compare , so a simple if for loop will fit.
To compare the names I just need to use .nodeName as that works cross browser ( correct me if I am wrong)
Some dev say that "dictionary of tag names and anonymous closures instead" - but couldn't find anything. If anyone has this library could you please post it to the question?
here is my code.
var node = selectedContentWrap;
console.log('node that is selectedwrapper', selectedContentWrap)
for (var i = 0; i < selectedContentWrap.length; i++) {
console.log('tag name is ',selectedContentWrap[i].nodeName);
var temptagname = selectedContentWrap[i].nodeName; // for debugging
if(selectedContentWrap[i].nodeName == 'B' ){
console.log('contains element B');
}
}
As part of a larger script, I've been trying to make a page that would take a block of text from another function and "type" it out onto the screen:
function typeOut(page,nChar){
var txt = document.getElementById("text");
if (nChar<page.length){
txt.innerHTML = txt.innerHTML + page[nChar];
setTimeout(function () {typeOut(page,nChar+1);},20);
}
}
This basically works the way I want it to, but if the block of text I pass it has any html tags in it (like links), those show up as plain-text instead of being interpreted. Is there any way to get around that and force it to display the html elements correctly?
The problem is that you will create invalid HTML in the process, which the browser will try to correct. So apparently when you add < or >, it will automatically encode that character to not break the structure.
A proper solution would not work literally with every character of the text, but would process the HTML element by element. I.e. whenever you encounter an element in the source HTML, you would clone the element and add it to target element. Then you would process its text nodes character by character.
Here is a solution I hacked together (meaning, it can probably be improved a lot):
function typeOut(html, target) {
var d = document.createElement('div');
d.innerHTML = html;
var source = d.firstChild;
var i = 0;
(function process() {
if (source) {
if (source.nodeType === 3) { // process text node
if (i === 0) { // create new text node
target = target.appendChild(document.createTextNode(''));
target.nodeValue = source.nodeValue.charAt(i++);
// stop and continue to next node
} else if (i === source.nodeValue.length) {
if (source.nextSibling) {
source = source.nextSibling;
target = target.parentNode;
}
else {
source = source.parentNode.nextSibling;
target = target.parentNode.parentNode;
}
i = 0;
} else { // add to text node
target.nodeValue += source.nodeValue.charAt(i++);
}
} else if (source.nodeType === 1) { // clone element node
var clone = source.cloneNode();
clone.innerHTML = '';
target.appendChild(clone);
if (source.firstChild) {
source = source.firstChild;
target = clone;
} else {
source = source.nextSibling;
}
}
setTimeout(process, 20);
}
}());
}
DEMO
Your code should work. Example here : http://jsfiddle.net/hqKVe/2/
The issue is probably that the content of page[nChar] has HTML chars escaped.
The easiest solution is to use the html() function of jQuery (if you use jQuery). There a good example given by Canavar here : How to decode HTML entities using jQuery?
If you are not using jQuery, you have to unescape the string by yourself. In practice, just do the opposite of what is described here : Fastest method to escape HTML tags as HTML entities?
This question already has answers here:
Highlight search terms (select only leaf nodes)
(7 answers)
Closed 9 years ago.
I'd like a Javascript Regex to wrap a given list of of words in a given start (<span>) and end tag (i.e. </span>), but only if the word is actually "visible text" on the page, and not inside of an html attribute (such as a link's title tag, or inside of a <script></script> block.
I've created a JS Fiddle with the basics setup: http://jsfiddle.net/4YCR6/1/
HTML is too complex to reliably parse with a regular expression.
If you're looking to do this client-side, you can create a document fragment and/or disconnected DOM node (neither of which is displayed anywhere) and initialize it with your HTML string, then walk through the resulting DOM tree and process the text nodes. (Or use a library to help you do that, although it's actually quite simple.)
Here's a DOM walking example. This example is slightly simpler than your problem because it just updates the text, it doesn't add new elements to the structure (wrapping parts of the text in spans involves updating the structure), but it should get you going. Notes on what you'll need to change at the end.
var html =
"<p>This is a test.</p>" +
"<form><input type='text' value='test value'></form>" +
"<p class='testing test'>Testing here too</p>";
var frag = document.createDocumentFragment();
var body = document.createElement('body');
var node, next;
// Turn the HTML string into a DOM tree
body.innerHTML = html;
// Walk the dom looking for the given text in text nodes
walk(body);
// Insert the result into the current document via a fragment
node = body.firstChild;
while (node) {
next = node.nextSibling;
frag.appendChild(node);
node = next;
}
document.body.appendChild(frag);
// Our walker function
function walk(node) {
var child, next;
switch (node.nodeType) {
case 1: // Element
case 9: // Document
case 11: // Document fragment
child = node.firstChild;
while (child) {
next = child.nextSibling;
walk(child);
child = next;
}
break;
case 3: // Text node
handleText(node);
break;
}
}
function handleText(textNode) {
textNode.nodeValue = textNode.nodeValue.replace(/test/gi, "TEST");
}
Live example
The changes you'll need to make will be in handleText. Specifically, rather than updating nodeValue, you'll need to:
Find the index of the beginning of each word within the nodeValue string.
Use Node#splitText to split the text node into up to three text nodes (the part before your matching text, the part that is your matching text, and the part following your matching text).
Use document.createElement to create the new span (this is literally just span = document.createElement('span')).
Use Node#insertBefore to insert the new span in front of the third text node (the one containing the text following your matched text); it's okay if you didn't need to create a third node because your matched text was at the end of the text node, just pass in null as the refChild.
Use Node#appendChild to move the second text node (the one with the matching text) into the span. (No need to remove it from its parent first; appendChild does that for you.)
T.J. Crowder's answer is correct. I've gone a little further code-wise: here's a fully-formed example that works in all major browsers. I've posted variations of this code on Stack Overflow before (here and here, for example), and made it nice and generic so I (or anyone else) don't have to change it much to reuse it.
jsFiddle example: http://jsfiddle.net/7Vf5J/38/
Code:
// Reusable generic function
function surroundInElement(el, regex, surrounderCreateFunc) {
// script and style elements are left alone
if (!/^(script|style)$/.test(el.tagName)) {
var child = el.lastChild;
while (child) {
if (child.nodeType == 1) {
surroundInElement(child, regex, surrounderCreateFunc);
} else if (child.nodeType == 3) {
surroundMatchingText(child, regex, surrounderCreateFunc);
}
child = child.previousSibling;
}
}
}
// Reusable generic function
function surroundMatchingText(textNode, regex, surrounderCreateFunc) {
var parent = textNode.parentNode;
var result, surroundingNode, matchedTextNode, matchLength, matchedText;
while ( textNode && (result = regex.exec(textNode.data)) ) {
matchedTextNode = textNode.splitText(result.index);
matchedText = result[0];
matchLength = matchedText.length;
textNode = (matchedTextNode.length > matchLength) ?
matchedTextNode.splitText(matchLength) : null;
// Ensure searching starts at the beginning of the text node
regex.lastIndex = 0;
surroundingNode = surrounderCreateFunc(matchedTextNode.cloneNode(true));
parent.insertBefore(surroundingNode, matchedTextNode);
parent.removeChild(matchedTextNode);
}
}
// This function does the surrounding for every matched piece of text
// and can be customized to do what you like
function createSpan(matchedTextNode) {
var el = document.createElement("span");
el.style.color = "red";
el.appendChild(matchedTextNode);
return el;
}
// The main function
function wrapWords(container, words) {
// Replace the words one at a time to ensure "test2" gets matched
for (var i = 0, len = words.length; i < len; ++i) {
surroundInElement(container, new RegExp(words[i]), createSpan);
}
}
wrapWords(document.getElementById("container"), ["test2", "test"]);
I have a set of strings and I need to find all all of the occurrences in an HTML document. Where the string occurs is important because I need to handle each case differently:
String is all or part of an attribute. e.g., the string is foo: <input value="foo"> -> Add class ATTR to the element.
String is the full text of an element. e.g., <button>foo</button> -> Add class TEXT to the element.
String is inline in the text of an element. e.g., <p>I love foo</p> -> Wrap the text in a span tag with class TEXT.
Also, I need to match the longest string first. e.g., if I have foo and foobar, then <p>I love foobar</p> should become <p>I love <span class="TEXT">foobar</span></p>, not <p>I love <span class="TEXT">foo</span>bar</p>.
The inline text is easy enough: Sort the strings descending by length and find and replace each in document.body.innerHTML with <span class="TEXT">$1</span>, although I'm not sure if that is the most efficient way to go.
For the attributes, I can do something like this:
sortedStrings.each(function(it) {
document.body.innerHTML.replace(new RegExp('(\S+?)="[^"]*'+escapeRegExChars(it)+'[^"]*"','g'),function(s,attr) {
$('[+attr+'*='+it+']').addClass('ATTR');
});
});
Again, that seems inefficient.
Lastly, for the full text elements, a depth first search of the document that compares the innerHTML to each string will work, but for a large number of strings, it seems very inefficient.
Any answer that offers performance improvements gets an upvote :)
EDIT: I went with a modification on Bob's answer. delim is an optional delimiter around the string (to differentiate it from normal text), and keys is the list of strings.
function dfs(iterator,scope) {
scope = scope || document.body;
$(scope).children().each(function() {
return dfs(iterator,this);
});
return iterator.call(scope);
}
var escapeChars = /['\/.*+?|()[\]{}\\]/g;
function safe(text) {
return text.replace(escapeChars, '\\$1');
}
function eachKey(iterator) {
var key, lit, i, len, exp;
for(i = 0, len = keys.length; i < len; i++) {
key = keys[i].trim();
lit = (delim + key + delim);
exp = new RegExp(delim + '(' + safe(key) + ')' + delim,'g');
iterator(key,lit,exp);
}
}
$(function() {
keys = keys.sort(function(a,b) {
return b.length - a.length;
});
dfs(function() {
var a, attr, html, val, el = $(this);
eachKey(function(key,lit,exp) {
// check attributes
for(a in el[0].attributes) {
attr = el[0].attributes[a].nodeName;
val = el.attr(attr);
if(exp.test(val)) {
el.addClass(attrClass);
el.attr(attr,val.replace(exp,"$1"));
}
}
// check all content
html = el.html().trim();
if(html === lit) {
el.addClass(theClass);
el.html(key); // remove delims
} else if(exp.test(html)) {
// check partial content
el.html(html.replace(exp,wrapper));
}
});
});
});
Under the assumption that the traversal is the most expensive operation, this seems optimal, although improvements are still welcome.
Trying to parse HTML with regex is a mug's game. It simply can't handle even the basic strucures of HTML, never mind the quirks. There's so much wrong with your snippet already. (Doesn't detect unquoted attributes; fails for a wide variety of punctuation in it due to lack of HTML-escaping, regex-escaping or CSS-escaping(*); failure for attributes with - in; strange non-use of replace...)
So, use the DOM. Yes, that'll mean a traversal. But then so does a selector like the [attr*=] you're using already.
var needle= 'foo';
$('*').each(function() {
var tag= this.tagName.toLowerCase();
if (tag==='script' || tag==='style' || tag==='textarea' || tag==='option') return;
// Find text in attribute values
//
for (var attri= this.attributes.length; attri-->0;)
if (this.attributes[attri].value.indexOf(needle)!==-1)
$(this).addClass('ATTR');
// Find text in child text nodes
//
for (var childi= this.childNodes.length; childi-->0;) {
var child= this.childNodes[childi];
if (child.nodeType!=3) continue;
// Sole text content of parent: add class directly to parent
//
if (child.data==needle && element.childNodes.length===1) {
$(this).addClass('TEXT');
break;
}
// Else find index of each occurence in text, and wrap each in span
//
var parts= child.data.split(needle);
for (var parti= parts.length; parti-->1;) {
var span= document.createElement('span');
span.className= 'TEXT';
var ix= child.data.length-parts[parti].length;
var trail= child.splitText(ix);
span.appendChild(child.splitText(ix-needle.length));
this.insertBefore(span, trail);
}
}
});
(The reverse-loops are necessary as this is a destructive iteration of content.)
(*: escape doesn't do any of those things. It's more like URL-encoding, but it's not really that either. It's almost always the wrong thing; avoid.)
There is really no good way to do this. Your last requirement makes you have to traverse the entire dom.
for the first 2 requirements i would select all elements by tag name, and interate over them inserting the stuff as needed.
only performance improvement i can think of is to do this on the server side at all costs, this may even mean an extra post to have your faster server do the work, otherwise this can be really slow on say, IE6