Understanding the recursive function - javascript

Maybe this function is very simple for you. but i have problems with how this functions works.please explain how to compile this function.
It is zero the count in the run loop ?
function countChars(elm) {
var i, count = 0;
if (elm.nodeType == 3) { // TEXT_NODE
return elm.nodeValue.length;
}
for (i = 0, child; child = elm.childNodes[i]; i++) {
count += countChars(child);
}
return count;
}

Some Text
The DOM will look like this:
[
Textnode {"Some Text"}
]
The upper code is valid HTML, actually it is a text node. So if we want to get its length we simply take its length:
if (elm.nodeType == 3) { // TEXT_NODE
return elm.nodeValue.length;
}
Lets imagine a more complicated case:
Some Text <span> nested </span>
DOM:
[
Textnode {"Some Text"},
Span {
children:[
Textnode {"nested"}
]
}
]
Now weve got four nodes. A text node and a span node, and a text node nested in the span node. And theyre all nested in some kind of body. So to get its text length, we need to iterate over the nodes, and then go deeper ( have a look into tree traversal ):
var count = 0;
for (var i = 0, child; child = elm.childNodes[i]; i++) {
count += countChars(child);
}
return count;
So the upper example will work like this:
count=0
iterate over body:
child textnode:
count+="Some Text".length
child span:
inner count=0
iterate over span:
child textnode
inner count+="nested".length
count+=inner count
finished

/**
** This function counts the characters of a given node.
**
** #param : The current node to work on
**/
function countChars(elm) {
// Set local variables
var i, count = 0;
// Check to see if the current node is text.
// If its a text it cannot have any other children so simply
// return the text length int his node
if (elm.nodeType == 3) { // TEXT_NODE
return elm.nodeValue.length;
}
// This is not a text node so driil doen in to its all
// children and loop into them to count the number of
// character they have
for (i = 0, child; child = elm.childNodes[i]; i++) {
// dive into the current child in count the number of
// characters it contains. since we use recursion the count will
// be returned form the previous run and will return the number of
// characters collect from all "previous" children
count += countChars(child);
}
return count;
}

Related

Drop empty childrens using Javascript

I try to drop empty nodes using JavaScript, because the navigator count the return line or space like a child node
So, I make this try, first it's work correctly but if I count the nodes for the second time it's give me a number ( contains the empty nodes ) despite I make removechild
My code :
function testing(){
var c = document.body.childNodes;
for(i=0;i<c.length;i++){
if(c[i].nodeName == "#text")
{
var rest = c[i].textContent;
if(rest.length == 0){
//I want to remove the empty nodes
document.body.removeChild(document.body.childNodes[i]);
}
}
}
//this give a number contains the empty nodes
var d =document.body.childNodes.length;
alert(d);
}
My HTML code
<body onclick="testing()">
<p>test</p>
<p>test1</p>
</body>
If I try to know the length of this HTML code, the navigator give me 5 not 2 (it's count the return line and space)
So, for that I want to delete the empty nodes the take result 2
if(c[i].nodeName == "#text")
At this line you check if it is a #text node but in your html are p elements.
var rest = c[i].textContent;
if(rest.length == 0){
//I want to remove the empty nodes
document.body.removeChild(document.body.childNodes[i]);
}
Also with those lines you check if the content of the element is empty which is also not the case in both of your p elements.
So your alert should correctly contain 2.
If your problem is that you have text elements which contain whitespaces and newlines in your body element you can check for them with regex.
Just change this line
if(rest.length == 0){
To
if(/^[\s\n]{0,}$/.test(rest)){
And for the compleatness sake:
change the following line
for(i=0;i<c.length;i++){
To
for(i=len -1;i>=0;i--){
As described in #Bindrid's answer because you can miss elements.
So the compleate code should be
function testing(){
var c = document.body.childNodes;
for(var i = c.length; i >= 0; i--) {
if(c[i].nodeName == "#text") {
var rest = c[i].textContent;
if(/^[\s\n]{0,}$/.test(rest)) {
//I want to remove the empty nodes
document.body.removeChild(document.body.childNodes[i]);
}
}
}
//this give a number contains the empty nodes
var d =document.body.childNodes.length;
alert(d);
}
An element is considered empty when has no content, whitespaces, line return are considered as content. So, you have two way depending on what you need. if you need for true empties elements use document.querySelectorAll(':empty'), otherwise, use document.body.childNodes and check if its content is empty with String.prototype.trim.
Hope it helps.
<div>
</div>
<section></section>
function isTextNode(element) {
return element.nodeType === 3;
}
function removeEmptyTextNodes(element) {
if(!isTextNode(element)) { return; }
var content = (element.textContent || "").trim();
if(content.length > 0) { return; }
console.log("removing", element);
element.parentNode.removeChild(element);
}
Array.prototype.forEach.call(
document.body.childNodes, removeEmptyTextNodes
);
<div>
</div>
<span></span>
Sounds like you're overthinking it. You've observed the difference between childNodes and children, which is that childNodes contains all nodes, including text nodes consisting entirely of whitespace, while children is a collection of just the child nodes that are elements. That's really all there is to it.
function testing(){
alert(document.body.children.length);
var c = document.body.children;
for(i=0;i<c.length;i++){
if(c[i].innerHTML == "")
{
//I want to remove the empty nodes
document.body.removeChild(c[i]);
}
}
//this give a number contains the empty nodes
var d =document.body.children.length;
alert(d);
}
<body onclick="testing()">
<p>test</p>
<p>test1</p>
<p></p>
ss
</body>
for(i=0;i<c.length;i++){
if(c[i].nodeName == "#text")
{
var rest = c[i].textContent;
if(rest.length == 0){
//I want to remove the empty nodes
document.body.removeChild(document.body.childNodes[i]);
}
}
}
should actually be as shown below to take in account that you are removing nodes as you run through the list. If node 7 and 8 are empty and you remove 7, 8 becomes 7 while you are in the 7 loop so 8 as the new 7 gets missed on the next loop.
var len = c.length;
for(i=len -1;i>=0;i--){
if(c[i].nodeName == "#text")
{
var rest = c[i].textContent;
if(rest.length == 0){
//I want to remove the empty nodes
document.body.removeChild(document.body.childNodes[i]);
}
}
}

How to write Javascript to search nodes - without getElementsByClassName

I'm very new at recursion, and have been tasked with writing getElementsByClassName in JavaScript without libraries or the DOM API.
There are two matching classes, one of which is in the body tag itself, the other is in a p tag.
The code I wrote isn't working, and there must be a better way to do this. Your insight would be greatly appreciated.
var elemByClass = function(className) {
var result = [];
var nodes = document.body; //<body> is a node w/className, it needs to check itself.
var childNodes = document.body.childNodes; //then there's a <p> w/className
var goFetchClass = function(nodes) {
for (var i = 0; i <= nodes; i++) { // check the parent
if (nodes.classList == className) {
result.push(i);
console.log(result);
}
for (var j = 0; j <= childNodes; j++) { // check the children
if (childNodes.classList == className) {
result.push(j);
console.log(result);
}
goFetchClass(nodes); // recursion for childNodes
}
goFetchClass(nodes); // recursion for nodes (body)
}
return result;
};
};
There are some errors, mostly logical, in your code, here's what it should have looked like
var elemByClass = function(className) {
var result = [];
var pattern = new RegExp("(^|\\s)" + className + "(\\s|$)");
(function goFetchClass(nodes) {
for (var i = 0; i < nodes.length; i++) {
if ( pattern.test(nodes[i].className) ) {
result.push(nodes[i]);
}
goFetchClass(nodes[i].children);
}
})([document.body]);
return result;
};
Note the use of a regex instead of classList, as it makes no sense to use classList which is IE10+ to polyfill getElementsByClassName
Firstly, you'd start with the body, and check it's className property.
Then you'd get the children, not the childNodes as the latter includes text-nodes and comments, which can't have classes.
To recursively call the function, you'd pass the children in, and do the same with them, check for a class, get the children of the children, and call the function again, until there are no more children.
Here are some reasons:
goFetchClass needs an initial call after you've defined it - for example, you need a return goFetchClass(nodes) statement at the end of elemByClass function
the line for (var i = 0; i <= nodes; i++) { will not enter the for loop - did you mean i <= nodes.length ?
nodes.classList will return an array of classNames, so a direct equality such as nodes.classList == className will not work. A contains method is better.
Lastly, you may want to reconsider having 2 for loops for the parent and children. Why not have 1 for loop and then call goFetchClass on the children? such as, goFetchClass(nodes[i])?
Hope this helps.

Replace text in the middle of a TextNode with an element

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);

JavaScript: Create Object from DOM

I'm trying to make a walkable DOM tree, like so:
Input:
<div>
<span>Foo</span>
<span>Bar</span>
</div>
Output (Python-like):
{'div': [{'span': 'Foo'},
{'span': 'Bar'}]}
I'd like to traverse it like so:
elements['div']['span']; // Output is "Foo".
My current code is this:
function createObject(element) {
var object = {};
if (element.childNodes.length > 0) {
for (var i = 0; i < element.childNodes.length; i++) {
object[element.tagName] = createObject(element.childNodes[i]);
}
return object;
} else {
return element.nodeValue;
}
}
But it doesn't work (the loop doesn't run). Could anyone help with this problem?
What should happen?
If no child {name: value}
if childs {name: [
{childname: childvalue}
]}
Following that logic, this is the result. Note nodeName should be used instead of tagName. Text nodes are also selected, which have nodeName #Text. If you want to only select elements, addif(element.childNodes[i].nodeType == 1)`:
function createObject(element) {
var object, childs = element.childNodes;
if (childs.length > 0) {
object = [];
for (var i = 0; i < childs.length; i++) {
//Uncomment the code if you want to ignore non-elements
// if(childs.nodeType == 1) {
object.push(createObject(childs[i]));
// }
}
return object;
} else {
object = {};
object[element.nodeName] = element.nodeValue;
return object;
}
}
Without trying to test this, it looks like the main problem is your for ... in loop - it doesn't work the same way in Javascript it does in Python.
for (child in element.childnodes)
should probably be an iterator-based loop:
for (var x=0, child; x<element.childNodes.length; x++) {
child = element.childNodes[x];
// etc
}
You'll also get text nodes you don't expect, and should check child.nodeType != Node.TEXT_NODE before recursing.
It looks like childNodes.length differs between browsers, maybe you should use hasChildNodes instead?
Also, did you use firebug (or any js debugger) to see if element was correctly filled in?
Edit : I found what is wrong. You can't create object of objects. Instead, you have to create array of objects. Check if you have childNodes, and create an object if there is none. Otherwise, create an array.
Just like your python-like output shows :-)

counting text node recursively using javascript

Let say I have a mark up like this
<html id="test">
<body>
Some text node.
<div class="cool"><span class="try">This is another text node.</span></div>
Yet another test node.
</body>
</html>
my js code
function countText(node){
var counter = 0;
if(node.nodeType === 3){
counter+=node.nodeValue.length;
countText(node);
}
else{}
}
Now if I want to count the text nodes
console.log("count text : " + countText(document.getElementById("test"));
this should return me the count but its not working and moreover what should I put in else condition.
I never used nodeType so kind of having problem using it . Any help will be appreciated.
There are a couple of things wrong in your code:
Your HTML is malformed.
You are appending text to your counter instead of increasing it.
You never loop over the children of the a node, you always pass the same node to the recursive call.
You don't do anything if a node is not a text node.
This will work:
function countText(node){
var counter = 0;
if(node.nodeType === 3){
counter++;
}
else if(node.nodeType === 1) { // if it is an element node,
var children = node.childNodes; // examine the children
for(var i = children.length; i--; ) {
counter += countText(children[i]);
}
}
return counter;
}
alert(countText(document.body));
DEMO
Which number corresponds to which node type can be found here.
Update:
If you want to count the words, you have to split each text node into words first. In the following I assume that words are separated by white spaces:
if(node.nodeType === 3){
counter = node.nodeValue.split(/\s+/g).length;
}
Update 2
I know you want to use a recursive function, but if you want to count the words only, then there is a much easier and more efficient way:
function countWords(node){
// gets the text of the node and all its descendants
var text = node.innerText || node.textContent
return text.split(/\s+/g).length;
}
You want something like
function countTextNodes(node) {
var n = 0;
if(node.nodeType == 3)
n = 1;
for(var i = 0; i < node.childNodes.length; ++i)
n += countTextNodes(node.childNodes[i]);
return n;
}
This can be compressed into more compact code, but I went for legibility here.
Call this on the root in which you want to count text nodes. For example, to count text nodes throughout the entire document, you would want to call countTextNodes(document.getDocumentElement()).

Categories

Resources