Mike Reddington
Mike Reddington

Reputation: 372

XML to HTML Tables using XSLT or any other method

I have an XML file which contains the following XML data.

<group1>
<item1>val1</item1>
<item2>val2</item2>
<group2>
    <item3>val3</item3>
    <item4>val4</item4>
    <group3>
        <item5>val5</item5>
    </group3>
</group2>
<group2>
    <item3>val6</item3>
    <item4>val7</item4>
    <group3>
        <item5>val8</item5>
    </group3>
</group2>
<group4>
    <item6>val9</item6>
    <item7>val10</item7>
</group4>
<group4>
    <item6>val11</item6>
    <item7>val12</item7>
</group4>

to HTML table like

<table --for group1>
    <tr>
        <th>item1</th>
        <th>item2</th>
    </tr>
    <tr>
        <td>val1</td>
        <td>val2</td>
    </tr>
</table>

<table --for group2>
    <tr>
        <th>item3</th>
        <th>item4</th>
    </tr>
    <tr>
        <td>val3</td>
        <td>val4</td>
    </tr>
</table>

<table --for group3>
    <tr>
        <th>item5</th>
    </tr>
    <tr>
        <td>val5</td>
    </tr>
</table>

<table --for group2>
    <tr>
        <th>item3</th>
        <th>item4</th>
    </tr>
    <tr>
        <td>val6</td>
        <td>val7</td>
    </tr>
</table>

<table --for group3>
    <tr>
        <th>item5</th>
    </tr>
    <tr>
        <td>val8</td>
    </tr>
</table>

<table --for group4>
    <tr>
        <th>item6</th>
        <th>item7</th>
    </tr>
    <tr>
        <td>val9</td>
        <td>val10</td>
    </tr>
    <tr>
        <td>val11</td>
        <td>val12</td>
    </tr>
</table>

In group1 there are two group2's which have group3 inside them. So, each group2 should be a separate table with group3 table following it. There are two group4's but it doesn't have another group inside them. So both group4's should be in a single table.

Note: Each group can have any number of groups nested inside them.

What I'm trying to achieve is to display results of queries like in Oracle bi publisher data model table view.

Able to do it for a fixed depth by having different XSL templates for different depth but don't have any idea on how to do it when depth is not fixed.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
    <HTML>
        <BODY>
            <xsl:apply-templates />
        </BODY>
    </HTML>
</xsl:template>
<xsl:template match="/*">
    <TABLE BORDER="1">
        <TR>
            <xsl:for-each select="*/*">
                <xsl:choose>
                    <xsl:when test="child::*">
                        <!-- <h1>
                            <xsl:value-of select="local-name()" />
                        </h1> -->
                    </xsl:when>
                    <xsl:otherwise>
                        <!-- <h1>
                        <xsl:value-of select="local-name()" />
                    </h1> -->
                        <xsl:for-each select=".">
                            <TD>
                                <xsl:value-of select="local-name()" />
                            </TD>
                        </xsl:for-each>
                    </xsl:otherwise>
                </xsl:choose>
            </xsl:for-each>
            <xsl:apply-templates />
        </TR>
    </TABLE>
</xsl:template>
<xsl:template match="/*/*">
    <TR>
        <xsl:for-each select="*">
            <xsl:choose>
                <xsl:when test="child::*">
                    <h1>
                        <xsl:value-of select="local-name()" />
                    </h1>
                </xsl:when>
                <xsl:otherwise>
                    <td>
                        <xsl:apply-templates select="." />
                    </td>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:for-each>
    </TR>
</xsl:template>
<!-- <xsl:template match="/*/*/*">
    <xsl:choose>
        <xsl:when test="child::*">
            <TABLE BORDER="1">
                <TR>
                    <xsl:for-each select="*/*">
                        <TD>
                            <xsl:value-of select="local-name()" />
                        </TD>
                    </xsl:for-each>
                </TR>
            </TABLE>
            <xsl:value-of select="." />
        </xsl:when>
        <xsl:otherwise>
            <xsl:value-of select="." />
        </xsl:otherwise>
    </xsl:choose>
</xsl:template> -->

Upvotes: 0

Views: 449

Answers (1)

Martin Honnen
Martin Honnen

Reputation: 167516

It seems for most part you just want to flatten the nested hierarchy, so use recursion for that. Furthermore, to merge/group the deepest level, use grouping, for instance in XSLT 1, with Muenchian grouping and key:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    version="1.0">

  <xsl:output method="html" indent="yes" version="5" doctype-system="about:legacy-doctype"/>

  <xsl:key name="table-group"
    match="*[* and not(*/*)]"
    use="concat(generate-id(..), '|', local-name())"/>

  <xsl:template match="*[*]">
      <table>
          <thead>
              <tr>
                  <xsl:apply-templates select="*[not(*)]" mode="header"/>
              </tr>
          </thead>
          <tbody>
              <tr>
                  <xsl:apply-templates select="*[not(*)]"/>
              </tr>
          </tbody>
      </table>
      <xsl:apply-templates select="*[*/*]"/>
      <xsl:apply-templates
        select="*[* and not(*/*)][generate-id() = generate-id(key('table-group', concat(generate-id(..), '|', local-name()))[1])]" mode="merge-groups"/>
  </xsl:template>

  <xsl:template match="*[not(*)]" mode="header">
      <th>
          <xsl:value-of select="local-name()"/>
      </th>
  </xsl:template>

  <xsl:template match="*[not(*)]">
      <td>
          <xsl:value-of select="."/>
      </td>
  </xsl:template>

  <xsl:template match="*[*]" mode="merge-groups">
      <table>
          <thead>
              <tr>
                  <xsl:apply-templates select="*[not(*)]" mode="header"/>
              </tr>
          </thead>
          <tbody>
              <xsl:apply-templates select="key('table-group', concat(generate-id(..), '|', local-name()))" mode="row"/>
          </tbody>
      </table>
  </xsl:template>

  <xsl:template match="*" mode="row">
      <tr>
          <xsl:apply-templates select="*"/>
      </tr>
  </xsl:template>

  <xsl:template match="/">
    <html>
      <head>
        <title>.NET XSLT Fiddle Example</title>
      </head>
      <body>
        <xsl:apply-templates/>
      </body>
    </html>
  </xsl:template>

</xsl:stylesheet>

https://xsltfiddle.liberty-development.net/ncnu9AZ/1

If you are new to Muenchian grouping in XSLT 1, please use any textbook or online introduction on Muenchian grouping in XSLT 1 to familiarize you with that approach, one online source is http://www.jenitennison.com/xslt/grouping/muenchian.html.

In the above stylesheet, as there is a need to group elements not further nested only in the context of the parent container, the used key includes the generated id of the parent element (generate-id(...)). Therefore calling key('table-group', concat(generate-id(..), '|', local-name())) in the context of an element gives us a node-set with a group of elements with the same name inside the same parent. And with the whole predicate [generate-id() = generate-id(key('table-group', concat(generate-id(..), '|', local-name()))[1])] we simply establish that the first element in document order in that node-set is currently being processed. The generate-id() = generate-id(key(...)[1]) is basically an XSLT 1 way of expressing what could be done in XSLT 2 or later as . is key(...)[1], that is, to check node identity of the context element in the predicate and the first node returned by that key(...) call.

Upvotes: 1

Related Questions