XML/XSLT nested loop of attributes to produce HTML table - html

I am trying to produce a HTML table from some XML I have got through a SQL query. The XML produced looks like the following:
<root>
<form attribute1="1" attribute2="1" />
<form attribute1="1" attribute2="2" />
<form attribute1="1" attribute2="3" />
<form attribute1="2" attribute2="1" />
<form attribute1="2" attribute2="2" />
<form attribute1="3" attribute2="1" />
</root>
The table I am trying to produce needs to have a header row for each unique attribute1 with rows underneath for each attribute2, something like this:
<attribute1="1" />
<attribute2="1" />
<attribute2="2" />
<attribute2="3" />
<attribute1="2" />
<attribute2="1" />
<attribute2="2" />
<attribute1="3" />
<attribute2="1" />
I don't have much experience using XML/XSLT but I am hoping it would be possible to do something like loop through the forms, create a header row for each unique attribute1 then create data rows associated to the unique attribute1 underneath.

If you can only use XSLT 1.0, try this XSLT for starters, which uses the Muenchian Grouping method
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" indent="yes" />
<xsl:key name="form" match="form" use="#attribute1" />
<xsl:template match="root">
<xsl:copy>
<!-- Get first "form" element that occurs in each group -->
<xsl:for-each select="form[generate-id() = generate-id(key('form',#attribute1)[1])]">
<group>
<header><xsl:value-of select="#attribute1" /></header>
<!-- Get the "form" elements that make up the group -->
<xsl:apply-templates select="key('form', #attribute1)" />
</group>
</xsl:for-each>
</xsl:copy>
</xsl:template>
<xsl:template match="form">
<row><xsl:value-of select="#attribute2" /></row>
</xsl:template>
</xsl:stylesheet>
And here's some XSLT that can create an HTML table. This one is slightly more advanced as it works out the maximum number of columns for a row, and uses that in creating colspan attributes for the shorter rows
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html" doctype-public="XSLT-compat" omit-xml-declaration="yes" encoding="UTF-8" indent="yes" />
<xsl:key name="form" match="form" use="#attribute1" />
<xsl:variable name="maxCells">
<xsl:for-each select="/root/form[generate-id() = generate-id(key('form',#attribute1)[1])]">
<xsl:sort select="count(key('form', #attribute1))" order="descending" />
<xsl:if test="position() = 1">
<xsl:value-of select="count(key('form', #attribute1))" />
</xsl:if>
</xsl:for-each>
</xsl:variable>
<xsl:template match="root">
<table border="1">
<!-- Get first "form" element that occurs in each group -->
<xsl:for-each select="form[generate-id() = generate-id(key('form',#attribute1)[1])]">
<tr>
<th colspan="{$maxCells}">
<xsl:value-of select="#attribute1" />
</th>
</tr>
<tr>
<!-- Get the "form" elements that make up the group -->
<xsl:apply-templates select="key('form', #attribute1)" />
</tr>
</xsl:for-each>
</table>
</xsl:template>
<xsl:template match="form">
<td>
<xsl:if test="position() = last()">
<xsl:attribute name="colspan">
<xsl:value-of select="$maxCells - count(key('form', #attribute1)) + 1" />
</xsl:attribute>
</xsl:if>
<xsl:value-of select="#attribute2" />
</td>
</xsl:template>
</xsl:stylesheet>

Related

XSLT 1.0 - Generate HTML table using elements having unequal attribute count but same attribute name

I need to generate an HTML table using the attribute names as header and the attribute values as the data. However the elements that need to be looped do not have all the attributes present all the time. In such a scenario a blank <td> needs to be added to the table for all such non-available attribute values.
Below is a sample XML (a scaled down version with dummy values).
<root>
<oelement attr="abc">
<ielement attr="101">
<child attr1="a" attr2="b" attr3="c" attr4="d" attr5="e" />
<child attr1="e" attr2="f" attr3="g" attr4="h" attr5="i" />
</ielement>
<ielement attr="102">
<child attr1="x" attr3="y" attr5="w" />
<child attr1="j" attr3="k" attr5="l" />
</ielement>
</oelement>
<oelement attr="pqr">
<ielement attr="101">
<child attr1="g" attr2="j" attr3="t" attr4="y" attr5="r" />
<child attr1="d" attr2="q" attr3="a" attr4="c" attr5="b" />
</ielement>
<ielement attr="102">
<child attr1="t" attr3="y" attr5="u" />
<child attr1="i" attr3="o" attr5="p" />
</ielement>
</oelement>
<oelement attr="xyz">
<ielement attr="101">
<child attr1="h" attr2="o" attr3="u" attr4="z" attr5="x" />
</ielement>
<ielement attr="103">
<child attr1="q" attr3="w" attr5="e" />
</ielement>
</oelement>
</root>
Output HTML
I have tried to put together the following XSLT, however it does not match the attribute names in column header when loading the data in the appropriate column of the table.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" />
<xsl:strip-space elements="*" />
<xsl:template match="root">
<html>
<body>
<table border="1" cellspacing="0" cellpadding="5">
<tr>
<th>oelement</th>
<th>ielement</th>
<xsl:for-each select="//oelement[1]/ielement[1]/child[1]/#*">
<th><xsl:value-of select="local-name()" /></th>
</xsl:for-each>
</tr>
<xsl:apply-templates />
</table>
</body>
</html>
</xsl:template>
<xsl:template match="child">
<tr>
<td><xsl:value-of select="ancestor::oelement/#attr" /></td>
<td><xsl:value-of select="ancestor::ielement/#attr" /></td>
<xsl:for-each select="#*">
<td><xsl:value-of select="." /></td>
</xsl:for-each>
</tr>
</xsl:template>
</xsl:stylesheet>
Need help in doing the column matching when adding value and inserting blank value for non-matching column. I am stuck with XSLT 1.0 and cannot upgrade to XSLT 2.0 for finding the distinct-values() of the attribute names.
There is no nice solution to do this in XLST 1.0.
You can use a key to achieve the desired effect, though:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" />
<xsl:strip-space elements="*" />
<xsl:key name="child-by-attr-name" match="child/#*" use="name(.)"/>
<xsl:template match="root">
<html>
<body>
<table border="1" cellspacing="0" cellpadding="5">
<!-- skipped for simplicity -->
<xsl:apply-templates />
</table>
</body>
</html>
</xsl:template>
<xsl:template match="child">
<tr>
<td><xsl:value-of select="ancestor::oelement/#attr" /></td>
<td><xsl:value-of select="ancestor::ielement/#attr" /></td>
<xsl:variable name="current-child" select="."/>
<xsl:for-each select="(//child/#*)[generate-id(.) = generate-id(key('child-by-attr-name',name(.))[1])]">
<td>
<xsl:value-of select="$current-child/#*[name(.) = name(current())]" />
</td>
</xsl:for-each>
</tr>
</xsl:template>
The "trick" here is that the key will index all attributes (in every child element) by it's name. When you query the key by an attribute name, you get all occurrences of attributes with that name in document order. You can then use generate-id() to make sure you only get the first one of each name.
The key to success is a proper way to loop through attribure names.
To do this, I used:
A key (named attrib), matching child attributes and saving their names (name()).
Two loops, one creating the header, and another printing attribute values.
To cope with changes in the context object in the template matching child,
and for readability, I used 2 variables:
curr - the current child element.
nn - the name of the current attribute.
So the whole script can look like below:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html"/>
<xsl:strip-space elements="*" />
<xsl:key name="attrib" match="child/#*" use="name()"/>
<xsl:template match="root">
<html>
<body>
<table>
<tr>
<th>oelement</th>
<th>ielement</th>
<xsl:for-each select="//child/#*[generate-id()=generate-id(key('attrib', name())[1])]">
<th><xsl:value-of select="name()"/></th>
</xsl:for-each>
</tr>
<xsl:apply-templates select="//child"/>
</table>
</body>
</html>
</xsl:template>
<xsl:template match="child">
<tr>
<td><xsl:value-of select="ancestor::oelement/#attr" /></td>
<td><xsl:value-of select="ancestor::ielement/#attr" /></td>
<xsl:variable name="curr" select="."/>
<xsl:for-each select="//child/#*[generate-id()=generate-id(key('attrib', name())[1])]">
<td>
<xsl:variable name="nn" select="name()"/>
<xsl:value-of select="$curr/#*[name()=$nn]"/>
</td>
</xsl:for-each>
</tr>
</xsl:template>
</xsl:stylesheet>

How to transform a CSV matrix to a HTML table via XSLT 1.0?

The XML matrix with csv rows
<matrix ROWS="3" COLS="4">
<row>"1,2,3,4"</row>
<row>"5,6,7,8"</row>
<row>"9,10,11,12"</row>
</matrix>
is to be transformed with XSLT 1.0 without extensions into a HTML table
<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>
</table>
The number of rows and columns (ROWS, COLS) are variable.
I appreciate any help.
How about:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" omit-xml-declaration="yes" version="1.0" encoding="utf-8" indent="yes"/>
<xsl:template match="/matrix">
<table>
<xsl:for-each select="row">
<tr>
<xsl:call-template name="tokenize">
<xsl:with-param name="text" select="substring(., 2, string-length(.) - 2)"/>
</xsl:call-template>
</tr>
</xsl:for-each>
</table>
</xsl:template>
<xsl:template name="tokenize">
<xsl:param name="text"/>
<xsl:param name="delimiter" select="','"/>
<xsl:variable name="token" select="substring-before(concat($text, $delimiter), $delimiter)" />
<xsl:if test="$token">
<td>
<xsl:value-of select="$token"/>
</td>
</xsl:if>
<xsl:if test="contains($text, $delimiter)">
<!-- recursive call -->
<xsl:call-template name="tokenize">
<xsl:with-param name="text" select="substring-after($text, $delimiter)"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>

Create HTML TD cell when node or attribute missing

I am working on improving on some stylesheets that I have inherited and converting them from using <xsl:for-each> to <xsl:apply-templates>. A very simplified version of one of the XML files I will be working with is:
<Root>
<Row ID="123" Region="AMS">
<First>Graham</First>
<Last>Smith</Last>
<Sales>12345.85</Sales>
<Team>Team A</Team>
</Row>
<Row id="321">
<First>John</First>
<Last>Brown</Last>
<Sales>18765.85</Sales>
<Team>Team C</Team>
</Row>
<Row id="456" Region="EMEA">
<First>Anne</First>
<Last>Jones</Last>
<Sales>34567.85</Sales>
<Team>Team B</Team>
</Row>
</Root>
The new stylesheet I have is:
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output method="html" indent="yes"/>
<xsl:variable name="RowCount" select="count(/*/*)"/>
<xsl:template match="/#* | node()">
<style>
body * {font-family:Arial;font-size:11pt}
table {border-collapse:collapse}
td {border-bottom:1px solid #D8D8D8;padding:7px}
tr.row1 {background:#F9F9F9;}
td.tdHeader {border-bottom:2px solid #DDD;font-weight:700}
</style>
<table>
<thead>
<tr>
<xsl:apply-templates select="*[1]/#*" mode="headerAttributes" />
<xsl:apply-templates select="*[1]/*" mode="headerFields"/>
</tr>
</thead>
<tbody>
<xsl:apply-templates select="*"/>
</tbody>
</table>
</xsl:template>
<xsl:template match="/*/*/#*" mode="headerAttributes">
<td class="tdHeader">
<xsl:value-of select="name()" />
</td>
</xsl:template>
<xsl:template match="/*/*/*" mode="headerFields">
<td class="tdHeader">
<xsl:value-of select="name()" />
</td>
</xsl:template>
<xsl:template match="/*/*">
<tr class="row{position() mod 2}">
<xsl:apply-templates select="#*" mode="attributes"/>
<xsl:apply-templates select="*" mode="fields"/>
</tr>
</xsl:template>
<xsl:template match="/*/*/#*" mode="attributes">
<td>
<xsl:value-of select="." />
</td>
</xsl:template>
<xsl:template match="/*/*/*" mode="fields">
<td>
<xsl:value-of select="." />
</td>
</xsl:template>
</xsl:stylesheet>
but, due to the second node in the XML is missing the <Region> attribute, the cells on the result are mis-aligned, with first name now in the Region column, Last name in First name column and so on. This also happens if there is a missing child node on the Row node. for example, no team element
I have tried to test for a missing node, before calling the apply-template, and within the last two templates, but to no avail.
Any ideas? What am I missing here? I am only just starting to get my head around using apply-templates, but other methods of writing stylesheets I am fine with.
Provisionally, this works for your input:
<xsl:template match="Row">
<tr class="row{position() mod 2}">
<td><xsl:apply-templates select="#ID|#id" mode="attr2"/></td>
<td><xsl:apply-templates select="#Region" mode="attr2"/></td>
<xsl:apply-templates select="*" mode="fields"/>
</tr>
</xsl:template>
<xsl:template match="#ID|#id|#Region" mode="attr2">
<b><xsl:value-of select="." /></b>
</xsl:template>
-- and you may remove the catch-all for mode="attributes".
This forces the inclusion of a <td>..</td> pair even if there is no id|ID or Region attribute.
Saxon 8.8 reports an error:
The attribute axis starting at a document-node() node will never select anything
because of your
<xsl:template match="/#* | node()">
Changing it to <xsl:template match="node()">, or, preferably, to <xsl:template match="Root"> will fix this. As I said in my comment, try to use * as little as possible. References to Root and Row can be changed to exact matches.

XSL Delimited Values for HTML Links and their Descriptions

I am trying to debug some xsl code that puts links gathered from an XML Document with their descriptions in a list, it seems to be working, except when handling multiple links gathered from a single string that are delimited with a "|" and "^" symbol.
The XML Line that contains this code is very similar to this:
<WebContent>
<content_type_desc>Links</content_type_desc>
<content_value>Google|http://www.google.com^Yahoo|http://www.yahoo.com^Bing|Http://www.bing.com</content_value>
</WebContent>
The xsl that handles this is like
<?xml version="1.0" encoding="iso-8859-1"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"xmlns="http://www.w3.org/1999/xhtml">
<xsl:output method="html" indent="yes" encoding="utf-8"/>
<xsl:template match="/">
<!--Links-->
<xsl:variable name="links" select="//WebContent[content_type_desc='Links']/content_value "/>
<!--Links-->
<!--Links section-->
<xsl:if test="$links != ''">
<br />
<xsl:choose>
<xsl:when test="contains($links,'^')">
<xsl:variable name="URL" select="substring-after(substring-before($links,'^'),'|')"/>
<xsl:variable name="URLText" select="substring-before($links,'|')"/>
<strong>Links: </strong><br />
<xsl:value-of select="$URLText"/>
<br />
</xsl:when>
<xsl:otherwise>
<xsl:variable name="URL" select="substring-after($links,'|')"/>
<xsl:variable name="URLText" select="substring-before($links,'|')"/>
<strong>Links: </strong><br />
<xsl:value-of select="$URLText"/>
<br />
</xsl:otherwise>
</xsl:choose>
<br />
</xsl:if>
<!--Links section-->
</xsl:template>
</xsl:stylesheet>
What's wrong with this is that it only outputs
<a href="http://www.google.com>Google</a>
When I would like it to output
<a href="http://www.google.com>Google</a>
<a href="http://www.yahoo.com>Yahoo</a>
<a href="http://www.bing.com>Bing</a>
Any help will be greatly appreciated as this has me completely stumped.
Assuming you are actually only using XSLT 1.0, to solve this you could make use of a named-template which is called recursively.
At the moment, the code is only processing the URL before the first ^ in the code. What you could do is put the main bulk of the code that checks the links variable in a named template. Then, in the xsl:when condition that runs when the URL does contain a ^ you add a recursive call to the named template, but passing in only the substring after the ^.
Try this XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"xmlns="http://www.w3.org/1999/xhtml">
<xsl:output method="html" indent="yes" encoding="utf-8"/>
<xsl:template match="/">
<xsl:param name="links" select="//WebContent[content_type_desc='Links']/content_value "/>
<xsl:if test="$links != ''">
<strong>Links: </strong>
<br />
<xsl:call-template name="splitlinks">
<xsl:with-param name="links" select="$links" />
</xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template name="splitlinks">
<xsl:param name="links" />
<xsl:choose>
<xsl:when test="contains($links,'^')">
<xsl:variable name="URL" select="substring-after(substring-before($links,'^'),'|')"/>
<xsl:variable name="URLText" select="substring-before($links,'|')"/>
<xsl:value-of select="$URLText"/>
<br />
<xsl:call-template name="splitlinks">
<xsl:with-param name="links" select="substring-after($links,'^')" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="URL" select="substring-after($links,'|')"/>
<xsl:variable name="URLText" select="substring-before($links,'|')"/>
<xsl:value-of select="$URLText"/>
<br />
</xsl:otherwise>
</xsl:choose>
<br />
</xsl:template>
</xsl:stylesheet>
Given that you have tagged your question as classic ASP your XSLT processor is likely some version of MSXML for which there is an implementation of http://www.exslt.org/str/functions/tokenize/index.html so you could use that.

Counting unique XML element attributes using XSLT

I've just started using XSLT and am trying to figure this out.
Your help would be greatly appreciated.
Example XML file
<purchases>
<item id = "1" customer_id = "5">
</item>
<item id = "2" customer_id = "5">
</item>
<item id = "7" customer_id = "4">
</item>
</purchases>
Desired HTML Output:
<table>
<tr><th>Customer ID</th><th>Number of Items Purchased</th></tr>
<tr><td>5</td><td>2</td></tr>
<tr><td>4</td><td>1</td></tr>
</table>
Customer with id number 5 has bought 2 items.
Customer with id number 4 has bought 1 item.
XSLT 1.0 solution ....
<?xml version="1.0" encoding="utf-8"?>
<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:apply-templates select="purchases/item"/>
</table>
</xsl:template>
<xsl:template match="item">
<xsl:if test="not(
preceding-sibling::*[#customer_id=current()/#customer_id]
)">
<tr><td><xsl:value-of select="#customer_id"/></td>
<td><xsl:value-of select="count(following-sibling::item
[#customer_id=current()/#customer_id])+1"/></td>
</tr>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
A possible Xslt 1.0 solution would be
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" indent="yes"/>
<xsl:key name="kCustomer" match="item" use="#customer_id"/>
<xsl:template match="/">
<xsl:apply-templates />
</xsl:template>
<xsl:template match="purchases">
<xsl:element name="table">
<xsl:element name="tr">
<xsl:element name="th">Customer ID</xsl:element>
<xsl:element name="th">Number of Items Purchased</xsl:element>
<xsl:for-each select="item[generate-id(.) = generate-id(key('kCustomer', #customer_id))]">
<xsl:variable name="total" select="count(/purchases/item[#customer_id=current()/#customer_id]/#id)" />
<xsl:element name="tr">
<xsl:element name="td">
<xsl:value-of select="./#customer_id" />
</xsl:element>
<xsl:element name="td">
<xsl:value-of select="$total" />
</xsl:element>
</xsl:element>
</xsl:for-each>
</xsl:element>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
Adopted from Dimitres answer on How can I Group and sort based on sum.
As pointed out by Dimitre the purchases template can be simplified to
<xsl:template match="purchases">
<table>
<tr>
<th>Customer ID</th>
<th>Number of Items Purchased</th>
</tr>
<xsl:for-each select="item[generate-id(.) = generate-id(key('kCustomer', #customer_id))]">
<xsl:variable name="total" select="count(/purchases/item[#customer_id=current()/#customer_id]/#id)" />
<tr>
<td>
<xsl:value-of select="./#customer_id" />
</td>
<td>
<xsl:value-of select="$total" />
</td>
</tr>
</xsl:for-each>
</table>
</xsl:template>
If you want XSLT 2.0 then the following XSLT should do the trick:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xsl:output indent="yes"/>
<xsl:template match="/purchases">
<table>
<tr>
<th>Customer ID</th>
<th>Number of Items Purchased</th>
</tr>
<xsl:for-each-group select="item" group-by="#customer_id">
<tr>
<td>
<xsl:value-of select="current-grouping-key()"/>
</td>
<td>
<xsl:value-of select="count(current-group())"/>
</td>
</tr>
</xsl:for-each-group>
</table>
</xsl:template>
</xsl:stylesheet>