XSLT dynamically creating rows and columns - xslt

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>

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>
Matching /DataSet produces the root element <report /> and continues applying templates.
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.
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.
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.

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 <= $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() <=
($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
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.
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>).
Use the Piez Method to emit the odd empty table cells on the last row of the table.

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>

Related

Xpath - How to get all existing attributes name of siblings

The goal is simple. I'm trying to build a table with attributes in columns.
My problem : Some siblings don't have the same attributes as the first one.
When I build the table head I retrieve the attributes name of the first node and then hope that attributes of the following siblings will be the same in the same order. That is not the case.
I get only columns id, key, value, myattr1 and the myattr2 attribute is placed in the myattr1 column.
For building the table, I want to get columns : id, key, value, myattr1, myattr2
How can I retrieve the whole list of existing attributes for the node I am working on and loop over it?
I am still working the same js and form (see link at the bottom). It now requires bootstrap (css and js).
I slightly changed the xml. Here it is :
<?xml version="1.0" encoding="UTF-8"?>
<Domain>
<Properties id="myid">
<Property id="DOM00000" key="mykey1" value="value1" myattr2="Mail"/>
<Property id="DOM00001" key="mykey2" value="value2" myattr1="EveryDay"/>
</Properties>
<Tokens>
<Token name="token1" comment="" ><![CDATA[mydata1---blah-blah-blah]]></Token>
<Token name="token2" comment="" ><![CDATA[mydata2---blah-blah-blah]]></Token>
</Tokens>
<Resources>
<Resource name="res1" type="W" current="0">
<Value><![CDATA[10]]></Value>
</Resource>
<Resource name="res2" type="W" current="0">
<Value><![CDATA[10]]></Value>
</Resource>
</Resources>
</Domain>
The current state of the xsl :
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/*">
<xsl:element name="div">
<xsl:attribute name="class">container</xsl:attribute>
<ul class="nav nav-tabs">
<xsl:for-each select="./*">
<xsl:call-template name="tabs" />
</xsl:for-each>
</ul>
</xsl:element>
<xsl:element name="div">
<xsl:attribute name="class">tab-content</xsl:attribute>
<xsl:for-each select="./*">
<xsl:call-template name="tabcontent" />
</xsl:for-each>
</xsl:element>
</xsl:template>
<xsl:template name="tabs">
<xsl:variable name="active">
<xsl:choose>
<xsl:when test="preceding-sibling::*"></xsl:when>
<xsl:otherwise>active</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:element name="li">
<xsl:attribute name="class">nav-item <xsl:value-of select="$active" /></xsl:attribute>
<xsl:element name="a">
<xsl:attribute name="href">#<xsl:value-of select="name(.)" /></xsl:attribute>
<xsl:attribute name="class">nav-link</xsl:attribute>
<xsl:attribute name="data-toggle">tab</xsl:attribute>
<xsl:value-of select="name(.)" />
</xsl:element>
</xsl:element>
</xsl:template>
<xsl:template name="tabcontent">
<xsl:variable name="activetab">
<xsl:choose>
<xsl:when test="preceding-sibling::*"></xsl:when>
<xsl:otherwise>active in</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:element name="div">
<xsl:attribute name="id"><xsl:value-of select="name(.)" /></xsl:attribute>
<xsl:attribute name="class">container tab-pane fade <xsl:value-of select="$activetab" /></xsl:attribute>
<h3><xsl:value-of select="name(.)" /></h3>
<table class="table table-striped table-hover">
<thead>
<tr><xsl:for-each select="./*[1]/#*"><th><xsl:value-of select="name(.)" /></th></xsl:for-each></tr>
</thead>
<tbody>
<xsl:for-each select="./*"><tr>
<xsl:for-each select="./#*"><td><xsl:value-of select="." /></td></xsl:for-each></tr>
</tr></xsl:for-each>
</tbody>
</table>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
XSLT - How to manage CDATA as common content?
Edit :
Thanks to Tim-C answer
Here is the full xsl working for my use case :
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:key name="attrs" match="/*/*/*/#*" use="concat(name(../..), '|', name())" />
<xsl:template match="/*">
<xsl:element name="div">
<xsl:attribute name="class">container</xsl:attribute>
<ul class="nav nav-tabs">
<xsl:for-each select="./*">
<xsl:call-template name="tabs" />
</xsl:for-each>
</ul>
</xsl:element>
<xsl:element name="div">
<xsl:attribute name="class">tab-content</xsl:attribute>
<xsl:for-each select="./*">
<xsl:call-template name="tabcontent" />
</xsl:for-each>
</xsl:element>
</xsl:template>
<xsl:template name="tabs">
<xsl:variable name="active">
<xsl:choose>
<xsl:when test="preceding-sibling::*"></xsl:when>
<xsl:otherwise>active</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:element name="li">
<xsl:attribute name="class">nav-item <xsl:value-of select="$active" /></xsl:attribute>
<xsl:element name="a">
<xsl:attribute name="href">#<xsl:value-of select="name(.)" /></xsl:attribute>
<xsl:attribute name="class">nav-link</xsl:attribute>
<xsl:attribute name="data-toggle">tab</xsl:attribute>
<xsl:value-of select="name(.)" />
</xsl:element>
</xsl:element>
</xsl:template>
<xsl:template name="tabcontent">
<xsl:variable name="activetab">
<xsl:choose>
<xsl:when test="preceding-sibling::*"></xsl:when>
<xsl:otherwise>active in</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:variable name="attrs" select="*/#*[generate-id() = generate-id(key('attrs', concat(name(../..), '|', name()))[1])]" />
<xsl:element name="div">
<xsl:attribute name="id"><xsl:value-of select="name(.)" /></xsl:attribute>
<xsl:attribute name="class">container tab-pane fade <xsl:value-of select="$activetab" /></xsl:attribute>
<h3><xsl:value-of select="name(.)" /></h3>
<table class="table table-striped table-hover">
<thead>
<tr>
<xsl:for-each select="$attrs">
<th>
<xsl:value-of select="name()" />
</th>
</xsl:for-each>
</tr>
</thead>
<tbody>
<xsl:for-each select="*">
<tr>
<xsl:variable name="current" select="." />
<xsl:for-each select="$attrs">
<td>
<xsl:value-of select="$current/#*[name() = name(current())]" />
</td>
</xsl:for-each>
</tr>
</xsl:for-each>
</tbody>
</table>
</xsl:element>
</xsl:template>
<xsl:template name="toto"></xsl:template>
</xsl:stylesheet>
What you make use of here is a technique called Muenchian Grouping to get a list of distinct attributes, based on their name.
However, although the question just mentions about id, key, value, myattr1, myattr2, which are the Property attributes, it looks like you want to repeat it for the Token and Resource nodes too (i.e. you are trying to be generic). In this case, you define a key like so, which takes into account the main element name
<xsl:key name="attrs" match="/*/*/*/#*" use="concat(name(../..), '|', name())" />
Then, for a given element (such as Properties) you can get distinct attributes like so:
<xsl:variable name="attrs" select="*/#*[generate-id() = generate-id(key('attrs', concat(name(../..), '|', name()))[1])]" />
This can then be used to get the headers, and access the relevant attributes for each row.
Try this (abridged) XSLT:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" />
<xsl:key name="attrs" match="/*/*/*/#*" use="concat(name(../..), '|', name())" />
<xsl:template match="/*">
<xsl:for-each select="*">
<xsl:call-template name="tabcontent" />
</xsl:for-each>
</xsl:template>
<xsl:template name="tabcontent">
<xsl:variable name="attrs" select="*/#*[generate-id() = generate-id(key('attrs', concat(name(../..), '|', name()))[1])]" />
<table class="table table-striped table-hover">
<thead>
<tr>
<xsl:for-each select="$attrs">
<th>
<xsl:value-of select="name()" />
</th>
</xsl:for-each>
</tr>
</thead>
<tbody>
<xsl:for-each select="*">
<tr>
<xsl:variable name="current" select="." />
<xsl:for-each select="$attrs">
<td>
<xsl:value-of select="$current/#*[name() = name(current())]" />
</td>
</xsl:for-each>
</tr>
</xsl:for-each>
</tbody>
</table>
</xsl:template>
</xsl:stylesheet>
See it in action at http://xsltfiddle.liberty-development.net/jyRYYi7

XSLT - format comma seperated list into table format

A web service is returning a list of persons in the below xml format. The values are seperated by a semicolon. I need to display the values in 2 or 3 columns (Needs to be a variable).
Desired Results:
<table border="1">
<tr>
<td>Smith, John</td>
<td>Jackson, Samuel</td>
<td>Wayne, Bruce</td>
</tr>
<tr>
<td>Cosby, Bill</td>
<td>Kent, Clarke</td>
<td>Leno, Jay</td>
</tr>
<tr>
<td>OBrian, Conan</td>
<td> </td>
<td> </td>
</tr>
</table>
Xml sample
<?xml version="1.0" encoding="UTF-8"?> <PI>Smith, John; Jackson,
Samuel; Wayne, Bruce; Cosby, Bill; Kent, Clarke; Leno, Jay; OBrian,
Conan; </PI>
Please accept Holman's solution. His technique is spot-on.
XSLT 2.0 Solution
Your sample input data suggests semicolon terminated list, rather than a semicolon separated list.
This XSLT 2.0 stylesheet will work correctly with either a semicolon terminated list or a semicolon separated list. It takes one input parameter col-count, which defaults to 3.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output method="html" doctype-system="about:legacy-compat" encoding="UTF-8" indent="yes" />
<xsl:strip-space elements="*" />
<xsl:param name="col-count" select="3" />
<xsl:template match="/*">
<table border="1">
<xsl:variable name="names" select="tokenize(.,';')[normalize-space(.)]" />
<xsl:for-each-group select="$names , for $x in 1 to
($col-count - (count($names) mod $col-count)) mod $col-count return ''"
group-by="(position() - 1) idiv $col-count">
<tr>
<xsl:for-each select="current-group()">
<td><xsl:value-of select="normalize-space(.)" /></td>
</xsl:for-each>
</tr>
</xsl:for-each-group>
</table>
</xsl:template>
</xsl:stylesheet>
Notes:
(1) Notice the brevity of the solution, and the absence of clumsy xsl:if statements. The use of calculated sequences (as opposed to input document node selections), is not just limited to the group-by attribute, but can also to applied to select. The use of computed sequences suggests a more functional view, rather than a procedural view.
(2) If you want the output to be safe for old browsers, then replace ...
<xsl:for-each-group select="$names , for $x in 1 to
($col-count - (count($names) mod $col-count)) mod $col-count return ''"
group-by="(position() - 1) idiv $col-count">
... with ...
<xsl:for-each-group select="$names , for $x in 1 to
($col-count - (count($names) mod $col-count)) mod $col-count return ' '"
group-by="(position() - 1) idiv $col-count">
XSLT 1.0 Solution
If you are stuck on XSLT 1.0, then you could use this equivalent, but less efficient stylesheet ...
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:esl="http://exslt.org/common"
xmlns:so="http://stackoverflow.com/questions/18066463"
version="1.0"
exclude-result-prefixes="xsl so esl">
<xsl:output method="html" doctype-system="about:legacy-compat" encoding="UTF-8" indent="yes" />
<xsl:strip-space elements="*" />
<xsl:variable name="col-count" select="3" /><!-- Must be 2 or 3. -->
<xsl:variable name="empty-rtf">
<so:name> </so:name>
</xsl:variable>
<xsl:variable name="empty" select="esl:node-set($empty-rtf)/*" />
<xsl:template match="/*">
<table border="1">
<xsl:variable name="names-rtf">
<xsl:call-template name="split">
<xsl:with-param name="list" select="." />
</xsl:call-template>
</xsl:variable>
<xsl:variable name="names" select="esl:node-set($names-rtf)/*" />
<xsl:for-each select="$names[(position() mod $col-count) = 1]">
<xsl:variable name="row" select="position() - 1" />
<tr>
<xsl:apply-templates select="$names[floor((position() - 1) div $col-count) = $row]" />
<xsl:if test="position()=last()">
<xsl:for-each select="($names|$empty)[position() <=
(($col-count - (count($names) mod $col-count)) mod $col-count)]">
<xsl:apply-templates select="$empty" />
</xsl:for-each>
</xsl:if>
</tr>
</xsl:for-each>
</table>
</xsl:template>
<xsl:template name="split">
<xsl:param name="list" />
<xsl:choose>
<xsl:when test="contains($list,';')">
<xsl:call-template name="split">
<xsl:with-param name="list" select="substring-before($list,';')" />
</xsl:call-template>
<xsl:call-template name="split">
<xsl:with-param name="list" select="substring-after($list,';')" />
</xsl:call-template>
</xsl:when>
<xsl:when test="normalize-space($list)">
<so:name><xsl:value-of select="normalize-space($list)" /></so:name>
</xsl:when>
<xsl:otherwise />
</xsl:choose>
</xsl:template>
<xsl:template match="so:*" priority="2">
<td><xsl:value-of select="." /></td>
</xsl:template>
</xsl:stylesheet>
Here is an XSLT 2.0 solution with the number of columns as a variable as requested. This employs a technique I teach in the classroom that underscores that grouping need not be by values in the XML (as many XSLT users assume), rather, it can be by any arbitrary calculation (in this case, the result of division).
[edited to show multiple invocations with different number of columns]
t:\ftemp>type names.xml
<?xml version="1.0" encoding="UTF-8"?> <PI>Smith, John; Jackson,
Samuel; Wayne, Bruce; Cosby, Bill; Kent, Clarke; Leno, Jay; OBrian,
Conan; </PI>
t:\ftemp>call xslt2 names.xml names.xsl names.out.xml "cols=3"
t:\ftemp>type names.out.xml
<?xml version="1.0" encoding="UTF-8"?>
<table border="1">
<tr>
<td>Smith, John</td>
<td>Jackson, Samuel</td>
<td>Wayne, Bruce</td>
</tr>
<tr>
<td>Cosby, Bill</td>
<td>Kent, Clarke</td>
<td>Leno, Jay</td>
</tr>
<tr>
<td>OBrian, Conan</td>
<td/>
<td/>
</tr>
</table>
t:\ftemp>call xslt2 names.xml names.xsl names.out.xml "cols=2"
t:\ftemp>type names.out.xml
<?xml version="1.0" encoding="UTF-8"?>
<table border="1">
<tr>
<td>Smith, John</td>
<td>Jackson, Samuel</td>
</tr>
<tr>
<td>Wayne, Bruce</td>
<td>Cosby, Bill</td>
</tr>
<tr>
<td>Kent, Clarke</td>
<td>Leno, Jay</td>
</tr>
<tr>
<td>OBrian, Conan</td>
<td/>
</tr>
</table>
t:\ftemp>type names.xsl
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0">
<xsl:output indent="yes"/>
<xsl:param name="cols" required="yes"/>
<xsl:template match="PI">
<table border="1">
<!--determine population and group by number of columns-->
<xsl:for-each-group select="tokenize(.,';\s+')"
group-by="(position()-1) idiv $cols">
<tr>
<!--put members into the row-->
<xsl:for-each select="current-group()">
<td>
<xsl:value-of select="normalize-space(.)"/>
</td>
</xsl:for-each>
<!--filler for the last row-->
<xsl:if test="position()=last()">
<xsl:for-each select="count(current-group())+1 to $cols">
<td/>
</xsl:for-each>
</xsl:if>
</tr>
</xsl:for-each-group>
</table>
</xsl:template>
</xsl:stylesheet>
t:\ftemp>rem

Generate each TR with 3 TD's in XSL (The Right Way)?

I simply need to have 3 products in each row. Each Product is in the template "OneProduct" (not included) and, the putter most Table tags are outside this table.. The following actually does work but, how is it done the right way?
<xsl:if test="(position() mod 3) = 1">
<xsl:text disable-output-escaping="yes">
<![CDATA[<TR name="PROD_ROW">]]>
</xsl:text>
</xsl:if>
<TD width="33%" align="center" name="PRODUCT_CELL">
<xsl:call-template name="OneProduct">
<xsl:with-param name="productId" select="$productId" />
</xsl:call-template>
</TD>
<xsl:if test="( (position()+1) mod 3 = 1)">
<xsl:text disable-output-escaping="yes">
<![CDATA[</TR>]]>
</xsl:text>
</xsl:if>
In answer to your question, that is not the preferred way. It is the sort of thing you may do in a procedural language; iterate over the elements incrementing a counter and then outputting the delimiting elements when you reach the 3rd, 6th, etc, elements. But XSLT is a functional language, and a different approach is needed.
What you could do is use xsl:apply-templates to select the elements that will be first in each row. Assuming you had some XML like this
<products>
<product id="1" name="Product 1" />
<product id="2" name="Product 2" />
<product id="3" name="Product 3" />
<product id="4" name="Product 4" />
<product id="5" name="Product 5" />
</products>
Then your xsl:apply-templates would be like this:
<xsl:apply-templates select="product[position() mod 3 = 1]" />
Within the template that matched the product element, you would then select all the elements in the row (i.e. the current element, plus the two following elements)
<xsl:apply-templates
select="self::*|following-sibling::product[position() < 3]" mode="cell" />
Note the use of mode here, as you will have two templates matching product and you need to distinguish between them.
Finally, you would just have this 'cell' template to output the product.
<xsl:template match="product" mode="cell">
<td>
<xsl:value-of select="#name" />
</td>
</xsl:template>
The only thing to worry about would be if you didn't have an exact number of cells for each row (for example, in this example there are five products, so the last row only has two cells.
Try the following XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" />
<xsl:template match="/*">
<table>
<xsl:apply-templates select="product[position() mod 3 = 1]" />
</table>
</xsl:template>
<xsl:template match="product">
<tr>
<xsl:apply-templates select="self::*|following-sibling::product[position() < 3]" mode="cell" />
</tr>
</xsl:template>
<xsl:template match="product" mode="cell">
<td>
<xsl:if test="position() < 3 and not(following-sibling::product[1])">
<xsl:attribute name="colspan">
<xsl:value-of select="4 - position()" />
</xsl:attribute>
</xsl:if>
<xsl:value-of select="#name" />
</td>
</xsl:template>
</xsl:stylesheet>
When appied to the sample XML, the following is output
<table>
<tr>
<td>Product 1</td>
<td>Product 2</td>
<td>Product 3</td>
</tr>
<tr>
<td>Product 4</td>
<td colspan="2">Product 5</td>
</tr>
</table>

how to design a template in xslt which adds strong tag based on some conditions

I have input xml of the form
<content xml:lang="en" xmlns:w="http://www.w.com/sch/W">
<w:firstRowHeader>true</w:firstRowHeader>
<w:firstColumnHeader>true</w:firstColumnHeader>
<w:customTable>
<w:tableContent>
<w:row>
<w:cell>
<w:spanInfo backgroundColor="Yellow" columnWidth="5" isRowHeader="true"/>
<text>ghnmghmg</text>
</w:cell>
<w:cell>
<w:spanInfo backgroundColor="Yellow" isRowHeader="false"/>
<text>ghmhgmgm</text>
</w:cell>
</w:row>
<w:row>
<w:cell>
<w:spanInfo backgroundColor="Yellow" columnWidth="5" isRowHeader="false"/>
<text>vj</text>
</w:cell>
<w:cell>
<w:spanInfo columnWidth="5" isRowHeader="true"/>
<text>mm</text>
</w:cell>
</w:row>
</w:tableContent>
</w:customTable>
</content>
This needs to be transformed to a xml in which:
w:tableContent mapped to tablecontent and
then under tablecontent tag 'table','tbody' tags are created
w:row mapped to tr tag
w:cell mapped to td tag
and conditions are like
if only 1st w:cell element in w:row has an attribute isRowHeader as "true" then every 'td' element under its respective 'tr' tag should contain a 'strong' tag and ignore 2nd w:cell's isRowHeader
if w:firstRowHeader is 'true' then transformed table should have 1st row text in bold, i.e., every 'td' tag in 1st row of table should contain 'strong' tag
if w:firstColumnHeader is 'true' then transformed table should have 1st column text in bold, i.e., every tr tag's 1st 'td' tag of table should contain 'strong' tag
Transformed xml:
<content>
<tablecontent>
<table cellspacing="1" cellpadding="1" border="1" style="WIDTH: 100%" title="Title" xmlns="http://www.w3.org/1999/xhtml">
<tbody>
<tr>
<td style="BACKGROUND-COLOR: Yellow; WIDTH: 5%"><strong>ghnmghmg</strong></td>
<td style="BACKGROUND-COLOR: Yellow"><strong>ghmhgmgm</strong></td>
</tr>
<tr>
<td style="BACKGROUND-COLOR: Yellow; WIDTH: 5%">vj</td>
<td style="WIDTH: 5%">mm</td>
</tr>
</tbody>
</table>
</tablecontent>
</content>
This is the xslt template that i have tried but cant figure out how to implement these 'strong' tags in it...
XSLT:
<xsl:template match="w:tableContent">
<xsl:variable name="var3" select="../w:firstRowHeader"/>
<xsl:variable name="var4" select="../w:firstColumnHeader"/>
<tablecontent>
<table cellspacing="1" cellpadding="1" border="1" style="WIDTH: 100%" title="Title" xmlns="http://www.w3.org/1999/xhtml" >
<tbody>
<xsl:for-each select="child::*">
<xsl:choose>
<xsl:when test="name()='w:row'">
<tr>
<xsl:for-each select="child::*">
<xsl:choose>
<xsl:when test="name()='w:cell'">
<td>
<xsl:for-each select="child::*">
<xsl:choose>
<xsl:when test="name()='w:spanInfo'">
<xsl:variable name="var8" select="#backgroundColor" />
<xsl:variable name="var9" select="#columnWidth" />
<xsl:variable name="var10" select="#isRowHeader" />
<xsl:if test="$var8!='' or $var9!=''">
<xsl:attribute name="style">
<xsl:if test="$var8!='' and $var9!=''">
<xsl:value-of select="concat('BACKGROUND-COLOR: ',$var8,'; WIDTH: ',$var9,'%')" />
</xsl:if>
<xsl:if test="$var8!='' and not($var9)">
<xsl:value-of select="concat('BACKGROUND-COLOR: ',$var8)" />
</xsl:if>
<xsl:if test="not($var8) and $var9!=''">
<xsl:value-of select="concat('WIDTH: ',$var9,'%')" />
</xsl:if>
</xsl:when>
<xsl:when test="name()='text'">
<xsl:if test="../w:spanInfo/#isRowHeader='true'">
<strong><xsl:value-of select="." /></strong>
</xsl:if>
<xsl:if test="../w:spanInfo/#isRowHeader!='true' or not(../w:spanInfo/#isRowHeader) ">
<xsl:value-of select="." />
</xsl:if>
</xsl:when>
</xsl:choose>
</xsl:for-each>
</td>
</xsl:when>
</xsl:choose>
</xsl:for-each>
</tr>
</xsl:when>
</xsl:choose>
</xsl:for-each>
</tbody>
</table>
</tablecontent>
</xsl:template>
But the above template adds 'strong' tags to the cells which has only w:spanInfo's 'isRowHeader' attribute as 'true'. But I require to get 'strong' tag to be added to 2nd cell content also irrespective of its w:spanInfo's 'isRowHeader' attribute's value, provided if 1st cell already has 'isRowHeader' attribute as 'true'.
This XSLT 1.0 style-sheet ...
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:w="http://www.w.com/sch/W">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<content>
<xsl:apply-templates select="*/*/w:tableContent"/>
</content>
</xsl:template>
<xsl:template match="w:tableContent">
<table cellspacing="1" cellpadding="1" border="1" style="WIDTH: 100%" title="Title" xmlns="http://www.w3.org/1999/xhtml">
<tbody>
<xsl:apply-templates select="w:row" />
</tbody>
</table>
</xsl:template>
<xsl:template match="w:row">
<tr>
<xsl:apply-templates select="w:cell" />
</tr>
</xsl:template>
<xsl:template match="w:cell">
<xsl:variable name="style">
<xsl:if test="w:spanInfo/#backgroundColor">
<xsl:value-of select="concat('BACKGROUND-COLOR: ',w:spanInfo/#backgroundColor)" />
</xsl:if>
<xsl:if test="w:spanInfo/#columnWidth">
<xsl:if test="w:spanInfo/#backgroundColor">
<xsl:value-of select="'; '" />
</xsl:if>
<xsl:value-of select="concat('WIDTH: ',w:spanInfo/#columnWidth,'%')" />
</xsl:if>
</xsl:variable>
<td>
<xsl:if test="$style">
<xsl:attribute name="style"><xsl:value-of select="$style" /></xsl:attribute>
</xsl:if>
<xsl:apply-templates select="text" />
</td>
</xsl:template>
<xsl:template match="w:cell/text[
not( ../../preceding-sibling::w:row) and (/*/w:firstRowHeader='true')
or
not( ../preceding-sibling::w:cell) and (/*/w:firstColumnHeader='true')
or
(../preceding-sibling::w:cell[last()]/w:spaninfo/#isRowHeader='true')
]">
<strong><xsl:call-template name="default-rendering-of-text" /></strong>
</xsl:template>
<xsl:template match="text" name="default-rendering-of-text">
<xsl:value-of select="." />
</xsl:template>
</xsl:stylesheet>
... should satisfy your rules. The 3 conditions that you have set for bold/strong rendering are made plainly evident by the predicate of the match condition for text elements (near the end of the style-sheet). By avoiding unnecessary xsl:for-each we can use a simpler, more modular and more readable template based solution.

alternate sorted nodes in XSLT 1.0 without extension function

This a very similar question as XSL: Transforming xml into a sorted multicolumn html table
But (unfortunately) there's an extra requirement: it should be XSLT 1.0 without extension functions, i.e. without using the node-set function.
This is my simplified XML:
<demo>
<config n_columns="3" />
<messages>
<msg date="2011-07-06" title="2nd message" />
<title>message list</title>
<msg date="2011-07-05" title="4th message" />
<msg date="2011-07-06" title="3rd message" />
<msg date="2011-07-07" title="1st message" />
</messages>
</demo>
Using this stylesheet:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" />
<xsl:template match="/">
<xsl:apply-templates select="demo/messages">
<xsl:with-param name="n_columns" select="number(/demo/config/#n_columns)" />
</xsl:apply-templates>
</xsl:template>
<xsl:template match="messages">
<xsl:param name="n_columns" />
<div>
<xsl:value-of select="concat(./title, ' (', $n_columns, ' columns)')" />
</div>
<table>
<xsl:variable name="cells" select="msg" />
<xsl:apply-templates select="$cells[(position() - 1) mod $n_columns = 0]"
mode="row">
<xsl:with-param name="n_columns" select="$n_columns" />
<xsl:with-param name="cells" select="$cells" />
</xsl:apply-templates>
</table>
</xsl:template>
<xsl:template match="msg" mode="row">
<xsl:param name="n_columns" />
<xsl:param name="cells" />
<xsl:variable name="n_row" select="position()" />
<xsl:variable name="row_cells"
select="$cells[position() > ($n_row - 1) * $n_columns][position() <= $n_columns]" />
<tr>
<xsl:apply-templates select="$row_cells" mode="cell" />
<xsl:call-template name="empty-cells">
<xsl:with-param name="n" select="$n_columns - count($row_cells)" />
</xsl:call-template>
</tr>
</xsl:template>
<xsl:template match="msg" mode="cell">
<td>
<xsl:value-of select="#title" />
</td>
</xsl:template>
<xsl:template name="empty-cells">
<xsl:param name="n" />
<xsl:if test="$n > 0">
<td>
<xsl:attribute name="colspan">
<xsl:value-of select="$n" />
</xsl:attribute>
<xsl:text> </xsl:text>
</td>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Produces this HTML fragment as output:
<div>message list (3 columns)</div>
<table>
<tr>
<td>2nd message</td>
<td>4th message</td>
<td>3rd message</td>
</tr>
<tr>
<td>1st message</td>
<td colspan="2"> </td>
</tr>
</table>
What is obviously missing is the sort part...
Redefining the "cells" variable as follows is actually what I need:
<xsl:variable name="cells">
<xsl:for-each select="msg">
<xsl:sort select="#date" order="descending" />
<xsl:sort select="#title" />
<xsl:copy-of select="." />
</xsl:for-each>
</xsl:variable>
But now I must define another variable to convert the RTF to a nodelist and pass that one to the template I'm applying.
<xsl:variable name="sCells" select="ext:node-set($cells)/*" />
Doing that would produce the following HTML fragment:
<div>message list (3 columns)</div>
<table>
<tr>
<td>1st message</td>
<td>2nd message</td>
<td>3rd message</td>
</tr>
<tr>
<td>4th message</td>
<td colspan="2"> </td>
</tr>
</table>
Unfortunately, my XSLT engine (SAP XML toolkit for java) doesn't support this (or a similar) extension function. Thus I'm looking for another solution that doesn't require the node-set extension function.
I spent quite some time reading all kinds of forums etc., but I really can't figure it out. Perhaps someone has a good idea for an alternative approach? tnx!
This is the follow up based on Dimitre's (slightly extended) solution.
This XML input
<demo>
<config n_columns="3" />
<messages>
<msg date="2011-07-06" title="2nd message" />
<title>message list</title>
<msg date="2011-07-05" title="4th message" />
<msg date="2011-07-06" title="3rd message" />
<msg date="2011-07-07" title="1st message" />
<msg date="2011-07-05" title="5th message" />
<msg date="2011-07-05" title="7th message" />
<msg date="2011-07-05" title="6th message" />
</messages>
</demo>
combined with this XSLT stylesheet
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" />
<xsl:variable name="vNumCols" select="/*/config/#n_columns" />
<xsl:variable name="vCells" select="/*/messages/msg" />
<xsl:variable name="vNumCells" select="count($vCells)" />
<xsl:variable name="vNumRows" select="ceiling($vNumCells div $vNumCols)" />
<xsl:variable name="vIndexPatternLength"
select="string-length(concat('', $vNumCells))" />
<xsl:variable name="vIndexPattern">
<xsl:call-template name="padding">
<xsl:with-param name="length" select="$vIndexPatternLength" />
<xsl:with-param name="chars" select="'0'" />
</xsl:call-template>
</xsl:variable>
<xsl:variable name="vSortedIndex">
<xsl:for-each select="$vCells">
<xsl:sort select="#date" order="descending" />
<xsl:sort select="#title" />
<xsl:value-of
select="format-number(count(preceding-sibling::msg) + 1,
$vIndexPattern)" />
</xsl:for-each>
</xsl:variable>
<xsl:template match="/">
<xsl:apply-templates select="demo/messages" />
</xsl:template>
<xsl:template match="messages">
<table>
<xsl:for-each select="$vCells[not(position() > $vNumRows)]">
<xsl:variable name="vRow" select="position()" />
<tr>
<xsl:for-each select="$vCells[not(position() > $vNumCols)]">
<xsl:variable name="vCol" select="position()" />
<xsl:variable name="vCell"
select="($vRow - 1) * $vNumCols + $vCol" />
<xsl:variable name="vIndex"
select="substring($vSortedIndex,
($vCell - 1) * $vIndexPatternLength + 1,
$vIndexPatternLength)" />
<xsl:variable name="vMessage"
select="$vCells[position() = $vIndex]" />
<xsl:choose>
<xsl:when test="$vMessage">
<xsl:apply-templates select="$vMessage"
mode="cell" />
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="empty-cell" />
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</tr>
</xsl:for-each>
</table>
</xsl:template>
<xsl:template match="msg" mode="cell">
<td>
<xsl:apply-templates select="." />
</td>
</xsl:template>
<xsl:template match="msg">
<xsl:value-of select="concat(#date, ' : ', #title)" />
</xsl:template>
<xsl:template name="empty-cell">
<td>
<xsl:text> </xsl:text>
</td>
</xsl:template>
<!-- http://www.exslt.org/str/functions/padding/ -->
<xsl:template name="padding">
<xsl:param name="length" select="0" />
<xsl:param name="chars" select="' '" />
<xsl:choose>
<xsl:when test="not($length) or not($chars)" />
<xsl:otherwise>
<xsl:variable name="string"
select="concat($chars, $chars, $chars, $chars, $chars,
$chars, $chars, $chars, $chars, $chars)" />
<xsl:choose>
<xsl:when test="string-length($string) >= $length">
<xsl:value-of select="substring($string, 1, $length)" />
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="padding">
<xsl:with-param name="length" select="$length" />
<xsl:with-param name="chars" select="$string" />
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
produces this HTML output
<table>
<tr>
<td>2011-07-07 : 1st message</td>
<td>2011-07-06 : 2nd message</td>
<td>2011-07-06 : 3rd message</td>
</tr>
<tr>
<td>2011-07-05 : 4th message</td>
<td>2011-07-05 : 5th message</td>
<td>2011-07-05 : 6th message</td>
</tr>
<tr>
<td>2011-07-05 : 7th message</td>
<td> </td>
<td> </td>
</tr>
</table>
Thanks Dimitre!
It is difficult, but not impossible to perform the required processing in XSLT 1.0 without using any extension functions:
<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:variable name="vNumCols"
select="/*/config/#n_columns"/>
<xsl:variable name="vItems" select="/*/messages/msg"/>
<xsl:variable name="vNumItems" select="count($vItems)"/>
<xsl:variable name="vNumRows" select=
"ceiling($vNumItems div $vNumCols)"/>
<xsl:variable name="vsortedInds">
<xsl:for-each select="$vItems">
<xsl:sort select="#date" order="descending"/>
<xsl:value-of select=
"format-number(count(preceding-sibling::msg)+1,
'0000'
)
"/>
</xsl:for-each>
</xsl:variable>
<xsl:template match="/">
<table>
<xsl:for-each select=
"$vItems[not(position() > $vNumRows)]">
<tr>
<xsl:variable name="vRow" select="position()"/>
<xsl:for-each select="$vItems[not(position() > $vNumCols)]">
<xsl:variable name="vcurIndIndex" select=
"($vRow -1)*$vNumCols + position()"/>
<xsl:variable name="vcurInd" select=
"substring($vsortedInds, 4*($vcurIndIndex -1) +1, 4)"/>
<xsl:variable name="vcurItem" select="$vItems[position()=$vcurInd]"/>
<xsl:if test="$vcurItem">
<td>
<xsl:value-of select="$vcurItem/#title"/>
</td>
</xsl:if>
</xsl:for-each>
</tr>
</xsl:for-each>
</table>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided XML document:
<demo>
<config n_columns="3" />
<messages>
<msg date="2011-07-06" title="2nd message" />
<title>message list</title>
<msg date="2011-07-05" title="4th message" />
<msg date="2011-07-06" title="3rd message" />
<msg date="2011-07-07" title="1st message" />
</messages>
</demo>
the significant part of the desired output is produced (I am leaving the rest as an exercise to the reader :) ):
<table>
<tr>
<td>1st message</td>
<td>2nd message</td>
<td>3rd message</td>
</tr>
<tr>
<td>4th message</td>
</tr>
</table>
Explanation:
In order to avoid having to convert an RTF to a node-set, we are using a string of the indexes of the sorted elements. Every index occupies four characters (left padded with zeroes when necessary). Then we are using these indexes in populating the rows of the table.
In order to avoid resursion, we are using the Piez method of iteration through N non-node items.
Do note: This solution assumes that the table will not contain more than 9999 cells. If more cells are expected, you can easily change the code, for example:
Replace:
format-number(count(preceding-sibling::msg)+1,
'0000'
)
with:
format-number(count(preceding-sibling::msg)+1,
'00000'
)
And replace:
<xsl:variable name="vcurInd" select=
"substring($vsortedInds, 4*($vcurIndIndex -1) +1, 4)"/>
with
<xsl:variable name="vcurInd" select=
"substring($vsortedInds, 5*($vcurIndIndex -1) +1, 5)"/>