tags:

views:

804

answers:

2

I’ve got some hierarchical XML like this:

<node text="a" value="1">
  <node text="gga" value="5">  
    <node text="dh" value="9">
      <node text="tyfg" value="4">  
      </node>  
    </node>  
  </node>  
  <node text="dfhgf" value="7">  
    <node text="fdsg" value="2">  
    </node>  
  </node>  
</node>

The names of the elements are the same all the way down (“node”), and the depth of the hierarchy isn’t known beforehand – in the above sample the deepest leaf is four down, but it can be of any depth.

What I need to do is take this XML and flatten it into a HTML table. The number of columns in the table should equal the depth of the deepest element, plus a column for the value attribute of each element. The "value" should appear in the rightmost column of the table, so the output rows cannot have ragged edges. There should be a row for each node regardless of what level it’s at. The above example should be transformed into:

<table>
  <tr>
    <td>a</td>
    <td></td>
    <td></td>
    <td></td>
    <td>1</td>
  </tr>
  <tr>
    <td>a</td>
    <td>gga</td>
    <td></td>
    <td></td>
    <td>5</td>
  </tr>
  <tr>
    <td>a</td>
    <td>gga</td>
    <td>dh</td>
    <td></td>
    <td>9</td>
  </tr>
  <tr>
    <td>a</td>
    <td>gga</td>
    <td>dh</td>
    <td>tyfg</td>
    <td>4</td>
  </tr>
  <tr>
    <td>a</td>
    <td>dfhgf</td>
    <td></td>
    <td></td>
    <td>7</td>
  </tr>
  <tr>
    <td>a</td>
    <td>dfhgf</td>
    <td>fdsg</td>
    <td></td>
    <td>2</td>
  </tr>
</table>

Anybody got some clever XSLT that can achieve this?

+3  A: 

It's not quite what you need (because it leaves a jagged table) but it'll still work in html

<xsl:template match="/">
    <html>
     <head>
     </head>
     <body>
      <table>
       <xsl:apply-templates select="//node" mode="row" />
      </table>
     </body>
    </html>
</xsl:template>

<xsl:template match="node" mode="row">
    <tr>
     <xsl:apply-templates select="ancestor-or-self::node" mode="popcell"/> 
     <xsl:apply-templates select="node[1]" mode="emptycell"/>
    </tr>
</xsl:template>

<xsl:template match="node" mode="popcell">
    <td><xsl:value-of select="@text"/></td>
</xsl:template>

<xsl:template match="node" mode="emptycell">
    <td></td>
    <xsl:apply-templates select="node[1]" mode="emptycell"/>
</xsl:template>


Version 2: Well I'm considerably less self-satisfied with it :P , but the following removes the jaggedness:

<xsl:variable name="depth">
    <xsl:for-each select="//node">
     <xsl:sort select="count(ancestor::node)" data-type="number" order="descending"/>
     <xsl:if test="position()=1">
      <xsl:value-of select="count(ancestor::node)+1"/>
     </xsl:if>
    </xsl:for-each>
</xsl:variable>

<xsl:template match="/">
    <html>
     <head>
     </head>
     <body>
      <table>
       <xsl:apply-templates select="//node" mode="row" />
      </table>
     </body>
    </html>
</xsl:template>

<xsl:template match="node" mode="row">
    <tr>
     <xsl:apply-templates select="ancestor-or-self::node" mode="popcell"/>
     <xsl:call-template name="emptycells">
      <xsl:with-param name="n" select="($depth)-count(ancestor-or-self::node)"/>
     </xsl:call-template>
     <td><xsl:value-of select="@value"/></td>
    </tr>
</xsl:template>

<xsl:template match="node" mode="popcell">
    <td><xsl:value-of select="@text"/></td>
</xsl:template>

<xsl:template name="emptycells">
    <xsl:param name="n" />
    <xsl:if test="$n &gt; 0">
     <td></td> 
     <xsl:call-template name="emptycells">
      <xsl:with-param name="n" select="($n)-1"/>
     </xsl:call-template>
    </xsl:if>
</xsl:template>
annakata
+1 This is an amazing xslt :)
Rashmi Pandit
Thanks :) Sometimes the elegance of XSLT really is astonishing.
annakata
Hi annakata, thanks for this! It's close to what I need, unfortunately the jagged table is a problem. In my first draft of the question I simplified my problem. I've amended it now, so the reason I can't have jagged edges should be clear.
Matt
Bah, you've ruined my beautiful XSL.
annakata
Annakata, so sorry for ruining your beautiful XSL, but your 2nd version does exactly what I need - thank you so much!
Matt
A: 

This XSLT 1.0 solution would do it.

  • produces a well-formed HTML table
  • no recursion used

XSLT code:

<xsl:stylesheet
  version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>

  <xsl:output method="xml" indent="yes" omit-xml-declaration="yes" />

  <!-- some preparation -->
  <xsl:variable name="vAllNodes" select="//node" />

  <!-- find out the deepest nested node -->
  <xsl:variable name="vMaxDepth">
    <xsl:for-each select="$vAllNodes">
      <xsl:sort 
        select="count(ancestor::node)" 
        data-type="number" 
        order="descending" 
      />
      <xsl:if test="position() = 1">
        <xsl:value-of select="count(ancestor-or-self::node)" />
      </xsl:if>
    </xsl:for-each>
  </xsl:variable>

  <!-- select a list of nodes, merely to iterate over them -->
  <xsl:variable name="vIteratorList" select="
    $vAllNodes[position() &lt;= $vMaxDepth]
  " />

  <!-- build the table -->
  <xsl:template match="/">
    <table>
      <!-- the rows will be in document order -->
      <xsl:apply-templates select="$vAllNodes" />
    </table>
  </xsl:template>

  <!-- build the rows -->
  <xsl:template match="node">
    <xsl:variable name="self" select="." />
    <tr>
      <!-- iteration instead of recursion -->
      <xsl:for-each select="$vIteratorList">
        <xsl:variable name="vPos" select="position()" />
        <td>
          <!-- the ancestor axis is indexed the other way around -->
          <xsl:value-of select="
            $self/ancestor-or-self::node[last() - $vPos + 1]/@text
          " />
        </td>
      </xsl:for-each>
      <td>
        <xsl:value-of select="@value" />
      </td>
    </tr>
  </xsl:template>

</xsl:stylesheet>

Output:

<table>
  <tr>
    <td>a</td>
    <td></td>
    <td></td>
    <td></td>
    <td>1</td>
  </tr>
  <tr>
    <td>a</td>
    <td>gga</td>
    <td></td>
    <td></td>
    <td>5</td>
  </tr>
  <tr>
    <td>a</td>
    <td>gga</td>
    <td>dh</td>
    <td></td>
    <td>9</td>
  </tr>
  <tr>
    <td>a</td>
    <td>gga</td>
    <td>dh</td>
    <td>tyfg</td>
    <td>4</td>
  </tr>
  <tr>
    <td>a</td>
    <td>dfhgf</td>
    <td></td>
    <td></td>
    <td>7</td>
  </tr>
  <tr>
    <td>a</td>
    <td>dfhgf</td>
    <td>fdsg</td>
    <td></td>
    <td>2</td>
  </tr>
</table>
Tomalak
Thanks for this Tomalak, it works very well. I made one tiny change to get the output I wanted -<xsl:value-of select="$vNodes[last() - $vPos + 1]/@text" />changed to:<xsl:value-of select="$vNodes[$vPos]/@text" />Otherwise the text cells in the row are in reverse order (i.e. leaf node text is in the leftmost column)
Matt
Actually, this ought not to be happening. $vNodes is made from the ancestor axis, meaning that $vNodes[1] should be the leaf. What XSLT processor are you using?
Tomalak
Visual Studio 2008
Matt
Hm. I've changed it so that it uses the "ancestor-or-self" axis directly instead of a variable. Can you try again?
Tomalak
I tried the change and now what I see is that every row in the output table contains the same text cells. The text in these cells corresponds to whatever the text of the longest path in the tree is. Hope I've explained that clearly!
Matt
Whoops, my bad. This has been fixed, try again.
Tomalak