Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I build a tree from a flat XML list using XSLT?

Tags:

tree

xslt

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

How can I convert this flat XML list to a nested HTML list using XSLT?

PS: this is the example tree from the Managing Hierarchical Data in MySQL.

like image 558
Pierre Spring Avatar asked Mar 10 '09 10:03

Pierre Spring


2 Answers

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">
  <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

like image 193
Marc Gravell Avatar answered Nov 02 '22 22:11

Marc Gravell


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.

like image 20
Bambax Avatar answered Nov 02 '22 22:11

Bambax