[xsl] Ordering nodes by number of children

Subject: [xsl] Ordering nodes by number of children
From: "Smilen Dimitrov (smidimi)" <smidimi@xxxxxxxxxx>
Date: Sun, 22 Feb 2004 14:31:10 +0100
I'm a newbie with XSL, and after long searching, and mainly thanks to the information in the list, I finally managed to solve the problem. Consider the following structure:
 
<?xml version="1.0"?>
 
<node label="aa" type="12">
 <node label="bb" type="21"/>
 <node label="cc" type="22">
  <node label="dd" type="44"/>
  <node label="ll" type="33">
   <node label="gg" type="22"/>
   <node label="hh" type="22"/>
  </node>
  <node label="ee" type="77"/>
  <node label="ff" type="55">
   <node label="gg" type="44"/>
   <node label="hh" type="44"/>
  </node>
  <node label="ww" type="44"/>
  <node label="xx" type="44"/>
 </node>
 <node label="yy" type="33"/>
 <node label="ll" type="33">
  <node label="gg" type="22"/>
  <node label="hh" type="22"/>
 </node>
 <node label="aa" type="22"/>
</node>
 
 
... etc
 
and suppose it was to be shown grafically on a tree display, in the same fashion as Windows Explorer shows them (folder icon and + expanding sign if the node has children, file icon and no sign if it has no children), and suppose that the engine that does the graphical display goes node by node in the hierarchy of the original xml. The problem is that for a long list, "files" (meaning a node without children) and "folders" (meaning a node with children) are scattered all over the place, and it don't look nice, basically... :) I was looking for a xsl script that would arrange the original xml in the same fashion that Windows explorer displays lists, that is, folders first, files later, for every level - that is, I was looking for the following output from the original xml:
 
<?xml version="1.0"?>
<node label="aa" type="12">
 <node label="cc" type="22">
  <node label="ll" type="33">
   <node label="gg" type="22"/>
   <node label="hh" type="22"/>
  </node>
  <node label="ff" type="55">
   <node label="gg" type="44"/>
   <node label="hh" type="44"/>
  </node>
  <node label="dd" type="44"/>
  <node label="ee" type="77"/>
  <node label="ww" type="44"/>
  <node label="xx" type="44"/>
 </node>
 <node label="ll" type="33">
  <node label="gg" type="22"/>
  <node label="hh" type="22"/>
 </node>
 <node label="bb" type="21"/>
 <node label="yy" type="33"/>
 <node label="aa" type="22"/>
</node>

Well, the solution that worked for me is below, and here is a short : First, template match="/node" instead of the root template match="/" since I do not want to access the document root, only my root node, which (should be) the only "node" child of the document root, so effectively it should be ran only once ?? (I hope :) )
 
Then a copy tag is opened, so that the root node correctly encompasses everything, and so that the actual attribute content of the root node can be copied, so that the script runs regardless of the node's attribute contents. The copy of all the attributes is output first, and then we iterate through all the children of the root node (which are again all nodes) with a for each. This is, metaphorically speaking, iteration in level 1 if we consider the root node (not the document root) as level 0. With using select="./*" in the for each, a direct reference to "node" is avoided, hopefully allowing that the script runs no matter what the names are named, that is it runs only according to number of element-type children (?)..
 
The children in level 1 are then arranged according to the number of children, with the elements with least children (0) set to the back (descending). This will ensure that the script afterwards, when examining in the next levels of iteration, will go through the "folders" first, and end with the "files"
 
We then call the ArrangeDirectory template for each node in the first level, using the "current node" itself as a parameter.
 
ArrangeDirectory accepts the current node, and examines it using choose. If it is has children (that is, <xsl:when test="$CurrentNode/child::*">), then it is a folder, so again a copy tag is opened, so that the node correctly encompasses everything, with the actual attribute content of the root node copied. After this, a for each iteration through the level defined by the current node is performed, first so that sorting is used to sort the files and directories in it, and second, so that ArrangeDirectory can be recursively called for each of the child nodes. After the for each loop is over, then the copy tag is closed, which closes the "parenting" folder, so that the inheritance is proper (this trick with the possibillity for separating the opening and closing tags of the copied element if it is a "folder" was the hardest for me, I kept on copying the the folders directly and the hierarchy kept on being messed up).. The other part of the having-children condition, is defined with the  <xsl:otherwise>, and it means that we came across a file, in which case, we just directly copy the element.
 
And here is the xsl:
 
 
<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"; version="1.0">
 <xsl:output method="xml" indent="yes"/>

 <xsl:template match="/node">
 
  <xsl:copy>
   <xsl:copy-of select="@*"/>
   
   <xsl:for-each select="./*">
   
    <xsl:sort select="count(child::*)" data-type="number" order="descending"/>
    
    <xsl:call-template name="ArrangeDirectory">
     <xsl:with-param name="CurrentNode" select="."/>
    </xsl:call-template>
    
   </xsl:for-each>
   
  </xsl:copy>
 </xsl:template>
 
 
 <xsl:template name="ArrangeDirectory">
  <xsl:param name="CurrentNode"/>
  
  <xsl:choose>
   <xsl:when test="$CurrentNode/child::*">
   
    <xsl:copy>
     <xsl:copy-of select="@*"/>
     
     <xsl:for-each select="$CurrentNode/child::*">
      <xsl:sort select="count(child::*)" data-type="number" order="descending"/>
      
      <xsl:call-template name="ArrangeDirectory">
       <xsl:with-param name="CurrentNode" select="."/>
      </xsl:call-template>
     </xsl:for-each>
     
    </xsl:copy>
   </xsl:when>
   
   <xsl:otherwise>
    <xsl:copy-of select="."/>
   </xsl:otherwise>
  </xsl:choose>
  
 </xsl:template>
 
</xsl:stylesheet>

I hope this can help someone - again, havent had much experience with XSL, so maybe the debate is a little amateurish (I have a bit of a programmer bckg so I still think in counters and loops), however you are more then welcome to set it in the right frame.
 
Best
smilen 
 

 XSL-List info and archive:  http://www.mulberrytech.com/xsl/xsl-list


Current Thread