<div class="title">
I am text node
<a class="edit">Edit</a>
</div>
I wish to get the "I am text node", do not wish to remove the "edit" tag, and need a cross browser solution.
var text = $(".title").contents().filter(function() {
return this.nodeType == Node.TEXT_NODE;
}).text();
This gets the contents of the selected element, and applies a filter function to it. The filter function returns only text nodes (i.e. those nodes with nodeType == Node.TEXT_NODE).
You can get the nodeValue of the first childNode using
$('.title')[0].childNodes[0].nodeValue
http://jsfiddle.net/TU4FB/
Another native JS solution that can be useful for "complex" or deeply nested elements is to use NodeIterator. Put NodeFilter.SHOW_TEXT as the second argument ("whatToShow"), and iterate over just the text node children of the element.
var root = document.querySelector('p'),
iter = document.createNodeIterator(root, NodeFilter.SHOW_TEXT),
textnode;
// print all text nodes
while (textnode = iter.nextNode()) {
console.log(textnode.textContent)
}
<p>
<br>some text<br>123
</p>
You can also use TreeWalker. The difference between the two is that NodeIterator is a simple linear iterator, while TreeWalker allows you to navigate via siblings and ancestors as well.
ES6 version that return the first #text node content
const extract = (node) => {
const text = [...node.childNodes].find(child => child.nodeType === Node.TEXT_NODE);
return text && text.textContent.trim();
}
If you mean get the value of the first text node in the element, this code will work:
var oDiv = document.getElementById("MyDiv");
var firstText = "";
for (var i = 0; i < oDiv.childNodes.length; i++) {
var curNode = oDiv.childNodes[i];
if (curNode.nodeName === "#text") {
firstText = curNode.nodeValue;
break;
}
}
You can see this in action here: http://jsfiddle.net/ZkjZJ/
Pure JavaScript: Minimalist
First off, always keep this in mind when looking for text in the DOM.
MDN - Whitespace in the DOM
This issue will make you pay attention to the structure of your XML / HTML.
In this pure JavaScript example, I account for the possibility of multiple text nodes that could be interleaved with other kinds of nodes. However, initially, I do not pass judgment on whitespace, leaving that filtering task to other code.
In this version, I pass a NodeList in from the calling / client code.
/**
* Gets strings from text nodes. Minimalist. Non-robust. Pre-test loop version.
* Generic, cross platform solution. No string filtering or conditioning.
*
* #author Anthony Rutledge
* #param nodeList The child nodes of a Node, as in node.childNodes.
* #param target A positive whole number >= 1
* #return String The text you targeted.
*/
function getText(nodeList, target)
{
var trueTarget = target - 1,
length = nodeList.length; // Because you may have many child nodes.
for (var i = 0; i < length; i++) {
if ((nodeList[i].nodeType === Node.TEXT_NODE) && (i === trueTarget)) {
return nodeList[i].nodeValue; // Done! No need to keep going.
}
}
return null;
}
Of course, by testing node.hasChildNodes() first, there would be no need to use a pre-test for loop.
/**
* Gets strings from text nodes. Minimalist. Non-robust. Post-test loop version.
* Generic, cross platform solution. No string filtering or conditioning.
*
* #author Anthony Rutledge
* #param nodeList The child nodes of a Node, as in node.childNodes.
* #param target A positive whole number >= 1
* #return String The text you targeted.
*/
function getText(nodeList, target)
{
var trueTarget = target - 1,
length = nodeList.length,
i = 0;
do {
if ((nodeList[i].nodeType === Node.TEXT_NODE) && (i === trueTarget)) {
return nodeList[i].nodeValue; // Done! No need to keep going.
}
i++;
} while (i < length);
return null;
}
Pure JavaScript: Robust
Here the function getTextById() uses two helper functions: getStringsFromChildren() and filterWhitespaceLines().
getStringsFromChildren()
/**
* Collects strings from child text nodes.
* Generic, cross platform solution. No string filtering or conditioning.
*
* #author Anthony Rutledge
* #version 7.0
* #param parentNode An instance of the Node interface, such as an Element. object.
* #return Array of strings, or null.
* #throws TypeError if the parentNode is not a Node object.
*/
function getStringsFromChildren(parentNode)
{
var strings = [],
nodeList,
length,
i = 0;
if (!parentNode instanceof Node) {
throw new TypeError("The parentNode parameter expects an instance of a Node.");
}
if (!parentNode.hasChildNodes()) {
return null; // We are done. Node may resemble <element></element>
}
nodeList = parentNode.childNodes;
length = nodeList.length;
do {
if ((nodeList[i].nodeType === Node.TEXT_NODE)) {
strings.push(nodeList[i].nodeValue);
}
i++;
} while (i < length);
if (strings.length > 0) {
return strings;
}
return null;
}
filterWhitespaceLines()
/**
* Filters an array of strings to remove whitespace lines.
* Generic, cross platform solution.
*
* #author Anthony Rutledge
* #version 6.0
* #param textArray a String associated with the id attribute of an Element.
* #return Array of strings that are not lines of whitespace, or null.
* #throws TypeError if the textArray param is not of type Array.
*/
function filterWhitespaceLines(textArray)
{
var filteredArray = [],
whitespaceLine = /(?:^\s+$)/; // Non-capturing Regular Expression.
if (!textArray instanceof Array) {
throw new TypeError("The textArray parameter expects an instance of a Array.");
}
for (var i = 0; i < textArray.length; i++) {
if (!whitespaceLine.test(textArray[i])) { // If it is not a line of whitespace.
filteredArray.push(textArray[i].trim()); // Trimming here is fine.
}
}
if (filteredArray.length > 0) {
return filteredArray ; // Leave selecting and joining strings for a specific implementation.
}
return null; // No text to return.
}
getTextById()
/**
* Gets strings from text nodes. Robust.
* Generic, cross platform solution.
*
* #author Anthony Rutledge
* #version 6.0
* #param id A String associated with the id property of an Element.
* #return Array of strings, or null.
* #throws TypeError if the id param is not of type String.
* #throws TypeError if the id param cannot be used to find a node by id.
*/
function getTextById(id)
{
var textArray = null; // The hopeful output.
var idDatatype = typeof id; // Only used in an TypeError message.
var node; // The parent node being examined.
try {
if (idDatatype !== "string") {
throw new TypeError("The id argument must be of type String! Got " + idDatatype);
}
node = document.getElementById(id);
if (node === null) {
throw new TypeError("No element found with the id: " + id);
}
textArray = getStringsFromChildren(node);
if (textArray === null) {
return null; // No text nodes found. Example: <element></element>
}
textArray = filterWhitespaceLines(textArray);
if (textArray.length > 0) {
return textArray; // Leave selecting and joining strings for a specific implementation.
}
} catch (e) {
console.log(e.message);
}
return null; // No text to return.
}
Next, the return value (Array, or null) is sent to the client code where it should be handled. Hopefully, the array should have string elements of real text, not lines of whitespace.
Empty strings ("") are not returned because you need a text node to properly indicate the presence of valid text. Returning ("") may give the false impression that a text node exists, leading someone to assume that they can alter the text by changing the value of .nodeValue. This is false, because a text node does not exist in the case of an empty string.
Example 1:
<p id="bio"></p> <!-- There is no text node here. Return null. -->
Example 2:
<p id="bio">
</p> <!-- There are at least two text nodes ("\n"), here. -->
The problem comes in when you want to make your HTML easy to read by spacing it out. Now, even though there is no human readable valid text, there are still text nodes with newline ("\n") characters in their .nodeValue properties.
Humans see examples one and two as functionally equivalent--empty elements waiting to be filled. The DOM is different than human reasoning. This is why the getStringsFromChildren() function must determine if text nodes exist and gather the .nodeValue values into an array.
for (var i = 0; i < length; i++) {
if (nodeList[i].nodeType === Node.TEXT_NODE) {
textNodes.push(nodeList[i].nodeValue);
}
}
In example two, two text nodes do exist and getStringFromChildren() will return the .nodeValue of both of them ("\n"). However, filterWhitespaceLines() uses a regular expression to filter out lines of pure whitespace characters.
Is returning null instead of newline ("\n") characters a form of lying to the client / calling code? In human terms, no. In DOM terms, yes. However, the issue here is getting text, not editing it. There is no human text to return to the calling code.
One can never know how many newline characters might appear in someone's HTML. Creating a counter that looks for the "second" newline character is unreliable. It might not exist.
Of course, further down the line, the issue of editing text in an empty <p></p> element with extra whitespace (example 2) might mean destroying (maybe, skipping) all but one text node between a paragraph's tags to ensure the element contains precisely what it is supposed to display.
Regardless, except for cases where you are doing something extraordinary, you will need a way to determine which text node's .nodeValue property has the true, human readable text that you want to edit. filterWhitespaceLines gets us half way there.
var whitespaceLine = /(?:^\s+$)/; // Non-capturing Regular Expression.
for (var i = 0; i < filteredTextArray.length; i++) {
if (!whitespaceLine.test(textArray[i])) { // If it is not a line of whitespace.
filteredTextArray.push(textArray[i].trim()); // Trimming here is fine.
}
}
At this point you may have output that looks like this:
["Dealing with text nodes is fun.", "Some people just use jQuery."]
There is no guarantee that these two strings are adjacent to each other in the DOM, so joining them with .join() might make an unnatural composite. Instead, in the code that calls getTextById(), you need to chose which string you want to work with.
Test the output.
try {
var strings = getTextById("bio");
if (strings === null) {
// Do something.
} else if (strings.length === 1) {
// Do something with strings[0]
} else { // Could be another else if
// Do something. It all depends on the context.
}
} catch (e) {
console.log(e.message);
}
One could add .trim() inside of getStringsFromChildren() to get rid of leading and trailing whitespace (or to turn a bunch of spaces into a zero length string (""), but how can you know a priori what every application may need to have happen to the text (string) once it is found? You don't, so leave that to a specific implementation, and let getStringsFromChildren() be generic.
There may be times when this level of specificity (the target and such) is not required. That is great. Use a simple solution in those cases. However, a generalized algorithm enables you to accommodate simple and complex situations.
.text() - for jquery
$('.title').clone() //clone the element
.children() //select all the children
.remove() //remove all the children
.end() //again go back to selected element
.text(); //get the text of element
This will ignore the whitespace as well so, your never got the Blank textNodes..code using core Javascript.
var oDiv = document.getElementById("MyDiv");
var firstText = "";
for (var i = 0; i < oDiv.childNodes.length; i++) {
var curNode = oDiv.childNodes[i];
whitespace = /^\s*$/;
if (curNode.nodeName === "#text" && !(whitespace.test(curNode.nodeValue))) {
firstText = curNode.nodeValue;
break;
}
}
Check it on jsfiddle : - http://jsfiddle.net/webx/ZhLep/
Simply via Vanilla JavaScript:
const el = document.querySelector('.title');
const text = el.firstChild.textContent.trim();
You can also use XPath's text() node test to get the text nodes only. For example
var target = document.querySelector('div.title');
var iter = document.evaluate('text()', target, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
var node;
var want = '';
while (node = iter.iterateNext()) {
want += node.data;
}
There are some overcomplicated solutions here but the operation is as straightforward as using .childNodes to get children of all node types and .filter to extract e.nodeType === Node.TEXT_NODEs. Optionally, we may want to do it recursively and/or ignore "empty" text nodes (all whitespace).
These examples convert the nodes to their text content for display purposes, but this is technically a separate step from filtering.
const immediateTextNodes = el =>
[...el.childNodes].filter(e => e.nodeType === Node.TEXT_NODE);
const immediateNonEmptyTextNodes = el =>
[...el.childNodes].filter(e =>
e.nodeType === Node.TEXT_NODE && e.textContent.trim()
);
const firstImmediateTextNode = el =>
[...el.childNodes].find(e => e.nodeType === Node.TEXT_NODE);
const firstImmediateNonEmptyTextNode = el =>
[...el.childNodes].find(e =>
e.nodeType === Node.TEXT_NODE && e.textContent.trim()
);
// example usage:
const text = el => el.textContent;
const p = document.querySelector("p");
console.log(immediateTextNodes(p).map(text));
console.log(immediateNonEmptyTextNodes(p).map(text));
console.log(text(firstImmediateTextNode(p)));
console.log(text(firstImmediateNonEmptyTextNode(p)));
// if you want to trim whitespace:
console.log(immediateNonEmptyTextNodes(p).map(e => text(e).trim()));
<p>
<span>IGNORE</span>
<b>IGNORE</b>
foo
<br>
bar
</p>
Recursive alternative to a NodeIterator:
const deepTextNodes = el => [...el.childNodes].flatMap(e =>
e.nodeType === Node.TEXT_NODE ? e : deepTextNodes(e)
);
const deepNonEmptyTextNodes = el =>
[...el.childNodes].flatMap(e =>
e.nodeType === Node.TEXT_NODE && e.textContent.trim()
? e : deepNonEmptyTextNodes(e)
);
// example usage:
const text = el => el.textContent;
const p = document.querySelector("p");
console.log(deepTextNodes(p).map(text));
console.log(deepNonEmptyTextNodes(p).map(text));
<p>
foo
<span>bar</span>
baz
<span><b>quux</b></span>
</p>
Finally, feel free to join the text node array into a string if you wish using .join(""). But as with trimming and text content extraction, I'd probably not bake this into the core filtering function and leave it to the caller to handle as needed.
Related
I want to insert html tags within a text node with TreeWalker, but TreeWalker forces my html brackets into & lt; & gt; no matter what I've tried. Here is the code:
var text;
var tree = document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT);
while (tree.nextNode()) {
text = tree.currentNode.nodeValue;
text = text.replace(/(\W)(\w+)/g, '$1<element onmouseover="sendWord(\'$2\')">$2</element>');
text = text.replace(/^(\w+)/, '<element onmouseover="sendWord(\'$1\')">$1</element>');
tree.currentNode.nodeValue = text;
}
Using \< or " instead of ' won't help. My workaround is to copy all of the DOM tree to a string and to replace the html body with that. It works on very simple webpages and solves my first problem, but is a bad hack and won't work on anything more than a trivial page. I was wondering if I could just work straight with the text node rather than use a workaround. Here is the code for the (currently buggy) workaround:
var text;
var newHTML = "";
var tree = document.createTreeWalker(document.body);
while (tree.nextNode()) {
text = tree.currentNode.nodeValue;
if (tree.currentNode.nodeType == 3){
text = text.replace(/(\W)(\w+)/g, '$1<element onmouseover="sendWord(\'$2\')">$2</element>');
text = text.replace(/^(\w+)/, '<element onmouseover="sendWord(\'$1\')">$1</element>');
}
newHTML += text
}
document.body.innerHTML = newHTML;
Edit: I realize a better workaround would be to custom tag the text nodes ((Customtag_Start_Here) etc.), copy the whole DOM to a string, and use my customs tags to identify text nodes and modify them that way. But if I don't have to, I'd rather not.
To 'change' a text node into an element, you must replace it with an element. For example:
var text = tree.currentNode;
var el = document.createElement('foo');
el.setAttribute('bar','yes');
text.parentNode.replaceChild( el, text );
If you want to retain part of the text node, and inject an element "in the middle", you need to create another text node and insert it and the element into the tree at the appropriate places in the tree.
Edit: Here's a function that might be super useful to you. :)
Given a text node, it runs a regex on the text values. For each hit that it finds it calls a custom function that you supply. If that function returns a string, then the match is replaced. However, if that function returns an object like:
{ name:"element", attrs{onmouseover:"sendWord('foo')"}, content:"foo" }
then it will split the text node around the match and inject an element in that location. You can also return an array of strings or those objects (and can recursively use arrays, strings, or objects as the content property).
Demo: http://jsfiddle.net/DpqGH/8/
function textNodeReplace(node,regex,handler) {
var mom=node.parentNode, nxt=node.nextSibling,
doc=node.ownerDocument, hits;
if (regex.global) {
while(node && (hits=regex.exec(node.nodeValue))){
regex.lastIndex = 0;
node=handleResult( node, hits, handler.apply(this,hits) );
}
} else if (hits=regex.exec(node.nodeValue))
handleResult( node, hits, handler.apply(this,hits) );
function handleResult(node,hits,results){
var orig = node.nodeValue;
node.nodeValue = orig.slice(0,hits.index);
[].concat(create(mom,results)).forEach(function(n){
mom.insertBefore(n,nxt);
});
var rest = orig.slice(hits.index+hits[0].length);
return rest && mom.insertBefore(doc.createTextNode(rest),nxt);
}
function create(el,o){
if (o.map) return o.map(function(v){ return create(el,v) });
else if (typeof o==='object') {
var e = doc.createElementNS(o.namespaceURI || el.namespaceURI,o.name);
if (o.attrs) for (var a in o.attrs) e.setAttribute(a,o.attrs[a]);
if (o.content) [].concat(create(e,o.content)).forEach(e.appendChild,e);
return e;
} else return doc.createTextNode(o+"");
}
}
It's not quite perfectly generic, as it does not support namespaces on attributes. But hopefully it's enough to get you going. :)
You would use it like so:
findAllTextNodes(document.body).forEach(function(textNode){
replaceTextNode( textNode, /\b\w+/g, function(match){
return {
name:'element',
attrs:{onmouseover:"sendWord('"+match[0]+"')"},
content:match[0]
};
});
});
function findAllTextNodes(node){
var walker = node.ownerDocument.createTreeWalker(node,NodeFilter.SHOW_TEXT);
var textNodes = [];
while (walker.nextNode())
if (walker.currentNode.parentNode.tagName!='SCRIPT')
textNodes.push(walker.currentNode);
return textNodes;
}
or if you want something closer to your original regex:
replaceTextNode( textNode, /(^|\W)(\w+)/g, function(match){
return [
match[1], // might be an empty string
{
name:'element',
attrs:{onmouseover:"sendWord('"+match[2]+"')"},
content:match[2]
}
];
});
Function that returns the parent element of any text node including partial match of passed string:
function findElByText(text, mainNode) {
let textEl = null;
const traverseNodes = function (n) {
if (textEl) {
return;
}
for (var nodes = n.childNodes, i = nodes.length; i--;) {
if (textEl) {
break;
}
var n = nodes[i], nodeType = n.nodeType;
// Its a text node, check if it matches string
if (nodeType == 3) {
if (n.textContent.includes(text)) {
textEl = n.parentElement;
break;
}
}
else if (nodeType == 1 || nodeType == 9 || nodeType == 11) {
traverseNodes(n);
}
}
}
traverseNodes(mainNode);
return textEl;
}
Usage:
findElByText('Some string in document', document.body);
I'm wondering if there is a lightweight way I could use JavaScript or jQuery to sniff out a specific text character across a document; say € and find all instances of this character. And then! Write an ability to replace all instances of this with say a $.
I found this snippet for starters:
var str = 'test: '';
str = str.replace(/'/g, "'");
Essentially; I am wanting a solution for a one page document. Grab all instances of X and make it XY. Only text characters.
How about this, replacing # with $:
$("body").children().each(function () {
$(this).html( $(this).html().replace(/#/g,"$") );
});
http://jsfiddle.net/maximua/jp96C/1/
ECMAScript 2015+ approach
Pitfalls when solving this task
This seems like an easy task, but you have to take care of several things:
Simply replacing the entire HTML (e.g. using innerHTML) causes the affected subtree of the DOM to be entirely deleted and replaced, however event listeners are attached to the existing, now deleted elements, so they’re deleted with them. Similarly, WeakMap entries for the existing elements will all be deleted. This is because all of these things need the exact references to the elements or nodes; a replaced innerHTML will create entirely new references and discard the old ones.
Replacing the HTML may also replace <script> or <style> contents, or HTML tag or attribute names, which is not always desired.
Changing the HTML may result in an xss attack.
You may want to replace attribute values, e.g. for title and alt, in a controlled manner as well, but those all-or-nothing approaches as well as regexes are ill-equipped to do so.
Guarding against xss attacks generally can’t be solved by using the approaches below. E.g. if a fetch call reads a URL from somewhere on the page, then sends a request to that URL, the functions below won’t stop that, since this scenario is inherently unsafe.
Replacing the text contents of all elements
This basically selects all elements that contain normal text, goes through their child nodes — among those are also text nodes —, seeks those text nodes out and replaces their contents.
You can optionally specify a different root target, e.g. replaceOnDocument(/€/g, "$", { target: someElement });; by default, the <body> is chosen.
const replaceOnDocument = (pattern, string, {target = document.body} = {}) => {
// Handle `string` — see the last section
[
target,
...target.querySelectorAll("*:not(script):not(noscript):not(style)")
].forEach(({childNodes: [...nodes]}) => nodes
.filter(({nodeType}) => nodeType === Node.TEXT_NODE)
.forEach((textNode) => textNode.textContent = textNode.textContent.replace(pattern, string)));
};
replaceOnDocument(/€/g, "$");
Replacing text nodes, element attributes and properties
Now, this is a little more complex: you need to check three cases: whether a node is a text node, whether it’s an element and its attribute should be replaced, or whether it’s an element and its property should be replaced. A replacer object provides methods for text nodes and for elements.
Before replacing attributes and properties, the replacer needs to check whether the element has a matching attribute; otherwise new attributes get created, undesirably. It also needs to check whether the targeted property is a string, since only strings can be replaced, or whether the matching property to the targeted attribute is not a function, since this may lead to an xss attack.
In the example below, you can see how to use the extended features: in the optional third argument, you may add an attrs property and a props property, which is an iterable (e.g. an array) each, for the attributes to be replaced and the properties to be replaced, respectively.
You’ll also notice that this snippet uses flatMap. If that’s not supported, use a polyfill or replace it by the reduce–concat, or map–reduce–concat construct, as seen in the linked documentation.
const replaceOnDocument = (() => {
const replacer = {
[Node.TEXT_NODE](node, pattern, string){
node.textContent = node.textContent.replace(pattern, string);
},
[Node.ELEMENT_NODE](node, pattern, string, {attrs, props} = {}){
attrs.forEach((attr) => {
if(typeof node[attr] !== "function" && node.hasAttribute(attr)){
node.setAttribute(attr, node.getAttribute(attr).replace(pattern, string));
}
});
props.forEach((prop) => {
if(typeof node[prop] === "string" && node.hasAttribute(prop)){
node[prop] = node[prop].replace(pattern, string);
}
});
}
};
return (pattern, string, {target = document.body, attrs: [...attrs] = [], props: [...props] = []} = {}) => {
// Handle `string` — see the last section
[
target,
...[
target,
...target.querySelectorAll("*:not(script):not(noscript):not(style)")
].flatMap(({childNodes: [...nodes]}) => nodes)
].filter(({nodeType}) => replacer.hasOwnProperty(nodeType))
.forEach((node) => replacer[node.nodeType](node, pattern, string, {
attrs,
props
}));
};
})();
replaceOnDocument(/€/g, "$", {
attrs: [
"title",
"alt",
"onerror" // This will be ignored
],
props: [
"value" // Changing an `<input>`’s `value` attribute won’t change its current value, so the property needs to be accessed here
]
});
Replacing with HTML entities
If you need to make it work with HTML entities like , the above approaches will just literally produce the string , since that’s an HTML entity and will only work when assigning .innerHTML or using related methods.
So let’s solve it by passing the input string to something that accepts an HTML string: a new, temporary HTMLDocument. This is created by the DOMParser’s parseFromString method; in the end we read its documentElement’s textContent:
string = new DOMParser().parseFromString(string, "text/html").documentElement.textContent;
If you want to use this, choose one of the approaches above, depending on whether or not you want to replace HTML attributes and DOM properties in addition to text; then simply replace the comment // Handle `string` — see the last section by the above line.
Now you can use replaceOnDocument(/Güterzug/g, "Güterzug");.
NB: If you don’t use the string handling code, you may also remove the { } around the arrow function body.
Note that this parses HTML entities but still disallows inserting actual HTML tags, since we’re reading only the textContent. This is also safe against most cases of xss: since we’re using parseFromString and the page’s document isn’t affected, no <script> gets downloaded and no onerror handler gets executed.
You should also consider using \xAD instead of directly in your JavaScript string, if it turns out to be simpler.
My own suggestion is as follows:
function nativeSelector() {
var elements = document.querySelectorAll("body, body *");
var results = [];
var child;
for(var i = 0; i < elements.length; i++) {
child = elements[i].childNodes[0];
if(elements[i].hasChildNodes() && child.nodeType == 3) {
results.push(child);
}
}
return results;
}
var textnodes = nativeSelector(),
_nv;
for (var i = 0, len = textnodes.length; i<len; i++){
_nv = textnodes[i].nodeValue;
textnodes[i].nodeValue = _nv.replace(/£/g,'€');
}
JS Fiddle demo.
The nativeSelector() function comes from an answer (posted by Anurag) to this question: getElementsByTagName() equivalent for textNodes.
I think you may be overthinking this.
My approach is simple.
Enclose you page with a div tag:
<div id="mydiv">
<!-- you page here -->
</div>
In your javascript:
var html=document.getElementById('mydiv').innerHTML;
html = html.replace(/this/g,"that");
document.getElementById('mydiv').innerHTML=html;
Similar to #max-malik's answer, but without using jQuery, you can also do this using document.createTreeWalker:
button.addEventListener('click', e => {
const treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
while (treeWalker.nextNode()) {
const node = treeWalker.currentNode;
node.textContent = node.textContent.replace(/#/g, '$');
}
})
<div>This is an # that we are # replacing.</div>
<div>This is another # that we are replacing.</div>
<div>
<span>This is an # in a span in # div.</span>
</div>
<br>
<input id="button" type="button" value="Replace # with $" />
Vanilla JavaScript solution:
document.body.innerHTML = document.body.innerHTML.replace(/Original/g, "New")
The best would be to do this server-side or wrap the currency symbols in an element you can select before returning it to the browser, however if neither is an option, you can select all text nodes within the body and do the replace on them. Below i'm doing this using a plugin i wrote 2 years ago that was meant for highlighting text. What i'm doing is finding all occurrences of € and wrapping it in a span with the class currency-symbol, then i'm replacing the text of those spans.
Demo
(function($){
$.fn.highlightText = function () {
// handler first parameter
// is the first parameter a regexp?
var re,
hClass,
reStr,
argType = $.type(arguments[0]),
defaultTagName = $.fn.highlightText.defaultTagName;
if ( argType === "regexp" ) {
// first argument is a regular expression
re = arguments[0];
}
// is the first parameter an array?
else if ( argType === "array" ) {
// first argument is an array, generate
// regular expression string for later use
reStr = arguments[0].join("|");
}
// is the first parameter a string?
else if ( argType === "string" ) {
// store string in regular expression string
// for later use
reStr = arguments[0];
}
// else, return out and do nothing because this
// argument is required.
else {
return;
}
// the second parameter is optional, however,
// it must be a string or boolean value. If it is
// a string, it will be used as the highlight class.
// If it is a boolean value and equal to true, it
// will be used as the third parameter and the highlight
// class will default to "highlight". If it is undefined,
// the highlight class will default to "highlight" and
// the third parameter will default to false, allowing
// the plugin to match partial matches.
// ** The exception is if the first parameter is a regular
// expression, the third parameter will be ignored.
argType = $.type(arguments[1]);
if ( argType === "string" ) {
hClass = arguments[1];
}
else if ( argType === "boolean" ) {
hClass = "highlight";
if ( reStr ) {
reStr = "\\b" + reStr + "\\b";
}
}
else {
hClass = "highlight";
}
if ( arguments[2] && reStr ) {
reStr = reStr = "\\b" + reStr + "\\b";
}
// if re is not defined ( which means either an array or
// string was passed as the first parameter ) create the
// regular expression.
if (!re) {
re = new RegExp( "(" + reStr + ")", "ig" );
}
// iterate through each matched element
return this.each( function() {
// select all contents of this element
$( this ).find( "*" ).andSelf().contents()
// filter to only text nodes that aren't already highlighted
.filter( function () {
return this.nodeType === 3 && $( this ).closest( "." + hClass ).length === 0;
})
// loop through each text node
.each( function () {
var output;
output = this.nodeValue
.replace( re, "<" + defaultTagName + " class='" + hClass + "'>$1</" + defaultTagName +">" );
if ( output !== this.nodeValue ) {
$( this ).wrap( "<p></p>" ).parent()
.html( output ).contents().unwrap();
}
});
});
};
$.fn.highlightText.defaultTagName = "span";
})( jQuery );
$("body").highlightText("€","currency-symbol");
$("span.currency-symbol").text("$");
Use split and join method
$("#idBut").click(function() {
$("body").children().each(function() {
$(this).html($(this).html().split('#').join("$"));
});
});
here is solution
In javascript without using jquery:
document.body.innerText = document.body.innerText.replace('actualword', 'replacementword');
You can use:
str.replace(/text/g, "replaced text");
For each element inside document body modify their text using .text(fn) function.
$("body *").text(function() {
return $(this).text().replace("x", "xy");
});
As you'll be using jQuery anyway, try:
https://github.com/cowboy/jquery-replacetext
Then just do
$("p").replaceText("£", "$")
It seems to do good job of only replacing text and not messing with other elements
str.replace(/replacetext/g,'actualtext')
This replaces all instances of replacetext with actualtext
Here is something that might help someone looking for this answer:
The following uses jquery it searches the whole document and only replaces the text.
for example if we had
overpopulation
and we wanted to add a span with the class overpop around the word overpopulation
<span class="overpop">overpopulation</span>
we would run the following
$("*:containsIN('overpopulation')").filter(
function() {
return $(this).find("*:contains('" + str + "')").length == 0
}
).html(function(_, html) {
if (html != 'undefined') {
return html.replace(/(overpopulation)/gi, '<span class="overpop">$1</span>');
}
});
the search is case insensitive searches the whole document and only replaces the text portions in this case we are searching for the string 'overpopulation'
$.extend($.expr[":"], {
"containsIN": function(elem, i, match, array) {
return (elem.textContent || elem.innerText || "").toLowerCase().indexOf((match[3] || "").toLowerCase()) >= 0;
}
});
I'm trying to write a jQuery or pure Javascript function (preferring the more readable solution) that can count the length of a starting tag or ending tag in an HTML document.
For example,
<p>Hello.</p>
would return 3 and 4 for the starting and ending tag lengths. Adding attributes,
<span class="red">Warning!</span>
would return 18 and 7 for the starting and ending tag lengths. Finally,
<img src="foobar.png"/>
would return 23 and 0 (or -1) for the starting and ending tag lengths.
I'm looking for a canonical, guaranteed-to-work-according-to-spec solution, so I'm trying to use DOM methods rather than manual text manipulations. For example, I would like the solution to work even for weird cases like
<p>spaces infiltrating the ending tag</ p >
and
<img alt="unended singleton tags" src="foobar.png">
and such. That is, my hope is that as long as we use proper DOM methods, we should be able to find the number of characters between < and > no matter how weird things get, even
<div data-tag="<div>">HTML-like strings within attributes</div>
I have looked at the jQuery API (especially the Manipulation section, including DOM Insertion and General Attributes subsections), but I don't see anything that would help.
Currently the best idea I have, given an element node is
lengthOfEndTag = node.tagName.length + 3;
lengthOfStartTag = node.outerHTML.length
- node.innerHTML.length
- lengthOfEndTag;
but of course I don't want to make such an assumption for the end tag.
(Finally, I'm familiar with regular expressions—but trying to avoid them if at all possible.)
EDIT
#Pointy and #squint helped me understand that it's not possible to see </ p >, for example, because the HTML is discarded once the DOM is created. That's fine. The objective, adjusted, is to find the length of the start and end tags as would be rendered in outerHTML.
An alternate way to do this could be to use XMLSerializer's serializeToString on a clone copy of the node (with id set) to avoid having to parse innerHTML, then split over "><"
var tags = (function () {
var x = new XMLSerializer(); // scope this so it doesn't need to be remade
return function tags(elm) {
var s, a, id, n, o = {open: null, close: null}; // spell stuff with var
if (elm.nodeType !== 1) throw new TypeError('Expected HTMLElement');
n = elm.cloneNode(); // clone to get rid of innerHTML
id = elm.getAttribute('id'); // re-apply id for clone
if (id !== null) n.setAttribute('id', id); // if it was set
s = x.serializeToString(n); // serialise
a = s.split('><');
if (a.length > 1) { // has close tag
o.close = '<' + a.pop();
o.open = a.join('><') + '>'; // join "just in case"
}
else o.open = a[0]; // no close tag
return o;
}
}()); // self invoke to init
After running this, you can access .length of open and close properties
tags(document.body); // {open: "<body class="question-page">", close: "</body>"}
What if an attribute's value has >< in it? XMLSerializer escapes this to >< so it won't change the .split.
What about no close tag? close will be null.
This answer helped me understand what #Pointy and #squint were trying to say.
The following solution works for me:
$.fn.lengthOfStartTag = function () {
var node = this[0];
if (!node || node.nodeType != 1) {
$.error("Called $.fn.lengthOfStartTag on non-element node.");
}
if (!$(node).is(":empty")) {
return node.outerHTML.indexOf(node.innerHTML);
}
return node.outerHTML.length;
}
$.fn.lengthOfEndTag = function () {
var node = this[0];
if (!node || node.nodeType != 1) {
$.error("Called $.fn.lengthOfEndTag on non-element node.");
}
if (!$(node).is(":empty")) {
var indexOfInnerHTML = node.outerHTML.indexOf(node.innerHTML);
return node.outerHTML.length - (indexOfInnerHTML + node.innerHTML.length);
}
return -1;
}
Sample jsFiddle here.
Many tools/APIs provide ways of selecting elements of specific classes or IDs. There's also possible to inspect the raw stylesheets loaded by the browser.
However, for browsers to render an element, they'll compile all CSS rules (possibly from different stylesheet files) and apply it to the element. This is what you see with Firebug or the WebKit Inspector - the full CSS inheritance tree for an element.
How can I reproduce this feature in pure JavaScript without requiring additional browser plugins?
Perhaps an example can provide some clarification for what I'm looking for:
<style type="text/css">
p { color :red; }
#description { font-size: 20px; }
</style>
<p id="description">Lorem ipsum</p>
Here the p#description element have two CSS rules applied: a red color and a font size of 20 px.
I would like to find the source from where these computed CSS rules originate from (color comes the p rule and so on).
Since this question currently doesn't have a lightweight (non-library), cross-browser compatible answer, I'll try to provide one:
function css(el) {
var sheets = document.styleSheets, ret = [];
el.matches = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector
|| el.msMatchesSelector || el.oMatchesSelector;
for (var i in sheets) {
var rules = sheets[i].rules || sheets[i].cssRules;
for (var r in rules) {
if (el.matches(rules[r].selectorText)) {
ret.push(rules[r].cssText);
}
}
}
return ret;
}
JSFiddle: http://jsfiddle.net/HP326/6/
Calling css(document.getElementById('elementId')) will return an array with an element for each CSS rule that matches the passed element.
If you want to find out more specific information about each rule, check out the CSSRule object documentation.
Short version12 April 2017
Challenger appears.
var getMatchedCSSRules = (el, css = el.ownerDocument.styleSheets) =>
[].concat(...[...css].map(s => [...s.cssRules||[]])) /* 1 */
.filter(r => el.matches(r.selectorText)); /* 2 */
Line /* 1 */ builds a flat array of all rules.
Line /* 2 */ discards non-matching rules.
Based on function css(el) by #S.B. on the same page.
Example 1
var div = iframedoc.querySelector("#myelement");
var rules = getMatchedCSSRules(div, iframedoc.styleSheets);
console.log(rules[0].parentStyleSheet.ownerNode, rules[0].cssText);
Example 2
var getMatchedCSSRules = (el, css = el.ownerDocument.styleSheets) =>
[].concat(...[...css].map(s => [...s.cssRules||[]]))
.filter(r => el.matches(r.selectorText));
function Go(big,show) {
var r = getMatchedCSSRules(big);
PrintInfo:
var f = (dd,rr,ee="\n") => dd + rr.cssText.slice(0,50) + ee;
show.value += "--------------- Rules: ----------------\n";
show.value += f("Rule 1: ", r[0]);
show.value += f("Rule 2: ", r[1]);
show.value += f("Inline: ", big.style);
show.value += f("Computed: ", getComputedStyle(big), "(…)\n");
show.value += "-------- Style element (HTML): --------\n";
show.value += r[0].parentStyleSheet.ownerNode.outerHTML;
}
Go(...document.querySelectorAll("#big,#show"));
.red {color: red;}
#big {font-size: 20px;}
<h3 id="big" class="red" style="margin: 0">Lorem ipsum</h3>
<textarea id="show" cols="70" rows="10"></textarea>
Shortcomings
No media handling, no #import, #media.
No access to styles loaded from cross-domain stylesheets.
No sorting by selector “specificity” (order of importance).
No styles inherited from parents.
May not work with old or rudimentary browsers.
Not sure how it copes with pseudo-classes and pseudo-selectors but seems to fare okay.
Maybe I will address these shortcomings one day.
Long version12 August 2018
Here’s a much more comprehensive implementation taken from someone’s GitHub page
(forked from this original code, via Bugzilla). Written for Gecko and IE, but is rumoured to work also with Blink.
4 May 2017: The specificity calculator has had critical bugs which I have now fixed. (I can’t notify the authors because I don’t have a GitHub account.)
12 August 2018: Recent Chrome updates seem to have decoupled object scope (this) from methods assigned to independent variables. Therefore invocation matcher(selector) has stopped working. Replacing it by matcher.call(el, selector) has solved it.
// polyfill window.getMatchedCSSRules() in FireFox 6+
if (typeof window.getMatchedCSSRules !== 'function') {
var ELEMENT_RE = /[\w-]+/g,
ID_RE = /#[\w-]+/g,
CLASS_RE = /\.[\w-]+/g,
ATTR_RE = /\[[^\]]+\]/g,
// :not() pseudo-class does not add to specificity, but its content does as if it was outside it
PSEUDO_CLASSES_RE = /\:(?!not)[\w-]+(\(.*\))?/g,
PSEUDO_ELEMENTS_RE = /\:\:?(after|before|first-letter|first-line|selection)/g;
// convert an array-like object to array
function toArray(list) {
return [].slice.call(list);
}
// handles extraction of `cssRules` as an `Array` from a stylesheet or something that behaves the same
function getSheetRules(stylesheet) {
var sheet_media = stylesheet.media && stylesheet.media.mediaText;
// if this sheet is disabled skip it
if ( stylesheet.disabled ) return [];
// if this sheet's media is specified and doesn't match the viewport then skip it
if ( sheet_media && sheet_media.length && ! window.matchMedia(sheet_media).matches ) return [];
// get the style rules of this sheet
return toArray(stylesheet.cssRules);
}
function _find(string, re) {
var matches = string.match(re);
return matches ? matches.length : 0;
}
// calculates the specificity of a given `selector`
function calculateScore(selector) {
var score = [0,0,0],
parts = selector.split(' '),
part, match;
//TODO: clean the ':not' part since the last ELEMENT_RE will pick it up
while (part = parts.shift(), typeof part == 'string') {
// find all pseudo-elements
match = _find(part, PSEUDO_ELEMENTS_RE);
score[2] += match;
// and remove them
match && (part = part.replace(PSEUDO_ELEMENTS_RE, ''));
// find all pseudo-classes
match = _find(part, PSEUDO_CLASSES_RE);
score[1] += match;
// and remove them
match && (part = part.replace(PSEUDO_CLASSES_RE, ''));
// find all attributes
match = _find(part, ATTR_RE);
score[1] += match;
// and remove them
match && (part = part.replace(ATTR_RE, ''));
// find all IDs
match = _find(part, ID_RE);
score[0] += match;
// and remove them
match && (part = part.replace(ID_RE, ''));
// find all classes
match = _find(part, CLASS_RE);
score[1] += match;
// and remove them
match && (part = part.replace(CLASS_RE, ''));
// find all elements
score[2] += _find(part, ELEMENT_RE);
}
return parseInt(score.join(''), 10);
}
// returns the heights possible specificity score an element can get from a give rule's selectorText
function getSpecificityScore(element, selector_text) {
var selectors = selector_text.split(','),
selector, score, result = 0;
while (selector = selectors.shift()) {
if (matchesSelector(element, selector)) {
score = calculateScore(selector);
result = score > result ? score : result;
}
}
return result;
}
function sortBySpecificity(element, rules) {
// comparing function that sorts CSSStyleRules according to specificity of their `selectorText`
function compareSpecificity (a, b) {
return getSpecificityScore(element, b.selectorText) - getSpecificityScore(element, a.selectorText);
}
return rules.sort(compareSpecificity);
}
// Find correct matchesSelector impl
function matchesSelector(el, selector) {
var matcher = el.matchesSelector || el.mozMatchesSelector ||
el.webkitMatchesSelector || el.oMatchesSelector || el.msMatchesSelector;
return matcher.call(el, selector);
}
//TODO: not supporting 2nd argument for selecting pseudo elements
//TODO: not supporting 3rd argument for checking author style sheets only
window.getMatchedCSSRules = function (element /*, pseudo, author_only*/) {
var style_sheets, sheet, sheet_media,
rules, rule,
result = [];
// get stylesheets and convert to a regular Array
style_sheets = toArray(window.document.styleSheets);
// assuming the browser hands us stylesheets in order of appearance
// we iterate them from the beginning to follow proper cascade order
while (sheet = style_sheets.shift()) {
// get the style rules of this sheet
rules = getSheetRules(sheet);
// loop the rules in order of appearance
while (rule = rules.shift()) {
// if this is an #import rule
if (rule.styleSheet) {
// insert the imported stylesheet's rules at the beginning of this stylesheet's rules
rules = getSheetRules(rule.styleSheet).concat(rules);
// and skip this rule
continue;
}
// if there's no stylesheet attribute BUT there IS a media attribute it's a media rule
else if (rule.media) {
// insert the contained rules of this media rule to the beginning of this stylesheet's rules
rules = getSheetRules(rule).concat(rules);
// and skip it
continue
}
// check if this element matches this rule's selector
if (matchesSelector(element, rule.selectorText)) {
// push the rule to the results set
result.push(rule);
}
}
}
// sort according to specificity
return sortBySpecificity(element, result);
};
}
Fixed bugs
= match → += match
return re ? re.length : 0; → return matches ? matches.length : 0;
_matchesSelector(element, selector) → matchesSelector(element, selector)
matcher(selector) → matcher.call(el, selector)
EDIT: This answer is now deprecated and no longer works in Chrome 64+. Leaving for historical context. In fact that bug report links back to this question for alternative solutions to using this.
Seems I managed to answer my own question after another hour of research.
It's as simple as this:
window.getMatchedCSSRules(document.getElementById("description"))
(Works in WebKit/Chrome, possibly others too)
Have a look at this library, which does what was asked for: http://www.brothercake.com/site/resources/scripts/cssutilities/
It works in all modern browsers right back to IE6, can give you rule and property collections like Firebug (in fact it's more accurate than Firebug), and can also calculate the relative or absolute specificity of any rule. The only caveat is that, although it understands static media types, it doesn't understand media-queries.
Here is my version of getMatchedCSSRules function which support #media query.
const getMatchedCSSRules = (el) => {
let rules = [...document.styleSheets]
rules = rules.filter(({ href }) => !href)
rules = rules.map((sheet) => [...(sheet.cssRules || sheet.rules || [])].map((rule) => {
if (rule instanceof CSSStyleRule) {
return [rule]
} else if (rule instanceof CSSMediaRule && window.matchMedia(rule.conditionText)) {
return [...rule.cssRules]
}
return []
}))
rules = rules.reduce((acc, rules) => acc.concat(...rules), [])
rules = rules.filter((rule) => el.matches(rule.selectorText))
rules = rules.map(({ style }) => style)
return rules
}
Here's a version of S.B.'s answer which also returns matching rules within matching media queries. I've removed the *.rules || *.cssRules coalescence and the .matches implementation finder; add a polyfill or add those lines back in if you need them.
This version also returns the CSSStyleRule objects rather than the rule text. I think this is a little more useful, since the specifics of the rules can be more easily probed programmatically this way.
Coffee:
getMatchedCSSRules = (element) ->
sheets = document.styleSheets
matching = []
loopRules = (rules) ->
for rule in rules
if rule instanceof CSSMediaRule
if window.matchMedia(rule.conditionText).matches
loopRules rule.cssRules
else if rule instanceof CSSStyleRule
if element.matches rule.selectorText
matching.push rule
return
loopRules sheet.cssRules for sheet in sheets
return matching
JS:
function getMatchedCSSRules(element) {
var i, len, matching = [], sheets = document.styleSheets;
function loopRules(rules) {
var i, len, rule;
for (i = 0, len = rules.length; i < len; i++) {
rule = rules[i];
if (rule instanceof CSSMediaRule) {
if (window.matchMedia(rule.conditionText).matches) {
loopRules(rule.cssRules);
}
} else if (rule instanceof CSSStyleRule) {
if (element.matches(rule.selectorText)) {
matching.push(rule);
}
}
}
};
for (i = 0, len = sheets.length; i < len; i++) {
loopRules(sheets[i].cssRules);
}
return matching;
}
var GetMatchedCSSRules = (elem, css = document.styleSheets) => Array.from(css)
.map(s => Array.from(s.cssRules).filter(r => elem.matches(r.selectorText)))
.reduce((a,b) => a.concat(b));
function Go(paragraph, print) {
var rules = GetMatchedCSSRules(paragraph);
PrintInfo:
print.value += "Rule 1: " + rules[0].cssText + "\n";
print.value += "Rule 2: " + rules[1].cssText + "\n\n";
print.value += rules[0].parentStyleSheet.ownerNode.outerHTML;
}
Go(document.getElementById("description"), document.getElementById("print"));
p {color: red;}
#description {font-size: 20px;}
<p id="description">Lorem ipsum</p>
<textarea id="print" cols="50" rows="12"></textarea>
Ensuring IE9+, I wrote a function which calculates CSS for requested element and its children, and gives possibility to save it to a new className if needed in snippet below.
/**
* #function getElementStyles
*
* Computes all CSS for requested HTMLElement and its child nodes and applies to dummy class
*
* #param {HTMLElement} element
* #param {string} className (optional)
* #param {string} extras (optional)
* #return {string} CSS Styles
*/
function getElementStyles(element, className, addOnCSS) {
if (element.nodeType !== 1) {
return;
}
var styles = '';
var children = element.getElementsByTagName('*');
className = className || '.' + element.className.replace(/^| /g, '.');
addOnCSS = addOnCSS || '';
styles += className + '{' + (window.getComputedStyle(element, null).cssText + addOnCSS) + '}';
for (var j = 0; j < children.length; j++) {
if (children[j].className) {
var childClassName = '.' + children[j].className.replace(/^| /g, '.');
styles += ' ' + className + '>' + childClassName +
'{' + window.getComputedStyle(children[j], null).cssText + '}';
}
}
return styles;
}
Usage
getElementStyles(document.getElementByClassName('.my-class'), '.dummy-class', 'width:100%;opaity:0.5;transform:scale(1.5);');
I think the answer from S.B. should be the accepted one at this point but it is not exact. It is mentioned a few times that there will be some rules that may be missed. Faced with that, I decided to use document.querySelectorAll instead of element.matches. The only thing is that you would need some kind of unique identification of elements to compare it to the one you are looking for. In most cases I think that is achievable by setting its id to have a unique value. That's how you can identify the matched element being yours. If you can think of a general way to match the result of document.querySelectorAll to the element you are looking for that would essentially be a complete polyfill of getMatchedCSSRules.
I checked the performance for document.querySelectorAll since it probably is slower than element.matches but in most cases it should not be a problem. I see that it takes about 0.001 milliseconds.
I also found CSSUtilities library that advertises that it can do this but I feel its old and has not been updated in a while. Looking at its source code, it makes me think there may be cases that it misses.
As the linked question is closed as a duplicate of this, I add an answer here instead.
The unanswered part 2: "Once I found the computed style, I want to know where it comes from"
By looping over the document.styleSheets, and looking at the getComputedStyle() before and after you modify it, you can detect what stylesheet is in use.
It's far from optimal, but at least it can detect if the rule you looking at is in use or not.
Here is an exemple:
<html><head>
<title>CSS Test</title>
<style id="style-a">
li {color: #333; font-size: 20px !important;}
li.bb {color: #600; font-size: 10px;}
p {margin: 5px;}
p {margin-bottom: 10px;}
</style>
<script>
window.addEventListener('DOMContentLoaded', async () => {
const selector = 'li';
// const selector = 'li.bb';
const exempleValues = {
'color': ['rgb(0, 0, 0)', 'rgb(255, 255, 255)'],
'font-size': ['10px', '12px'],
};
const delay = (t) => new Promise((k, e) => {setTimeout(k, t)});
for(const element of document.querySelectorAll(selector)) {
const elementCss = document.defaultView.getComputedStyle(element);
for(const sheet of document.styleSheets) {
for(const rule of sheet.cssRules) {
if(rule.selectorText !== selector) {
continue;
}
for(const properyName of rule.style) {
const currentValue = rule.style[properyName];
const priority = rule.style.getPropertyPriority(properyName)
if(!exempleValues[properyName]) {
console.warn('no exemple values for', properyName);
continue;
}
const exempleValue = exempleValues[properyName][exempleValues[properyName][0] === currentValue ? 1 : 0];
rule.style.setProperty(properyName, exempleValue, priority);
await delay(100);
if(exempleValue === elementCss[properyName]) {
console.log(selector, properyName, currentValue, priority || false, true, 'in use', element, sheet.ownerNode);
} else {
console.log(selector, properyName, currentValue, priority || false, false, 'overrided', element);
}
rule.style.setProperty(properyName, currentValue, priority);
await delay(100);
}
}
}
}
}, {once: true});
</script>
</head><body>
<h1>CSS Test</h1>
<p>html-file for testing css</p>
<ul>
<li>AAAA</li>
<li class="bb">BBBB</li>
<li>CCCC</li>
</ul>
</body></html>
I'm a mostly-newbie when it comes to web development (though not to programming in general) so pardon any incorrect terminology.
I want to build a script that, when added to an HTML page, detects each Hebrew word in the page and transforms that word into an HTML element, e.g. into a hyperlink with title.
So, the following:
<p>ראש הלשכה</p>
Is transformed into:
<p><a title="word 1" href="#">הלשכה</a> <a title="word 2" href="#">ראש</a></p>
Make sense?
So, I suppose the first order of business is detecting Hebrew words in a page. How would I go about doing this? I don't know where to start, outside of poking around jQuery documentation.
Searching for a Hebrew word in a string is fairly simple. Use a regexp that matches a contiguous sequence of Hebrew code points:
/[\u05D0-\u05FF]+/
Since JS supports functional programming, we can easily write our own functions to walk the document tree, calling a function on each text node. First, a bit of scaffolding.
if (! window.assert) {
window.dbgLvl = 1; // change this to 0 for production release
window.assert=function(succeeded, msg) {
if (dbgLvl && !succeeded) {
if (!msg) msg = 'assertion failed';
throw msg;
}
}
}
Next, we define a method to split strings into an array, including separators in the output.
/* String.separate is like String.split, but the result includes the
separators.
These implementations of 'String.separate' will work for our purposes,
but are buggy in general, due to differences in the implementation of
String.split.
The two misbehaviors we correct are including neither grouped patterns
nor empty strings in the result, though the latter is only corrected
when the missing empty string is at the start or the end.
*/
if ('-'.split(/(-)/).length & 1) {
assert('a'.split(/a/).length, 'split includes grouping but not empty strings');
// split includes groups in result
String.prototype.separate = function (separator) {
if (typeof separator == 'string') {
if (separator.charAt(0) != '('
|| separator.charAt(separator.length-1) != ')')
{
separator = new RegExp('(' + separator + ')', 'g');
} else {
separator = new RegExp(separator, 'g');
}
}
return this.split(separator);
}
} else {
if ('a'.split(/a/).length) {
// empty strings included, grouped aren't
String.prototype.separate = function (separator) {
if (typeof separator == 'string') {
separator = new RegExp(separator, 'g');
}
var fence = this.match(separator);
if (!fence) {
return [this];
}
var posts = this.split(separator);
assert(posts.length = fence.length+1);
var result = [], i;
for (i=0; i<fence.length; ++i) {
result.push(posts[i]);
result.push(fence[i]);
}
result.push(posts[i]);
return result;
}
} else {
// neither empty strings nor groups are included. IE, you suck.
String.prototype.separate = function (separator) {
if (typeof separator == 'string') {
separator = new RegExp(separator, 'g');
}
var fence = this.match(separator);
if (!fence) {
return [this];
}
var posts = this.split(separator);
if (posts.length <= fence.length) {
/* missing some posts. Assume that they are the first or
last, though this won't be true in general.
*/
if (posts.length < fence.length) {
posts.unshift('');
posts.push('');
} else {
if (this.substring(0, fence[0].length) == fence[0]) {
posts.unshift('');
} else {
posts.push('');
}
}
}
var result = [], i;
for (i=0; i<fence.length; ++i) {
result.push(posts[i]);
result.push(fence[i]);
}
result.push(posts[i]);
return result;
}
}
}
Next, some node predicates.
if (! window.Node) {
window.Node={TEXT_NODE: 3};
} else if (typeof Node.TEXT_NODE == 'undefined') {
Node.TEXT_NODE = 3;
}
function isTextNode(node) {return node.nodeType == Node.TEXT_NODE;}
function hasKids(node) {return node.childNodes && node.childNodes.length;}
function allNodes(node) {return true;}
Now the functions to walk the DOM.
/*
forEachChild: pre-order traversal of document tree. Applies a function to some nodes, determined by the 'which' and 'descendInto' arguments.
Arguments:
which (function): Returns true if 'action' should be applied to a node.
action (function): Takes a node and does something to it.
parent (Node): The node to start from.
descendInto (function, optional): By default, forEachChild will descend into every child that itself has children. Place additional restrictions by passing this argument.
*/
var forEachChild = (function() {
/* the actual implementation is made a local function so that the
optional parameter can be handled efficiently.
*/
function _forEachChild(which, action, node, descendInto) {
for (var child=node.firstChild; child; child=child.nextSibling) {
if (which(child)) {
action(child);
}
if (hasKids(child) && descendInto(child)) {
_forEachChild(which, action, child, descendInto);
}
}
}
return function (which, action, node, descendInto) {
if (!descendInto) {descendInto=allNodes}
_forEachChild(which, action, node, descendInto);
}
})();
function forEachNode(which, action, descendInto) {
return forEachChild(which, action, document, descendInto);
}
function forEachTextNode(action, descendInto) {
return forEachNode(isTextNode, action, descendInto);
}
function forEachTextNodeInBody(action, descendInto) {
return forEachChild(isTextNode, action, document.body, descendInto);
}
The last group of functions replace text in a text node that matches a pattern with a new node of your choosing. This group (well, the function returned by wrapText) hasn't been completely tested for cross-browser compatibility, including whether it handles text direction properly.
/*
wrapText replaces substrings in a text node with new nodes.
Arguments:
pattern (RegExp || string): If a RegExp, must be of the form: '/(...)/g'.
replace (function): Takes a string and returns a Node to replace the string.
Returns a function that takes a text node.
*/
function wrapText(pattern, replace) {
return function (node) {
var chunks = node.nodeValue.separate(pattern);
if (chunks.length < 2)
return;
var wordCount=0;
var fragment = document.createDocumentFragment();
var i;
// don't bother adding first chunk if it's empty.
if (chunks[0].length) {
fragment.appendChild(document.createTextNode(chunks[0]));
}
for (i=1; i < chunks.length; i+=2) {
fragment.appendChild(replace(chunks[i])); // †
fragment.appendChild(document.createTextNode(chunks[i+1])); // ‡
}
// clean-up
assert(i == chunks.length, 'even number of chunks in ['+chunks+'] when it should be odd.');
/* chunks.length and i will always be odd, thus i == chunks.length
* when the loop finishes. This means the last element is never
* missed.
* Here's another way of thinking about this. Since the last
* (and first) chunk won't match the pattern, it won't be
* processed by the line †. The penultimate chunk, however, does
* match. Assuming the loop condition is correct,the penultimate
* chunk must be processed by †, hence the last chunk is
* processed by ‡.
*/
if (! chunks[i-1].length) {
// last chunk is empty; remove it.
fragment.removeChild(fragment.lastChild);
}
node.parentNode.replaceChild(fragment, node);
}
}
/*
createAnchorWrap wraps a string in an anchor node. createAnchorWrap also
sets the title of the anchor.
Arguments:
title (string || function, optional): The title for the anchor element.
If title is a function, it's called with the string to wrap. If
title is a string, wrapper will use a word counter for the title
function.
Returns a function that takes a string and returns an anchor element.
*/
function createAnchorWrap(title) {
if (typeof title == 'string') {
title=createWordCounter(title);
} else if (!title) {
title=createWordCounter();
}
return function(word) {
var a = document.createElement('a');
a.title=title(word);
a.appendChild(document.createTextNode(word));
return a;
}
}
/*
createWordCounter creates a word counter, which returns the number of
times it's been called (including the current call), prefixed by a string.
Arguments:
pre (string, optional): prefix for return value.
Returns a function that takes a string (ignored) and returns a string.
*/
function createWordCounter(pre) {
var wordCount=0;
if (pre) {
pre = pre.replace(/ *$/, ' ');
} else {
pre = 'word ';
}
return function(text) {
return pre + wordCount;
}
}
The last thing you have to do is start the process in (e.g.) a load handler or a script at the bottom of the page.
forEachTextNodeInBody(wrapText(/([\u05D0-\u05FF]+)/g,
createAnchorWrap()));
If you want to change the prefix for the title, pass the result of createWordCounter(...) to the createAnchorWrap.