xsl: transforming a list into a 2-D table

前端 未结 4 1185
星月不相逢
星月不相逢 2021-01-12 16:32

Let\'s say I have this XML node:


    ...
    ...
    ...
    

        
相关标签:
4条回答
  • 2021-01-12 17:11

    With for-each-group you can get a more elegant solution:

    <xsl:template match="items">
      <table>
        <xsl:for-each-group select="item" group-by="ceiling(position() div $column_width)">
          <tr>
            <xsl:for-each select="current-group()">
              <td>
                <xsl:apply-templates/>
              </td>
            </xsl:for-each>
          </tr>
        </xsl:for-each-group>
      </table>
    </xsl:template>
    
    0 讨论(0)
  • 2021-01-12 17:13

    That's my working solution.

    As you didn't provide a desired output, this particular one may be uncomplete for your needs.

    <xsl:stylesheet
        version="1.0"
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
        <xsl:output method="html" indent="yes"/>
    
        <xsl:template match="/*">
            <table>
                <xsl:call-template name="make-columns">
                    <xsl:with-param name="nodelist" select="item"/>
                </xsl:call-template>
            </table>
        </xsl:template>
    
        <xsl:template name="make-columns">
            <xsl:param name="nodelist"/>
            <xsl:param name="columns-number" select="4"/>
    
            <tr>
                <xsl:apply-templates select="$nodelist[
                                not(position() > $columns-number)
                                ]"/>
            </tr>
    
            <!-- If some nodes are left, recursively call current
            template, passing only nodes that are left -->
            <xsl:if test="count($nodelist) > $columns-number">
                <xsl:call-template name="make-columns">
                    <xsl:with-param name="nodelist" select="$nodelist[
                                            position() > $columns-number
                                            ]"/>
                </xsl:call-template>
            </xsl:if>
        </xsl:template>
    
        <xsl:template match="item">
            <td>
                <xsl:apply-templates/>
            </td>
        </xsl:template>
    
    </xsl:stylesheet>
    

    Test input:

    <items>
        <item>1</item>
        <item>2</item>
        <item>3</item>
        <item>4</item>
        <item>5</item>
        <item>6</item>
        <item>7</item>
        <item>8</item>
        <item>9</item>
        <item>10</item>
        <item>11</item>
        <item>12</item>
        <item>13</item>
        <item>14</item>
        <item>15</item>
        <item>16</item>
        <item>17</item>
        <item>18</item>
        <item>19</item>
        <item>20</item>
        <item>21</item>
        <item>22</item>
        <item>23</item>
        <item>24</item>
        <item>25</item>
        <item>26</item>
        <item>27</item>
    </items>
    

    Output:

    <table>
        <tr>
            <td>1</td>
            <td>2</td>
            <td>3</td>
            <td>4</td>
        </tr>
        <tr>
            <td>5</td>
            <td>6</td>
            <td>7</td>
            <td>8</td>
        </tr>
        <tr>
            <td>9</td>
            <td>10</td>
            <td>11</td>
            <td>12</td>
        </tr>
        <tr>
            <td>13</td>
            <td>14</td>
            <td>15</td>
            <td>16</td>
        </tr>
        <tr>
            <td>17</td>
            <td>18</td>
            <td>19</td>
            <td>20</td>
        </tr>
        <tr>
            <td>21</td>
            <td>22</td>
            <td>23</td>
            <td>24</td>
        </tr>
        <tr>
            <td>25</td>
            <td>26</td>
            <td>27</td>
        </tr>
    </table>
    

    Do note: you can pass columns number dynamically.

    Additional requirements and edit.

    <xsl:stylesheet
        version="1.0"
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
        xmlns:my="http://localhost"
        exclude-result-prefixes="my">
        <xsl:output method="html" indent="yes"/>
    
        <my:layout>
            <td/><td/><td/><td/>
            <td/><td/><td/><td/>
            <td/><td/><td/><td/>
            <td/><td/><td/><td/>
        </my:layout>
    
        <xsl:template match="/*">
            <table>
                <xsl:call-template name="make-columns">
                    <xsl:with-param name="nodelist" select="item"/>
                </xsl:call-template>
            </table>
        </xsl:template>
    
        <xsl:template name="make-columns">
            <xsl:param name="nodelist"/>
            <xsl:param name="columns-number" select="4"/>
    
            <tr>
                <xsl:apply-templates select="$nodelist[
                                not(position() > $columns-number)
                                ]"/>
                <xsl:if test="count($nodelist) &lt; $columns-number">
                    <xsl:copy-of select="document('')/*/my:layout/td[
                        position() &lt;= $columns-number - count($nodelist)
                        ]"/>
                </xsl:if>
            </tr>
    
            <!-- If some nodes are left, recursively call current
            template, passing only nodes that are left -->
            <xsl:if test="count($nodelist) > $columns-number">
                <xsl:call-template name="make-columns">
                    <xsl:with-param name="nodelist" select="$nodelist[
                                            position() > $columns-number
                                            ]"/>
                </xsl:call-template>
            </xsl:if>
        </xsl:template>
    
        <xsl:template match="item">
            <td>
                <xsl:apply-templates/>
            </td>
        </xsl:template>
    
    </xsl:stylesheet>
    

    It can be applied to the previous sample or to this concise XML:

    <items>
        <item>1</item>
    </items>
    

    Result will be:

    <table>
        <tr>
            <td>1</td>
            <td xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:my="http://localhost"></td>
            <td xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:my="http://localhost"></td>
            <td xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:my="http://localhost"></td>
        </tr>
    </table>
    

    Do note:

    1. Hardcoded data to add elements, when there are less item elements than number of columns is.
    2. Extra hardcoded elements, if number of columns will ever change.

    If there won't ever be less elements than number of columns, you can just apply to the item elements with the same predicate and a different mode.

    And last edit. With a counted loop.

    <xsl:stylesheet
        version="1.0"
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
        <xsl:output method="html" indent="yes"/>
    
        <xsl:template match="/*">
            <table>
                <xsl:call-template name="make-columns">
                    <xsl:with-param name="nodelist" select="item"/>
                </xsl:call-template>
            </table>
        </xsl:template>
    
        <xsl:template name="make-columns">
            <xsl:param name="nodelist"/>
            <xsl:param name="columns-number" select="4"/>
    
            <tr>
                <xsl:apply-templates select="$nodelist[
                                not(position() > $columns-number)
                                ]"/>
                <xsl:if test="count($nodelist) &lt; $columns-number">
                    <xsl:call-template name="empty-cells">
                        <xsl:with-param name="finish" select="$columns-number - count($nodelist)"/>
                    </xsl:call-template>
                </xsl:if>
            </tr>
    
            <!-- If some nodes are left, recursively call current
            template, passing only nodes that are left -->
            <xsl:if test="count($nodelist) > $columns-number">
                <xsl:call-template name="make-columns">
                    <xsl:with-param name="nodelist" select="$nodelist[
                                            position() > $columns-number
                                            ]"/>
                </xsl:call-template>
            </xsl:if>
        </xsl:template>
    
        <xsl:template match="item">
            <td>
                <xsl:apply-templates/>
            </td>
        </xsl:template>
    
        <xsl:template name="empty-cells">
            <xsl:param name="finish"/>
            <td/>
            <xsl:if test="not($finish = 1)">
                <xsl:call-template name="empty-cells">
                    <xsl:with-param name="finish" select="$finish - 1"/>
                </xsl:call-template>
            </xsl:if>
        </xsl:template>
    
    </xsl:stylesheet>
    
    0 讨论(0)
  • 2021-01-12 17:26

    I. XSLT 1.0 solution:

    Here is probably one of the shortest possible solutions that notably doesn't require explicit recursion:

    <xsl:stylesheet version="1.0"
     xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
     <xsl:output omit-xml-declaration="yes" indent="yes"/>
     <xsl:param name="pNumCols" select="4"/>
    
     <xsl:template match="/*">
      <table>
       <xsl:apply-templates select="*[position() mod $pNumCols =1]"/>
      </table>
     </xsl:template>
    
     <xsl:template match="item">
      <tr>
        <xsl:apply-templates mode="copy" select=
        ". | following-sibling::*[not(position() >= $pNumCols)]"/>
      </tr>
     </xsl:template>
    
     <xsl:template match="item" mode="copy">
      <td><xsl:value-of select="."/></td>
     </xsl:template>
    </xsl:stylesheet>
    

    when this transformation is applied on the following XML document:

    <items>
        <item>1</item>
        <item>2</item>
        <item>3</item>
        <item>4</item>
        <item>5</item>
        <item>6</item>
        <item>7</item>
        <item>8</item>
        <item>9</item>
        <item>10</item>
        <item>11</item>
        <item>12</item>
        <item>13</item>
        <item>14</item>
        <item>15</item>
        <item>16</item>
        <item>17</item>
        <item>18</item>
        <item>19</item>
        <item>20</item>
        <item>21</item>
        <item>22</item>
        <item>23</item>
        <item>24</item>
        <item>25</item>
        <item>26</item>
        <item>27</item>
    </items>
    

    the wanted, correct result is produced:

    <table>
       <tr>
          <td>1</td>
          <td>2</td>
          <td>3</td>
          <td>4</td>
       </tr>
       <tr>
          <td>5</td>
          <td>6</td>
          <td>7</td>
          <td>8</td>
       </tr>
       <tr>
          <td>9</td>
          <td>10</td>
          <td>11</td>
          <td>12</td>
       </tr>
       <tr>
          <td>13</td>
          <td>14</td>
          <td>15</td>
          <td>16</td>
       </tr>
       <tr>
          <td>17</td>
          <td>18</td>
          <td>19</td>
          <td>20</td>
       </tr>
       <tr>
          <td>21</td>
          <td>22</td>
          <td>23</td>
          <td>24</td>
       </tr>
       <tr>
          <td>25</td>
          <td>26</td>
          <td>27</td>
       </tr>
    </table>
    

    Explanation:

    1. The wanted number of cells per row is specified in the external/global parameter $pNumCols.

    2. Templates are applied only on such children of the top element, whose position is the start of a new row -- they are generated by the expression $k * $pNumCols +1, where $k can be any integer.

    3. The template that processing each row-starting item creates a row (tr element) and within it applies templates in a special mode "copy" for the $pNumCols starting with itself.

    4. The template matching an item in mode "copy" simply creates a cell (td element) and outputs inside it the string value of the item element being matched.

    II. XSLT 2.0 solution:

    <xsl:stylesheet version="2.0"
     xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
        <xsl:output omit-xml-declaration="yes" indent="yes"/>
        <xsl:param name="pNumCols" select="4"/>
    
        <xsl:template match="items">
            <table>
                <xsl:for-each-group select="item"
                group-by="(position()-1) idiv $pNumCols">
                    <tr>
                        <xsl:for-each select="current-group()">
                            <td>
                                <xsl:apply-templates/>
                            </td>
                        </xsl:for-each>
                    </tr>
                </xsl:for-each-group>
            </table>
        </xsl:template>
    </xsl:stylesheet>
    

    applied on the same XML document as before, this transformation produces the same, correct result.

    Explanation:

    1. The <xsl:for-each-group> instruction is used to select the different groups of item elements where each group contains the elements that must be represented in one row.

    2. The standard XPath 2.0 operator idiv is used for this purpose.

    3. The XSLT 2.0 function current-group() contains all items that must be presented in the current row.

    0 讨论(0)
  • 2021-01-12 17:26

    Just for style, this XSLT 1.0 stylesheet:

    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
        <xsl:param name="pColumns" select="4"/>
        <xsl:template match="/*">
            <table>
                <xsl:apply-templates select="*[position() mod $pColumns = 1]"/>
            </table>
        </xsl:template>
        <xsl:template match="item">
            <xsl:variable name="vItems"
                          select=".|following-sibling::*[$pColumns > position()]"/>
            <tr>
                <xsl:apply-templates select="$vItems" mode="makeCell"/>
                <xsl:call-template name="fillRow">
                    <xsl:with-param name="pItems" 
                                    select="$pColumns - count($vItems)"/>
                </xsl:call-template>
            </tr>
        </xsl:template>
        <xsl:template match="item" mode="makeCell">
            <td>
                <xsl:value-of select="."/>
            </td>
        </xsl:template>
        <xsl:template name="fillRow">
            <xsl:param name="pItems" select="0"/>
            <xsl:if test="$pItems">
                <td/>
                <xsl:call-template name="fillRow">
                    <xsl:with-param name="pItems" select="$pItems - 1"/>
                </xsl:call-template>
            </xsl:if>
        </xsl:template>
    </xsl:stylesheet>
    

    With @Flack's answer input, output:

    <table>
        <tr>
            <td>1</td>
            <td>2</td>
            <td>3</td>
            <td>4</td>
        </tr>
        <tr>
            <td>5</td>
            <td>6</td>
            <td>7</td>
            <td>8</td>
        </tr>
        <tr>
            <td>9</td>
            <td>10</td>
            <td>11</td>
            <td>12</td>
        </tr>
        <tr>
            <td>13</td>
            <td>14</td>
            <td>15</td>
            <td>16</td>
        </tr>
        <tr>
            <td>17</td>
            <td>18</td>
            <td>19</td>
            <td>20</td>
        </tr>
        <tr>
            <td>21</td>
            <td>22</td>
            <td>23</td>
            <td>24</td>
        </tr>
        <tr>
            <td>25</td>
            <td>26</td>
            <td>27</td>
            <td />
        </tr>
    </table>
    
    0 讨论(0)
提交回复
热议问题