Too much recursion - javascript

I'm using JS on firefox 4 and get the "too much recursion error" for the following code:
extractText: function(domObj) {
if (domObj == null) {
return "";
} else {
var acc = "";
if (domObj.nodeType == Node.TEXT_NODE) {
acc += domObj.nodeValue;
}
if (domObj.hasChildNodes()) {
var children = currentObj.childNodes;
for (var i = 0; i < children.length; i++) {
acc += sui.extractText(children[i]);
}
}
return acc;
}
}
};
Anyone?

I think that this line:
var children = currentObj.childNodes;
should be:
var children = domObj.childNodes;
It looks to me as if your reference to "currentObj" is starting over at the top instead of descending from the element under examination. It's hard to tell of course because you didn't include the relevant definition or initialization of "currentObj".

You can also try an iterative approach instead of recursion:
extractText: function(domObj) {
if (!(domObj instanceof Node)) return null;
var stack = [domObj], node, tf = [];
while (stack.length > 0) {
node = stack.pop();
switch (node.nodeType) {
case Node.TEXT_NODE:
tf.push(node.nodeValue);
break;
case Node.ELEMENT_NODE:
for (var i=node.childNodes.length-1; i>=0; i--)
stack.push(node.childNodes[i]);
break;
}
}
return tf.join("");
}
This algorithm implements a depth first search using a stack for the nodes that still must be visited. The first item on the stack is domObj if it’s a Node instance. Then for each node on the stack: if it is a Text node, its value is added to the text fragments array tf; if it’s an Element node, its child nodes are put on the stack in reverse order so that the first child is on top of the stack. These steps are repeated until the stack is empty. At the end, the text fragments in tf are put together using the array’s join method.

Related

Javascript traversing DOM upwards

In jQuery to traverse DOM several steps upward instead of
$(this).parent().parent().parent().parent().parent().click();
I can write short version:
$(this).parent(5).click();
So, I want to know, if there's a way to shorten code instead of spamming '.parentNode'?
Rather trivial:
function parent (element, n = 1) {
let {parentNode} = element;
for (let i = 1; parentNode && i < n; i++) {
({parentNode} = parentNode);
}
return parentNode;
}
const span = document.querySelector('span');
const directParent = parent(span); // direct parent node
const greatGreatGreatGrandParent = parent(span, 5); // parent node 5 times
console.log(directParent.getAttribute('data-foo')); // baz
console.log(greatGreatGreatGrandParent.getAttribute('data-foo')); // bar
<div data-foo="bar">
<div>
<div>
<div>
<div data-foo="baz">
<span>Hello, World!</span>
</div>
</div>
</div>
</div>
</div>
I check for parentNode because it might be null. In that case, I break the loop and return null, because continuing the loop would result in an error:
({parentNode} = null); // TypeError: can't convert null to object
The parentheses are necessary to indicate that the opening { doesn't start a block, but a destructuring assignment:
{parentNode} = parentNode; // SyntaxError: expected expression, got '='
Edit:
I must admit that my solution is rather condensed and uses some new JavaScript features to make it even more succinct.
What does let {parentNode} = element; mean?
Object destructuring lets you read properties from an object in a more succinct way:
let {parentNode} = element;
is equivalent to:
let parentNode = element.parentNode;
As explained above,
({parentNode} = parentNode);
is equivalent to:
parentNode = parentNode.parentNode;
What does parentNode && i < n exactly?
Every for-loop has a condition. If this condition is true, the loop is continued. If the condition is false, it breaks out of the loop.
I could write:
for (let i = 1; i < n; i++) {
// ...
}
That would run the loop n - 1 iterations. After the last iteration, i is equal to n, so the condition evaluates to false and it stops, which is fine.
parentNode && i < n extends this condition. It not only checks whether i is less than n, but it also checks if parentNode contains a truthy value. If parentNode is null (which might happen is an element has no parent node), the loop will break, because it can't read the property parentNode of null.
I hope you understand the explanations with the following function which is equivalent to the original one:
function parent (element, times) {
var n; // how many times 'parentNode'?
if (times) {
n = times;
} else {
n = 1;
}
var elementToReturn = element.parentNode;
for (var i = 1; i < n; i++) {
if (elementToReturn === null) {
break;
} else {
elementToReturn = elementToReturn.parentNode;
}
}
return elementToReturn;
}
The sorcerer's one-liner...
It's the same as #petermader's only shorter. I guess there's less of a need to be super explicit here, as this would probably be just an imported utility function, and it's still more reliable than a for loop.
const getParentElement = ({ parentNode }, n = 1) =>
Array.from({ length: n - 1 }, () => ({ parentNode } = parentNode)) && parentNode

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.

How to traverse a json tree file and remove nodes?

I'm trying to traverse through a json file (a tree) and given a specific node, I want to keep THAT node and all its children. I have tried writing some javascript code that does this, but I get an error "Uncaught RangeError: Maximum call stack size exceeded". Here's the code I use:
function removeNodes(tree){
//Check if root node contains any children.
if(tree["children"]){
var children = tree["children"];
//Loop through all children and delete their everything further down in their hierarchy.
for(var i = 0; i < children.length; i++) {
var node = children[i];
var grandChildren = node["children"];
if(grandChildren){
grandChildren.splice(i,1);
i--;
}
removeNodes(node);
}
}
}
What am I doing wrong here? And how can I traverse my json file properly. To explain once more:
Given a root node ("tree" in this case), I want to keep the root node and all it's children, but remove anything else below in the hiercharchy.
Thanks in advance!
function removeNodes(tree, desiredNode){
//check if node is one we want
if (tree == desiredNode) {
return tree;
}
//Check if root node contains any children.
if(tree && tree.children){
var children = tree.children;
//Loop through all children and delete their everything further down in their hierarchy.
for(var i = 0; i < children.length; i++) {
var node = removeNodes(children[i], desiredNode);
if (node == desiredNode) {
return node;
}
}
}
return false; //if no match found
}
var foo = removeNodes(tree, someNodeToKeep);
if (foo) {
var parent = tree.parentNode;
parent.removeChild(tree);
parent.appendChild(foo); //parent now contains only that node and children
}
Please note that like any recursive function, this can blow the stack on languages without proper tail calls, but unless you're using this to search a large data structure it should be fine.

Javascript: Recursively count number of children in a tree

We have a JSON tree structure which maintains the children of a specific node. This children parameter is either Undefined (no children) or an array.
I want to compute the score of a node, which is the sum of the scores of the children nodes. And a node without children has a score of one.
This seemed pretty straightforward to me, however only the last node from the leaf-nodes are updated correctly, the rest show as NaN. I'm not sure where Undefined/NaN is being introduced, I suspect it might be due to how JS handles references within recursion?
function updateScore(n) {
if (n.children == undefined) return 1;
n.children.forEach(function(c){
var r = updateScore(c);
//if (!r) console.log(n);
n.score += r;
});
}
updateScore(sparent);
The data is as follows:
var sparent = {"id":"src","score":0,"children":[{"id":0,"score":0,"children":[{"id":14,"score":0}]},{"id":2,"score":0},{"id":4,"score":0},{"id":11,"score":0},{"id":17,"score":0,"children":[{"id":31,"score":0}]},{"id":18,"score":0},{"id":23,"score":0,"children":[{"id":13,"score":0,"children":[{"id":10,"score":0},{"id":21,"score":0,"children":[{"id":16,"score":0}]},{"id":82,"score":0,"children":[{"id":22,"score":0},{"id":75,"score":0}]},{"id":91,"score":0}]},{"id":48,"score":0,"children":[{"id":70,"score":0,"children":[{"id":46,"score":0},{"id":74,"score":0}]},{"id":97,"score":0,"children":[{"id":25,"score":0}]}]}]},{"id":28,"score":0,"children":[{"id":85,"score":0}]},{"id":30,"score":0,"children":[{"id":24,"score":0,"children":[{"id":26,"score":0},{"id":53,"score":0,"children":[{"id":45,"score":0}]},{"id":76,"score":0,"children":[{"id":38,"score":0},{"id":62,"score":0},{"id":66,"score":0}]},{"id":93,"score":0,"children":[{"id":1,"score":0},{"id":56,"score":0}]}]},{"id":78,"score":0,"children":[{"id":50,"score":0,"children":[{"id":15,"score":0},{"id":67,"score":0}]}]},{"id":94,"score":0}]},{"id":36,"score":0},{"id":39,"score":0,"children":[{"id":35,"score":0,"children":[{"id":73,"score":0},{"id":83,"score":0,"children":[{"id":3,"score":0}]}]}]},{"id":41,"score":0,"children":[{"id":5,"score":0},{"id":32,"score":0}]},{"id":43,"score":0,"children":[{"id":20,"score":0,"children":[{"id":89,"score":0,"children":[{"id":52,"score":0}]}]},{"id":27,"score":0},{"id":86,"score":0,"children":[{"id":6,"score":0}]},{"id":98,"score":0,"children":[{"id":54,"score":0},{"id":59,"score":0},{"id":65,"score":0,"children":[{"id":8,"score":0},{"id":47,"score":0}]},{"id":79,"score":0}]}]},{"id":64,"score":0},{"id":71,"score":0,"children":[{"id":19,"score":0},{"id":51,"score":0,"children":[{"id":57,"score":0,"children":[{"id":29,"score":0,"children":[{"id":49,"score":0}]},{"id":60,"score":0,"children":[{"id":34,"score":0}]}]},{"id":61,"score":0,"children":[{"id":63,"score":0},{"id":72,"score":0,"children":[{"id":9,"score":0},{"id":37,"score":0}]}]},{"id":90,"score":0}]},{"id":92,"score":0,"children":[{"id":96,"score":0,"children":[{"id":55,"score":0,"children":[{"id":12,"score":0},{"id":69,"score":0}]},{"id":80,"score":0}]}]}]},{"id":77,"score":0,"children":[{"id":81,"score":0,"children":[{"id":40,"score":0}]}]},{"id":84,"score":0,"children":[{"id":7,"score":0,"children":[{"id":33,"score":0,"children":[{"id":42,"score":0},{"id":95,"score":0}]}]},{"id":44,"score":0,"children":[{"id":58,"score":0}]}]},{"id":87,"score":0,"children":[{"id":68,"score":0}]},{"id":88,"score":0},{"id":99,"score":0}]};
you forgot to return the score if the element has children. try this one:
function updateScore(n) {
if (n.children == undefined) return 1;
n.children.forEach(function(c){
var r = updateScore(c);
//if (!r) console.log(n);
n.score += r;
});
return n.score;
}

getElementsByTagName() equivalent for textNodes

Is there any way to get the collection of all textNode objects within a document?
getElementsByTagName() works great for Elements, but textNodes are not Elements.
Update: I realize this can be accomplished by walking the DOM - as many below suggest. I know how to write a DOM-walker function that looks at every node in the document. I was hoping there was some browser-native way to do it. After all it's a little strange that I can get all the <input>s with a single built-in call, but not all textNodes.
Update:
I have outlined some basic performance tests for each of these 6 methods over 1000 runs. getElementsByTagName is the fastest but it does a half-assed job, as it does not select all elements, but only one particular type of tag ( i think p) and blindly assumes that its firstChild is a text element. It might be little flawed but its there for demonstration purpose and comparing its performance to TreeWalker. Run the tests yourselves on jsfiddle to see the results.
Using a TreeWalker
Custom Iterative Traversal
Custom Recursive Traversal
Xpath query
querySelectorAll
getElementsByTagName
Let's assume for a moment that there is a method that allows you to get all Text nodes natively. You would still have to traverse each resulting text node and call node.nodeValue to get the actual text as you would do with any DOM Node. So the issue of performance is not with iterating through text nodes, but iterating through all nodes that are not text and checking their type. I would argue (based on the results) that TreeWalker performs just as fast as getElementsByTagName, if not faster (even with getElementsByTagName playing handicapped).
Ran each test 1000 times.
Method Total ms Average ms
--------------------------------------------------
document.TreeWalker 301 0.301
Iterative Traverser 769 0.769
Recursive Traverser 7352 7.352
XPath query 1849 1.849
querySelectorAll 1725 1.725
getElementsByTagName 212 0.212
Source for each method:
TreeWalker
function nativeTreeWalker() {
var walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
null,
false
);
var node;
var textNodes = [];
while(node = walker.nextNode()) {
textNodes.push(node.nodeValue);
}
}
Recursive Tree Traversal
function customRecursiveTreeWalker() {
var result = [];
(function findTextNodes(current) {
for(var i = 0; i < current.childNodes.length; i++) {
var child = current.childNodes[i];
if(child.nodeType == 3) {
result.push(child.nodeValue);
}
else {
findTextNodes(child);
}
}
})(document.body);
}
Iterative Tree Traversal
function customIterativeTreeWalker() {
var result = [];
var root = document.body;
var node = root.childNodes[0];
while(node != null) {
if(node.nodeType == 3) { /* Fixed a bug here. Thanks #theazureshadow */
result.push(node.nodeValue);
}
if(node.hasChildNodes()) {
node = node.firstChild;
}
else {
while(node.nextSibling == null && node != root) {
node = node.parentNode;
}
node = node.nextSibling;
}
}
}
querySelectorAll
function nativeSelector() {
var elements = document.querySelectorAll("body, body *"); /* Fixed a bug here. Thanks #theazureshadow */
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.nodeValue);
}
}
}
getElementsByTagName (handicap)
function getElementsByTagName() {
var elements = document.getElementsByTagName("p");
var results = [];
for(var i = 0; i < elements.length; i++) {
results.push(elements[i].childNodes[0].nodeValue);
}
}
XPath
function xpathSelector() {
var xpathResult = document.evaluate(
"//*/text()",
document,
null,
XPathResult.ORDERED_NODE_ITERATOR_TYPE,
null
);
var results = [], res;
while(res = xpathResult.iterateNext()) {
results.push(res.nodeValue); /* Fixed a bug here. Thanks #theazureshadow */
}
}
Also, you might find this discussion helpful - http://bytes.com/topic/javascript/answers/153239-how-do-i-get-elements-text-node
Here's a modern Iterator version of the fastest TreeWalker method:
function getTextNodesIterator(el) { // Returns an iterable TreeWalker
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
walker[Symbol.iterator] = () => ({
next() {
const value = walker.nextNode();
return {value, done: !value};
}
});
return walker;
}
Usage:
for (const textNode of getTextNodesIterator(document.body)) {
console.log(textNode)
}
Safer version
Using the iterator directly might get stuck if you move the nodes around while looping. This is safer, it returns an array:
function getTextNodes(el) { // Returns an array of Text nodes
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
const nodes = [];
while (walker.nextNode()) {
nodes.push(walker.currentNode);
}
return nodes;
}
I know you specifically asked for a collection, but if you just meant that informally and didn't care if they were all joined together into one big string, you can use:
var allTextAsString = document.documentElement.textContent || document.documentElement.innerText;
...with the first item being the DOM3 standard approach. Note however that innerText appears to exclude script or style tag contents in implementations that support it (at least IE and Chrome) while textContent includes them (in Firefox and Chrome).
Here's an alternative that's a bit more idiomatic and (hopefully) easier to understand.
function getText(node) {
// recurse into each child node
if (node.hasChildNodes()) {
node.childNodes.forEach(getText);
}
// get content of each non-empty text node
else if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent.trim();
if (text) {
console.log(text); // do something
}
}
}
after createTreeWalker is deprecated you can use
/**
* Get all text nodes under an element
* #param {!Element} el
* #return {Array<!Node>}
*/
function getTextNodes(el) {
const iterator = document.createNodeIterator(el, NodeFilter.SHOW_TEXT);
const textNodes = [];
let currentTextNode;
while ((currentTextNode = iterator.nextNode())) {
textNodes.push(currentTextNode);
}
return textNodes;
}
document.deepText= function(hoo, fun){
var A= [], tem;
if(hoo){
hoo= hoo.firstChild;
while(hoo!= null){
if(hoo.nodeType== 3){
if(typeof fun== 'function'){
tem= fun(hoo);
if(tem!= undefined) A[A.length]= tem;
}
else A[A.length]= hoo;
}
else A= A.concat(document.deepText(hoo, fun));
hoo= hoo.nextSibling;
}
}
return A;
}
/*
You can return an array of all the descendant text nodes of some parent element,
or you can pass it some function and do something (find or replace or whatever)
to the text in place.
This example returns the text of the non-whitespace textnodes in the body:
var A= document.deepText(document.body, function(t){
var tem= t.data;
return /\S/.test(tem)? tem: undefined;
});
alert(A.join('\n'))
*/
Handy for search and replace, highlighting and so on
var el1 = document.childNodes[0]
function get(node,ob)
{
ob = ob || {};
if(node.childElementCount)
{
ob[node.nodeName] = {}
ob[node.nodeName]["text"] = [];
for(var x = 0; x < node.childNodes.length;x++)
{
if(node.childNodes[x].nodeType == 3)
{
var txt = node.childNodes[x].nodeValue;
ob[node.nodeName]["text"].push(txt)
continue
}
get(node.childNodes[x],ob[node.nodeName])
};
}
else
{
ob[node.nodeName] = (node.childNodes[0] == undefined ? null :node.childNodes[0].nodeValue )
}
return ob
}
var o = get(el1)
console.log(o)

Categories

Resources