views:

874

answers:

4

I've got some basic code that can loop through some XML that's generated from Adobe RoboHelp (for our help documentation). This works fine, but since a topic could be nested as many time as the writer wants, i need a better way to loop through this XML rather than just nesting .each() loops.

Here's what the XML looks like

<?xml version="1.0" encoding="utf-8"?>
<!--RoboML: Table of Content-->
<roboml_toc>
  <page title="Welcome" url="Welcome.htm"/>
 <book title="Getting Started" url="Getting_Started/Initial_Setup.htm">
   <page title="Initial Setup" url="Getting_Started/Initial_Setup.htm"/>
   <page title="Customize Settings" url="Getting_Started/Settings.htm"/>
 </book>
 <book title="Administrator Services" url="Administrator_Services/General_Administrator.htm">
  <book title="Portal Workspace" url="Administrator_Services/Portal_Workspace/AdminHome.htm">
    <page title="Home" url="Administrator_Services/Portal_Workspace/AdminHome.htm"/>
    <page title="Portal Accounts" url="Administrator_Services/Portal_Workspace/Portal_Accounts.htm"/>

  </book>
  <book title="SpamLab" url="Administrator_Services/SpamLab/SpamLab_Admin_General.htm">
    <page title="Alerts" url="Administrator_Services/SpamLab/Alerts.htm"/>
    <page title="Spam Quarantine" url="Administrator_Services/SpamLab/Admin_Spam_Quarantine_.htm"/>

  </book>

 </book>
 <book title="User Services" url="User_Services/General_User.htm">
  <book title="Portal Workspace" url="User_Services/Portal_Workspace/Home.htm">
    <page title="Home" url="User_Services/Portal_Workspace/Home.htm"/>
    <page title="Self Help" url="User_Services/Portal_Workspace/Self_Help.htm"/>
  </book>
  <book title="SpamLab" url="User_Services/SpamLab/SpamLab_General.htm">
    <page title="Spam Quarantine" url="User_Services/SpamLab/Spam_Quarantine.htm"/>
    <page title="Virus Quarantine" url="User_Services/SpamLab/Virus_Quarantine.htm"/>
  </book>

  <book title="Encryption" url="User_Services/Encryption/Encryption_General.htm">
    <page title="Outlook Plug-in" url="User_Services/Encryption/Encryption_Outlook_Plug_in.htm"/>
  </book>
 </book>
</roboml_toc>

A <page> is an article, and a <book> is a folder.

Her's my jQuery code, which only can look one level deep of tags

   //Get the TOC
$tocOutput="";
$.get(tocURL,function(toc){
    $(toc).children().each(function(){
     $tocOutput+="<li><a href='"+$(this).attr("url")+"'>"+$(this).attr("title")+"</a>";
     if(this.tagName=="BOOK"){
      $tocOutput+="<ul>";
      $(this).find("page").each(function(){
       $tocOutput+="<li><a href='"+$(this).attr("url")+"'>"+$(this).attr("title")+"</a></li>";
      });
      $tocOutput+="</ul>";
     }
     $tocOutput+="</li>";
    });
    $("#list").html($tocOutput);

I know there's a better way to just loop through all elements and then determine if the element has children, etc. but I just can't think of how to do it.

Any help is greatly appreciated!

+1  A: 

You can use

$(el).children().length which would return '0' or a positive number, then loop through if it's a positive number which evaluates to true. You could also use a while loop to do this recursively, and re-set the reference handler however I'm not quite sure that would work out because your nodeNames for each subsequent child differ ( or do they? ) .. What's the most nested example you can provide?

meder
No, the node names are either <book> or <page>. A page can exist inside or outside a book. A book can contain other books or pages.In the XML I posted above there's a page that has 2 book parents.
Chris Barr
+3  A: 

Recursive functions work well for this. When you create a function that creates and uses an internal recursive closure you can wrap it all up in a neat little package:

    $.get(tocURL, function(toc) {
    function makeToc($xml) {
        // variable to accumulate markup
        var markup = "";
        // worker function local to makeToc
        function processXml() {
            markup += "<li><a href='" + $(this).attr("url") + "'>" + $(this).attr("title") + "</a>";
            if (this.nodeName == "BOOK") {
                markup += "<ul>";
                // recurse on book children
                $(this).find("page").each(processXml);
                markup += "</ul>";
            }
            markup += "</li>";
        }
        // call worker function on all children
        $xml.children().each(processXml);
        return markup;
    }
    var tocOutput = makeToc($(toc));
    $("#list").html(tocOutput);
});
Keith Morgan
A: 

THanks so much Keith, that was the ticket - well almost, I had to make one MINOR change and then it worked perfectly!

My code is below.

$tocOutput="";
$.get(tocURL,function(toc){
 function makeToc($xml) {
  // worker function local to makeToc
  function processXml() {
   console.log($(this));
   $tocOutput += "<li><a href='" + $(this).attr("url") + "'>" + $(this).attr("title") + "</a>";
   if (this.nodeName == "BOOK") {
    $tocOutput += "<ul>";
    // recurse on book children
    $(this).children().each(processXml);
    $tocOutput += "</ul>";
   }
   $tocOutput += "</li>";
  }
  // call worker function on all children
  $xml.children().each(processXml);
 }
 var tocOutput = makeToc($(toc));
 $("#toc").html($tocOutput);
 completed($("#toc"));
});

You'll notice all I'm doing is declaring the variable outside the $.get() and then I use $xml.children().each(processXml); instead of $(this).find("page").each(processXml); that you had.

The reason for this is that the children could be pages OR books, but what you had was limiting it to only pages.

Thanks again!

Chris Barr
You can thank me by accepting my answer! Incidently, my goal wasn't to solve your whole problem, just to duplicate the logic in your question. Also, I placed the variable declaration inside makeToc() as a general best practice. Doing it that way makes it clear that variable is only for the use of makeToc to accumulate results.
Keith Morgan
A: 

Here's something to earn some more praise. I made it an anonymous function call, and used the arguments.callee to recurse. I was myself looking for this method, this and another thread at stackoverflow helped me out and I want to pay it back :-)

$.get(tocURL,function(data){
    var markup = "<ul>";
    $(data).each(function(){
        markup += "<li><a href='" + $(this).attr("url") + "'>" + $(this).attr("title") + "</a>";
        if (this.nodeName == "BOOK") {
            $markup += "<ul>";
            $(this).children().each(arguments.callee);    
            $markup += "</ul>";
        }
        markup += "</li>";
    });
    $("#list").html(markup+"</ul>");
});
Sanjeev Satheesh