Xsl: converting a list to a two-dimensional table

Let's say I have this XML node:

<items> <item>...<item> <item>...<item> <item>...<item> <item>...<item> <item>...<item> ... </items> 

where there are N item nodes.

Now I would like to convert it to an HTML table with 4 columns. (for example, if N = 12, that is, 3 complete rows, and if N = 27, that is, 7 rows, the latter has 3 cells)

How can i do this?

My gut call should do it this way, where {{something}} is what I don't know how to implement:

 <xsl:template match="items"> <table> <xsl:call-template name="partition-items"> <xsl:with-param name="skip" select="0" /> </xsl:call-template> </table> </xsl:template> <xsl:template name="partition-items"> <xsl:param name="skip" /> {{ if # of items in current node > $skip, output a row, and call partition-items($skip+4) }} <xsl:template /> 

Parts that I don’t know how to implement,

  • how to make a predicate for testing # of item elements in the current node
  • how to get nth item element in current node

Update from comments

How to put the last line with an empty <td /> so that each line contains exactly the right cells?

+5
source share
4 answers

This is my working solution .

Since you did not provide the desired result, this particular one may not be complete 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> 

Conclusion:

 <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> 

Please note: you can dynamically transfer column numbers.

Additional requirements and changes.

 <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 compressed XML:

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

The 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> 

Note:

  • Hard-coded data to add items when there are fewer item items than the number of columns.
  • Extra hard-wired items if the number of columns is ever changed.

If the number of elements is less than the number of columns, you can simply apply the same predicate and different mode to the item elements.

And the last edit. With a counted cycle.

 <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> 
+3
source

I. XSLT 1.0 Solution:

Here is probably one of the shortest possible solutions, which, in particular, does not 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 conversion is applied to 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> 

required, the correct result is obtained :

 <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

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

  • Patterns apply only to such children of the top element, whose position is the beginning of a new line - they are generated by the expression $k * $pNumCols +1 , where $ k can be any integer.

  • The template that processes each initial element of the string creates a string ( tr element), and inside it, templates are applied in the special "copy" mode for $pNumCols , starting with itself.

  • The template corresponding to item in "copy" mode simply creates a cell element ( td ) and displays the string value of the item element inside it.

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> 

Applies to the same XML document as before, this conversion produces the same correct result.

Explanation

  • <xsl:for-each-group> used to select different groups of item elements, where each group contains items that should be presented on the same line.

  • For this purpose, the standard XPath 2.0 idiv

    .
  • The XSLT 2.0 function current-group() contains all the elements that should be represented on the current line .

+5
source

For style only, this XSLT 1.0 style sheet:

 <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> 

When you type @Flack's answer, the output is:

 <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> 
+1
source

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
source

Source: https://habr.com/ru/post/892132/


All Articles