XSLT dynamically creating rows and columns

2019-05-31 05:36发布

问题:

I have an XML file with following format:

 <DataSet>
  <Data id ="1" columns ="4">
    <item name ="data1" value="value1"/>
    <item name ="data2" value="value2"/>
    <item name ="data3" value="value3"/>
    <item name ="data4" value="value4"/>
    <item name ="data5" value="value5"/>
  </Data>
  <Data id="2" columns ="2">
    <item name ="data1" value="value1"/>
    <item name ="data2" value="value2"/>
    <item name ="data3" value="value3"/>
    <item name ="data4" value="value4"/>
  </Data>
</DataSet>

and I need an XSL transformation to get following table structure. Here the idea is to display the name and value attributes in two adjacent cells. So an 'item' will be associated with 2 columns and a row will be holding name/value pairs of two items. The number of columns will be specified in the Data element and will be always multiples of 2.

<report>
  <table>
    <tr>
      <td>data1</td>
      <td>value1</td>
      <td>data2</td>
      <td>value2</td>
    </tr>
    <tr>
      <td>data3</td>
      <td>value3</td>
      <td>data4</td>
      <td>value4</td>
    </tr>
    <tr>
      <td>data5</td>
      <td>value5</td>
      <td></td>
      <td></td>
    </tr>
 </table>
 <table>
    <tr>
      <td>data1</td>
      <td>value1</td>
    </tr>
    <tr>
      <td>data2</td>
      <td>value2</td>
    </tr>
    <tr>
      <td>data3</td>
      <td>value3</td>
    </tr>
    <tr>
      <td>data4</td>
      <td>value4</td>
    </tr>
  </table>
</report>

回答1:

The following XSL transformation applied to the provided input produces the desired output. Some explanation is provided below.

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

    <xsl:template match="/DataSet"><report>
        <xsl:apply-templates select="@*|node()" />
    </report></xsl:template>

    <xsl:template match="Data">
        <xsl:variable name="count" select="count(item)" />
        <xsl:variable name="M" select="@columns div 2" />
        <xsl:variable name="N" select="($count + ($count mod $M)) div $M" />
        <table>
            <xsl:call-template name="nth-row">
                <xsl:with-param name="n" select="1" />
                <xsl:with-param name="M" select="$M" />
                <xsl:with-param name="N" select="$N" />
            </xsl:call-template>
        </table>
    </xsl:template>

    <xsl:template name="nth-row">
        <xsl:param name="n" />
        <xsl:param name="N" />
        <xsl:param name="M" />
        <tr>
            <xsl:call-template name="nmth-cell">
                <xsl:with-param name="n" select="$n" />
                <xsl:with-param name="m" select="1" />
                <xsl:with-param name="N" select="$N" />
                <xsl:with-param name="M" select="$M" />
            </xsl:call-template>
        </tr>
        <xsl:if test="$N > $n">
            <xsl:call-template name="nth-row">
                <xsl:with-param name="n" select="$n + 1" />
                <xsl:with-param name="N" select="$N" />
                <xsl:with-param name="M" select="$M" />
            </xsl:call-template>
        </xsl:if>
    </xsl:template>

    <xsl:template name="nmth-cell">
        <xsl:param name="n" />
        <xsl:param name="m" />
        <xsl:param name="N" />
        <xsl:param name="M" />
        <xsl:variable name="pos" select="($n - 1) * $M + $m" />
        <xsl:choose>
            <xsl:when test="item[position()=$pos]">
                <td><xsl:value-of select="item[position()=$pos]/@name" /></td>
                <td><xsl:value-of select="item[position()=$pos]/@value" /></td>
            </xsl:when>
            <xsl:otherwise>
                <td></td>
                <td></td>
            </xsl:otherwise>
        </xsl:choose>
        <xsl:if test="$M > $m">
            <xsl:call-template name="nmth-cell">
                <xsl:with-param name="n" select="$n" />
                <xsl:with-param name="m" select="$m + 1" />
                <xsl:with-param name="N" select="$N" />
                <xsl:with-param name="M" select="$M" />
            </xsl:call-template>
        </xsl:if>
    </xsl:template>
</xsl:transform>
  1. Matching /DataSet produces the root element <report /> and continues applying templates.

  2. Matching Data from inside /DataSet produces a <table /> for each <Data /> element and then starts the interesting part by calling the template named nth-row. The variables and parameters used are:

    • n: number of current row, starting at 1
    • M: number of columns, calculated from the attribute @columns divided by two, because each <item /> results in two <td /> elements.
    • N: number of rows, calculated from the number of <item /> elements present and divided by M. To account for div truncating integer values the remainder $count mod $M is added to $count before.
  3. Now there come some recursive template calls. Each time nth-row is called, it outputs a <tr /> and then calls nmth-cell with appropriate parameters. As long as the current row is not the last one, nth-row is recursively called with an incremented value of $n.

  4. Finally the template nmth-cell each time it is called outputs two <td /> elements containing the values from the appropriate <item /> or nothing, if there is no corresponding <item />. As long as the current column is not the last one, nmth-cell is recursively called with an incremented value of $m.

I hope this helps. Feel free to ask, if there is anything wrong with this or unclear to you.



回答2:

Here is a much simpler solution.

This XSLT 1.0 style-sheet...

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

<xsl:template match="/*">
  <report>
    <xsl:apply-templates />
  </report>
</xsl:template>

<xsl:template match="Data">
  <xsl:variable name="cols" select="@columns" />
  <table>
    <xsl:for-each select="item[position()*2 mod $cols = (2 mod $cols)]">
      <tr>
       <xsl:for-each select="(.|following-sibling::item)
          [ position()*2 &lt;= $cols]">
         <td><xsl:value-of select="@name" /></td>  
         <td><xsl:value-of select="@value" /></td>  
       </xsl:for-each> 
       <xsl:if test="position()=last()">
         <xsl:for-each select="((/)//*)[position() &lt;=
              ($cols - (count(.|following-sibling::item)*2))]">
            <td />
         </xsl:for-each>   
       </xsl:if>  
      </tr>  
    </xsl:for-each>  
  </table>
</xsl:template>

</xsl:stylesheet>

...when applied to this input...

<DataSet>
  <Data id ="1" columns ="4">
    <item name ="data1" value="value1"/>
    <item name ="data2" value="value2"/>
    <item name ="data3" value="value3"/>
    <item name ="data4" value="value4"/>
    <item name ="data5" value="value5"/>
  </Data>
  <Data id="2" columns ="2">
    <item name ="data1" value="value1"/>
    <item name ="data2" value="value2"/>
    <item name ="data3" value="value3"/>
    <item name ="data4" value="value4"/>
  </Data>
</DataSet>   

...yields...

<report>
  <table>
    <tr>
      <td>data1</td>
      <td>value1</td>
      <td>data2</td>
      <td>value2</td>
    </tr>
    <tr>
      <td>data3</td>
      <td>value3</td>
      <td>data4</td>
      <td>value4</td>
    </tr>
    <tr>
      <td>data5</td>
      <td>value5</td>
      <td></td>
      <td></td>
    </tr>
  </table>
  <table>
    <tr>
      <td>data1</td>
      <td>value1</td>
    </tr>
    <tr>
      <td>data2</td>
      <td>value2</td>
    </tr>
    <tr>
      <td>data3</td>
      <td>value3</td>
    </tr>
    <tr>
      <td>data4</td>
      <td>value4</td>
    </tr>
  </table>
</report>

Explanation

  1. You can very simple use position() to structure the output into a matrix. This is far preferable to a convoluted call-template with many parameters.
  2. Use the html output method instead of the default xml output method. This gives your html style of encoding for empty elements (like <td></td> over xml style </td>).
  3. Use the Piez Method to emit the odd empty table cells on the last row of the table.


回答3:

I don't know exactly what formatting you want but this should get you close i hope. It makes a table per data set with the data and value pairs next to each other. Just comment if you need formatting help too

<?xml version="1.0" encoding="ISO-8859-1"?>
<!-- Edited by XMLSpy® -->
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:template match="/">
  <html>
  <body>
  <report>
      <xsl:for-each select="DataSet/Data">
      <table>
          <xsl:for-each select="item">
              <tr>
                  <td><xsl:value-of select="@name"/></td>
                  <td><xsl:value-of select="@value"/></td>
              </tr>
          </xsl:for-each>
       </table>
       </xsl:for-each>
  </report>
  </body>
  </html>
</xsl:template>



标签: xslt