tags:

views:

1729

answers:

5

hi all,

i use a minimalist MVC framework, where the PHP controler hands the DOM model to the XSLT view (c.f. okapi).

in order to build a navigation tree, i used nested sets in MYSQL. this way, i end up with a model XML that looks as follows:

<tree>
    <node>
        <name>root</name>
        <depth>0</depth>
    </node>
    <node>
        <name>TELEVISIONS</name>
        <depth>1</depth>
    </node>
    <node>
        <name>TUBE</name>
        <depth>2</depth>
    </node>
    <node>
        <name>LCD</name>
        <depth>2</depth>
    </node>
    <node>
        <name>PLASMA</name>
        <depth>2</depth>
    </node>
    <node>
        <name>PORTABLE ELECTRONICS</name>
        <depth>1</depth>
    </node>
    <node>
        <name>MP3 PLAYERS</name>
        <depth>2</depth>
    </node>
    <node>
        <name>FLASH</name>
        <depth>3</depth>
    </node>
    <node>
        <name>CD PLAYERS</name>
        <depth>2</depth>
    </node>
    <node>
        <name>2 WAY RADIOS</name>
        <depth>2</depth>
    </node>
</tree>

which represents the following structure

  • root
    • TELEVISIONS
      • TUBE
      • LCD
      • PLASMA
    • PORTABLE ELECTRONICS
      • MP3 PLAYERS
        • FLASH
      • CD PLAYERS
      • 2 WAY RADIOS

now, i can't seem to find a way to get from my XML to this html list using XSLT. does anyone have a suggestion?

cheers ... pierre

PS: this is the example tree from the nested sets tutorial on dev.mysql.com

+1  A: 

That form of flat list is very hard to work with in xslt, as you need to find the position of the next grouping, etc. Can you use different xml? For example, with the flat xml:

<?xml version="1.0" encoding="utf-8" ?>
<tree>
  <node key="0">root</node>
  <node key="1" parent="0">TELEVISIONS</node>
  <node key="2" parent="1">TUBE</node>
  <node key="3" parent="1">LCD</node>
  <node key="4" parent="1">PLASMA</node>
  <node key="5" parent="0">PORTABLE ELECTRONICS</node>
  <node key="6" parent="5">MP3 PLAYERS</node>
  <node key="7" parent="6">FLASH</node>
  <node key="8" parent="5">CD PLAYERS</node>
  <node key="9" parent="5">2 WAY RADIOS</node>
</tree>

It becomes trivial to do (very efficiently):

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
     xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
  <xsl:key name="nodeChildren" match="/tree/node" use="@parent"/>
  <xsl:template match="tree">
    <ul>
      <xsl:apply-templates select="node[not(@parent)]"/>
    </ul>
  </xsl:template>
  <xsl:template match="node">
    <li>
      <xsl:value-of select="."/>
      <ul>
        <xsl:apply-templates select="key('nodeChildren',@key)"/>
      </ul>
    </li>
  </xsl:template>
</xsl:stylesheet>

Is that an option?

Of course, if you build the xml as a hierarchy it is even easier ;-p

Marc Gravell
A: 

You could check out the Muenchian Method of grouping.

Kees de Kooter
A: 

You haven't actually said what you'd like the html output to look like, but I can tell you that from an XSLT point of view going from a flat structure to a tree is going to be complex and expensive if you're also basing this on the position of items in the tree and their relation to siblings.

It would be far better to supply a <parent> attribute/node than the <depth>.

annakata
+1  A: 

In XSLT 2.0 it would be rather easy with the new grouping functions.

In XSLT 1.0 it's a little more complicated but this works:

<xsl:template match="/tree">
    <xhtml>
     <head/>
     <body>
      <ul>
       <xsl:apply-templates select="node[depth='0']"/>
       </ul>
      </body>
     </xhtml>
    </xsl:template>

<xsl:template match="node">
    <xsl:variable name="thisNodeId" select="generate-id(.)"/>
    <xsl:variable name="depth" select="depth"/>
    <xsl:variable name="descendants">
     <xsl:apply-templates select="following-sibling::node[depth = $depth + 1][preceding-sibling::node[depth = $depth][1]/generate-id() = $thisNodeId]"/>
     </xsl:variable>
    <li>
     <xsl:value-of select="name"/>
     </li>
    <xsl:if test="$descendants/*">
     <ul>
      <xsl:copy-of select="$descendants"/>
      </ul>
     </xsl:if>
    </xsl:template>

The heart of the matter is the long and ugly "descendants" variable, which looks for nodes after the current node that have a "depth" child greater than the current depth, but are not after another node that would have the same depth as the current depth (because if they were, they would be children of that node instead of the current one).

BTW there is an error in your example result: "FLASH" should be a child of "MP3 PLAYERS" and not a sibling.

EDIT

In fact (as mentionned in the comments), in "pure" XSLT 1.0 this does not work for two reasons: the path expression uses generate-id() incorrectly, and one cannot use a "result tree fragment" in a path expression.

Here is a correct XSLT 1.0 version of the "node" template (successfully tested with Saxon 6.5) that does not use EXSLT nor XSLT 1.1:

<xsl:template match="node">
    <xsl:variable name="thisNodeId" select="generate-id(.)"/>
    <xsl:variable name="depth" select="depth"/>
    <xsl:variable name="descendants">
     <xsl:apply-templates select="following-sibling::node[depth = $depth + 1][generate-id(preceding-sibling::node[depth = $depth][1]) = $thisNodeId]"/>
     </xsl:variable>
    <xsl:variable name="descendantsNb">
     <xsl:value-of select="count(following-sibling::node[depth = $depth + 1][generate-id(preceding-sibling::node[depth = $depth][1]) = $thisNodeId])"/>
     </xsl:variable>
    <li>
     <xsl:value-of select="name"/>
     </li>
    <xsl:if test="$descendantsNb &gt; 0">
     <ul>
      <xsl:copy-of select="$descendants"/>
      </ul>
     </xsl:if>
    </xsl:template>

Of course, one should factor the path expression that is repeated, but without the ability to turn "result tree fragments" into XML that can actually be processed, I don't know if it's possible? (writing a custom function would do the trick of course, but then it's much simpler to use EXSLT)

Bottom line: use XSLT 1.1 or EXSLT if you can!

2nd Edit

In order to avoid to repeat the path expression, you can also forget the test altogether, which will simply result in some empty that you can either leave in the result or post-process to eliminate.

that is just what i was looking for! cheers!
Pierre Spring
it seems like you can not do things like <xsl:if test="$descendants/*"> … but using EXSLT you can do <xsl:if test="exslt:node-set($descendants)/*"> …
Pierre Spring
A: 

very helpful!

one suggestion is moving the < ul > inside the template would remove the empty ul.

Shawn