开发者

How to use XSLT recursion to convert any xml data to a html table view:

开发者 https://www.devze.com 2023-01-25 12:46 出处:网络
For a given xml, I need to generate a html table to represent the values in the xml. I开发者_JAVA技巧 need the recursion for any keyN, if valueN is text then just print it. If valueN is xml, then prin

For a given xml, I need to generate a html table to represent the values in the xml. I开发者_JAVA技巧 need the recursion for any keyN, if valueN is text then just print it. If valueN is xml, then print a (nested)table with it's value. I think my lack of understanding of how to use XSLT recursion correctly is the base of the question. Any help appreciated.

Input:

<root>
    <key1> Text Value  </key1>
<key2> 
    <a> aaa </a>
    <b> bbb </b>
</key2>
<keyN> valueN </keyN>
<root>

Output:

<table border="1px">
    <tr>
        <td> key1 </td>
        <td> Text Value </td>
    </tr>

    <tr>
        <td> key2 </td>
        <td>
            <table border="1px">
                <tr> <td> a </td> <td> aaa </td> </tr>
                <tr> <td> b </td> <td> bbb </td> </tr>
            </table>
        </td>
    </tr>

    <tr> 
        <td> keyN </td>
        <td>
            valueN (if valueN is text)
                    OR
            <table> ... </table> (if valueN is xml)
        <td>
    </tr>
</table>


This stylesheet:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:template match="/*//*[1]">
        <table border="1">
            <xsl:call-template name="makeRow"/>
        </table>
    </xsl:template>
    <xsl:template match="*" name="makeRow">
        <tr>
            <td>
                <xsl:value-of select="name()"/>
            </td>
            <td>
                <xsl:apply-templates select="node()[1]"/>
            </td>
        </tr>
        <xsl:apply-templates select="following-sibling::node()[1]"/>
    </xsl:template>
    <xsl:template match="/*">
        <xsl:apply-templates select="node()[1]"/>
    </xsl:template>
</xsl:stylesheet>

Output:

<table border="1">
    <tr>
        <td>key1</td>
        <td> Text Value  </td>
    </tr>
    <tr>
        <td>key2</td>
        <td>
            <table border="1">
                <tr>
                    <td>a</td>
                    <td> aaa </td>
                </tr>
                <tr>
                    <td>b</td>
                    <td> bbb </td>
                </tr>
            </table></td>
    </tr>
    <tr>
        <td>keyN</td>
        <td> valueN </td>
    </tr>
</table>

Note: This use the fine grained traversal patter. Three rules: "first child descendant of root element", output table and call makeRow; makeRow (match any element not being the first child descendant nor root element) output tr and table cells with name and first child applycation, then apply templates to next sibling; root element rule, start the fine grained traversal.


This transformation:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <xsl:template match="/*">
  <table border="1px">
   <xsl:apply-templates/>
  </table>
 </xsl:template>

 <xsl:template match="*[*][parent::*]">
     <tr>
      <td><xsl:value-of select="name()"/></td>
      <td>
        <table border="1px">
          <xsl:apply-templates/>
        </table>
      </td>
     </tr>
 </xsl:template>

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

when applied on the provided XML document:

<root>
    <key1> Text Value  </key1>
    <key2>
        <a> aaa </a>
        <b> bbb </b>
    </key2>
    <keyN> valueN </keyN>
</root>

produces the wanted, correct result:

<table border="1px">
   <tr>
      <td>key1</td>
      <td> Text Value  </td>
   </tr>
   <tr>
      <td>key2</td>
      <td>
         <table border="1px">
            <tr>
               <td>a</td>
               <td> aaa </td>
            </tr>
            <tr>
               <td>b</td>
               <td> bbb </td>
            </tr>
         </table>
      </td>
   </tr>
   <tr>
      <td>keyN</td>
      <td> valueN </td>
   </tr>
</table>

Do note the power of XSLT:

  1. No explicit recursion.

  2. No conditionals inside any template.

  3. Completely push-style of processing.


<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:template match="/">
        <html>
            <body>
                <!-- apply root as a table -->
                <xsl:apply-templates select="root" mode="table"/>
            </body>
        </html>
    </xsl:template>

    <xsl:template match="node()[not(self::text())]" mode="table">
        <table border="1px">
            <!-- create table and process children as rows -->
            <xsl:apply-templates select="node()" mode="row"/>
        </table>
    </xsl:template>

    <xsl:template match="node()[not(self::text())]" mode="row">
        <tr>
            <!-- make decision here. 
                 If row contains only text - apply is as a simple row
                 In case there are some other nodes proces them as a table.
           -->
            <xsl:choose>
                <xsl:when test=". = text()">
                    <xsl:apply-templates select="." mode="column"/>
                </xsl:when>
                <xsl:otherwise>
                    <td><xsl:value-of select="name()"/></td>
                    <td><xsl:apply-templates select="." mode="table"/></td>
                </xsl:otherwise>
            </xsl:choose>
        </tr>
    </xsl:template>

    <xsl:template match="node()" mode="column">
        <td><xsl:value-of select="name()"/></td>
        <td><xsl:value-of select="."/></td>
    </xsl:template>
</xsl:stylesheet>

Output:

<html>
    <body>
        <table border="1px">
            <tr>
                <td>key1</td>
                <td>Text Value</td>
            </tr>

            <tr>
                <td>key2</td>
                <td>
                <table border="1px">
                    <tr>
                        <td>a</td>
                        <td>aaa</td>
                    </tr>

                    <tr>
                        <td>b</td>
                        <td>bbb</td>
                    </tr>
                </table>
                </td>
            </tr>

            <tr>
                <td>keyN</td>
                <td>valueN</td>
            </tr>

        </table>
    </body>
</html>

BTW it will be better to rewrite xsl:choose as two separate templates.


This sounds like homework :-)

You're working with 2 things here:

  1. Arbitrarily named elements that you need to apply an XSLT template to, and
  2. Determining what type of content an element has (either text or an XML fragment).

You want to use a template that can match elements in your XML arbitrarily (i.e. not elements with a specific name). <xsl:template match="*"> will match all elements in your XML document.

When you encounter an element, you need to create a table:

<xsl:template match="*">
  <table border="1px">
  </table>
</xsl:template>

Now we want to work out whether we're dealing with an XML fragment (an element) or a piece of text. To do this we match on node(). Remember that a node can be an element, text, whitespace, processing instruction or a comment in an XML document. When you've matched a node, you want to create a new table row and display the name of the current node:

<xsl:template match="node()">
  <tr>
    <td>
      <xsl:value-of select="local-name()"/>
    </td>
  </tr>
</xsl:template>

You then need to work out whether the node is a text node or not. You can use an <xsl:if> or an <xsl:choose>. I tend to prefer the latter. If it's a text node, display the value of the text otherwise treat the node like an XML fragment and call our initial template again (this is the recursive part).

...
<xsl:choose>
  <xsl:when test="current() = text()">
    <td>
      <xsl:value-of select="." />
    </td>
  </xsl:when>
  <xsl:otherwise>
    <td>
      <xsl:apply-templates select="*" mode="table"/>
    </td>
  </xsl:otherwise>
</xsl:choose>
...

Here's the final solution.

<xsl:template match="/root">
  <xsl:apply-templates select="*" mode="table" />
</xsl:template>

<xsl:template match="*" mode="table">
  <table border="1px">
    <xsl:apply-templates select="." mode="table-row" />
  </table>
</xsl:template>

<xsl:template match="node()" mode="table-row">
  <tr>
    <td>
      <xsl:value-of select="local-name()"/>
    </td>
    <xsl:choose>
      <xsl:when test="current() = text()">
        <td>
          <xsl:value-of select="." />
        </td>
      </xsl:when>
      <xsl:otherwise>
        <td>
          <xsl:apply-templates select="*" mode="table"/>
        </td>
      </xsl:otherwise>
    </xsl:choose>
  </tr>
</xsl:template>

I'm using the mode attribute on the template because an element is also a node in an XML document and <xsl:apply-templates select="*"/> would match both <xsl:template match="*"> as well as <xsl:template match="node()">. Using the mode attribute removes the ambiguity.


ADDED FUNCTIONALITY FOR DISPLAYING ATTRIBUTES

    <xsl:stylesheet version="1.0"
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
        <xsl:template match="/*//*[1]">
            <table border="1">
                <xsl:call-template name="makeRow" />
            </table>
        </xsl:template>
        <xsl:template match="*" name="makeRow">
            <tr>
                <td><xsl:value-of select="name()" /> <xsl:if
                    test="count(@*) > 0">
                    <table>
                        <xsl:apply-templates select="@*" />
                    </table>
                </xsl:if></td>
                <td><xsl:apply-templates select="node()[1]" /></td>
            </tr>
            <xsl:apply-templates select="following-sibling::node()[1]" />
        </xsl:template>
        <xsl:template match="/*">
            <xsl:apply-templates select="node()[1]" />
        </xsl:template>
        <xsl:template match="@*">
            <tr>
                <td>@<xsl:value-of select="name()" /></td>
                <td><xsl:value-of select="." /></td>
            </tr>
        </xsl:template>
    </xsl:stylesheet>
0

精彩评论

暂无评论...
验证码 换一张
取 消