RE: [xsl] Generate Yahoo-like directory structure

Subject: RE: [xsl] Generate Yahoo-like directory structure
From: "M. David Peterson" <m.david@xxxxxxxxxx>
Date: Tue, 11 May 2004 20:06:04 -0600
When I saw your post it reminded me that I needed to create this exact
feature for an up and coming site of mine, http://www.generationXML.com
.  This site is actually a high priority for me but my other project,
http://www.aspectXML.org has taken precedence simply because the benefit
of having the AspectXML project in a 1.0 state will benefit greatly
generationXML.com as well as several of my other projects.  I realize
this is a lot more information than you needed but I thought I would
explain that in this particular case I am not trying to be the good
Samaritan who simply wants to help where he can.  No my motives were
ulterior this time around but fortunately the fact that I needed this
code and had planned on writint it this week lends well to all of us.  

As it turns out the code took less than an hour to develop so it wasn't
even as big of a deal as I thought it would be. The resulting code
should do for you exactly what you want... output a Yahoo style
directory based on a parameter that is set to the current directory
structure and then mapping that against an XML tree that we can then
parse to output our desired directory... fortunately our needs were
exact and we both now have what we want.  Well, I hope anyway :D

I apologize for not getting this done and out sooner but the events of
the day have not lent well to such luxuries so it is now almost 8pm
where I'm at and the day isn't even close to coming to an end.  Such is
the life of a software developer/code junkie. 

I'll provide some quick documentation to try to explain what I did.  If
this becomes code that gets used by more than one person I may consider
writing heavier docs but these should be suffice to anyone but the
novice XSLT developer.  

Keep in mind that I've only done preliminary testing on this code but so
far is seems to output correctly the children of the last element in the
directory structure.  A great contribution back for developing this code
would be to test it until it begs for mercy (try going 20 or 30 (or even
40 or 50 if you feel brave) levels deep and then process as many
children as you have the patience to create in your sample XML file) and
then post back your results to the list so that everyone can benefit
from it.

I still like Michael Kay's solution a million times better (he tends to
write pretty good code ;) but I, like you, have to use 1.0 code for the
time being in production so until the day comes that 2.0 is a choice
this will have to do.

A quick synopsis of the code:

	- I first created a param called 'dirPath' and set it to my
desired test setting, in this case I set it to 'Main/Computers/WWW'.

	- I then created my first template and matched it to root.

	- Within this first template I created a variable called
'dirPathXML' and used call-template to call the 'PathToXML' template
passing it the param 'dirPath' set to the value of the global param
'dirPath'.

	- Within this template I used a named-template and called it
recursively to build an XML data set from the directory structure.  The
code should be pretty straight forward in how this works but in general
it parses the string using the '/' and the current string-length to
determine if it should keep recursively building the tree.  Actually, it
first tests to see if '/' is the first character of the string and if so
calls the template again but this time setting the value of dirPath to
the substring-after '/'.  With this we have allowed ourselves the
ability to test for the presence of '/' as well as the string-length
after the first (and potentially last) occurrence of '/' to determine if
we need to call the template again to catch the remaining values and put
them into the tree structure.  This test statement:

	string-length(substring-after($dirPath, '/')) &gt; 0

will both test for the presence of '/' as well as check the length of
the string contained after '/' if it finds it.  Obviously if the
substring-after function doesn't find an occurrence of '/' the entire
equation will evaluate to false causing the code to fall-through to the
next 'when' block if present or 'otherwise' block if not. If this
happens the only thing left to test for is if there's a '/' at end of
our string (depending on how the directory structure was passed in there
may be a '/' appended to the end).  If there is we need to get rid of it
and then write out the string (the string becomes the value of the
'name' attribute of 'element' btw...) to the output otherwise just write
out the string.  The next 'when' and 'otherwise' block take care of this
for us.

At this point we have contained in our variable a data set that looks a
lot like XML.  However, if we tried to use this variable as the basis of
a select attribute of apply-templates or for-each we would find an error
thrown.  Even though it looks to us like XML it looks like a string or
at best a result tree fragment to the processor and as such we need to
tell the processor to treat this like regular old XML data.  In 1.0 the
only way to do this is with an extension function.  If your processor
doesn't support some sort of nodeset() extension function then you are
most likely not using one of the standard processors as all of the
standard processor vendors utilize some form of the nodeset() function
to allow you to convert transformed data we have stored into a temporary
tree or current flow of the processor into something that we can now
continue to parse as if it was part of the existing XML data flow.  Ive
used Xalan in this example but msxml uses the msxml namespace and
implements using msxml:node-set() while Saxon uses the EXSLT library to
invoke the conversion which uses the exslt namespace and
exslt:node-set() to invoke the function (this would be in place of
xalan:nodeset() which is currently being used.)  Remember to add the
correct xmlns: statement to the xsl:stylesheet declaration to ensure
things go over smoothly.  Just look to the docs for whichever processor
you use to show how to do this correctly.

Once we have our nodeset we can simply use apply-templates and the
elements of the variable we created to continue the processing.  By
passing as a parameter the value of the current directory structure we
can then match each element with the element contained in our variable,
recursively parsing the children of our created nodeset until we find
there are no more children to process.  When this happens we know we
have reached the end of our directory structure (and the whole time we
have been continuing to water down the directory structure using the
name attribute as reference and passing it back to the template as a
parameter) and if we have matched the names of the directory structure
correctly with element names of our directory we can now apply-templates
to the children of the current element in context and process them as we
wish to get the desired styled output.

Note: Notice the use of mode="..." in both the call to apply-templates
as well as the template it matches to. This ensures that the processor
knows which template to process the current 'element' element with as
otherwise it would just use the first template that matched 'element'
which obviously wouldn't work in every case.

I have just put some basic formatting in as the way I plan on outputting
the directory structure may be different form how you would want to do
it.  However, I will make available (on generationXML.com if I ever get
it finished ;) the implementation that I develop simply because it will
be table-less and driven by CSS classes and as such completely
customizable via these same classes making it a pretty universal
solution.  Im not sure when ill get that done but it will be soon and I
will post a link to the list when it happens.  In the mean time, enjoy
the following code and please, if you have the time to pressure cook it,
it would be a huge help and will benefit all the members of the list and
beyond (if they of course were to choose to use this code for there own
project :)

Also, keep in mind that I wrote this in less than an hour and although
conceptually it seems sound I could be completely missing something I
haven't even thought of.  So, if you have any suggestions on how to make
this better, faster, more robust, etc... feel free to make the changes
yourself or let me know your thoughts and Ill see if I can incorporate
them.

Hope this helps!

Best regards,

<M:D/>

The following XML:
<?xml version="1.0"?>
<element name="Main">
  <element name="Business">
    <element name="Finance"/>
  </element>
  <element name="Computers">
    <element name="Internet"/>
    <element name="WWW">
      <element name="Chat"/>
      <element name="DNS"/>
    </element>
  </element>
  <element name="Business"/>
</element>

When processed by this XSLT:

<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform";
    xmlns:xalan="http://xml.apache.org/xalan";
    version="1.0">
  <xsl:param name="dirPath" select="'Main/Computers/WWW'"/>
  
  <xsl:output method="html" indent="yes"/>
  <xsl:template match="/">
    <xsl:variable name="dirPathXML">
      <xsl:call-template name="PathToXML">
        <xsl:with-param name="dirPath" select="$dirPath"/>
      </xsl:call-template>
    </xsl:variable>
    <xsl:variable name="dirXML" select="xalan:nodeset($dirPathXML)"/>
    <xsl:apply-templates select="$dirXML/*" mode="locDir">
      <xsl:with-param name="directory" select="*"/>
    </xsl:apply-templates>
  </xsl:template>
  <xsl:template name="PathToXML">
    <xsl:param name="dirPath"/>
    <xsl:choose>
      <xsl:when test="starts-with($dirPath, '/')">
        <xsl:call-template name="PathToXML">
          <xsl:with-param name="dirPath"
select="substring-after($dirPath, '/')"/>
        </xsl:call-template>
      </xsl:when>
      <xsl:otherwise>
        <xsl:element name="element">
          <xsl:choose>
            <xsl:when test="string-length(substring-after($dirPath,
'/')) &gt; 0">
              <xsl:attribute name="name">
                <xsl:value-of select="substring-before($dirPath, '/')"/>
              </xsl:attribute>
              <xsl:call-template name="PathToXML">
                <xsl:with-param name="dirPath"
select="substring-after($dirPath, '/')"/>
              </xsl:call-template>
            </xsl:when>
            <xsl:when test="contains($dirPath, '/')">
              <xsl:value-of select="substring-before($dirPath, '/')"/>
            </xsl:when>
            <xsl:otherwise>
              <xsl:value-of select="$dirPath"/>
            </xsl:otherwise>
          </xsl:choose>
        </xsl:element>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
  <xsl:template match="element" mode="locDir">
    <xsl:param name="directory"/>
    <xsl:variable name="name" select="@name"/>
    <xsl:choose>
      <xsl:when test="*">
        <xsl:apply-templates select="*" mode="locDir">
          <xsl:with-param name="directory" select="$directory[@name =
$name]/*"/>
        </xsl:apply-templates>
      </xsl:when>
      <xsl:otherwise>
        <table>
          <tr>
            <xsl:apply-templates select="$directory[@name = $name]/*"
mode="outputYahoo"/>
          </tr>
        </table>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
  <xsl:template match="element[last()]" mode="outputYahoo">
    <td>
      <a href="{@name}">&#160;<xsl:value-of select="@name"/>&#160;</a>
    </td>
  </xsl:template>
  <xsl:template match="element" mode="outputYahoo">
    <td>
      <a href="{@name}">&#160;<xsl:value-of select="@name"/>&#160;</a>
    </td>
    <td>|</td>
  </xsl:template>

</xsl:stylesheet>

Will return the following HTML to the output:

<table>
  <tr>
    <td><a href="Chat">&nbsp;Chat&nbsp;</a></td><td>|</td><td><a
href="DNS">&nbsp;DNS&nbsp;</a></td>
  </tr>
</table>

Again, feel free to modify, enhance, or suggest enhancements...  And if
you come up with something really sweet please post it back for the rest
of us to enjoy :D

Thanks!

<M:D/>

> -----Original Message-----
> From: Philipp Burkert [mailto:mailings@xxxxxxxxxx]
> Sent: Tuesday, May 11, 2004 11:07 AM
> To: xsl-list@xxxxxxxxxxxxxxxxxxxxxx
> Subject: RE: [xsl] Generate Yahoo-like directory structure
> 
> Hi,
> 
> Michael, thankx for the quick response. Anyhow I should have made a
note
> that I can not make use of Xpath2. Can you - or someone else - outline
the
> way in Version 1 in more detail?
> 
> Thankx
> 
> PHILIPP BURKERT
> mailings@xxxxxxxxxx

Current Thread