XSLT use first child node value in later child nodes - html

I got a HTML file to transform into a XML file using XSLT, where I need to grab only the first child node's date value to be used in the latter child nodes, since the latter child nodes do not have the data values and the tags will appear empty. I tried to use positioning to do that for the XSLT but to no avail.
The relevant HTML code is:
<tbody>
<tr>
<th rowspan="8" scope="rowgroup">2005DEC</th>
<th scope="row">Blemish on Card</th>
</tr>
<tr>
<th scope="row">Damaged</th>
</tr>
<tr>
<th rowspan="8" scope="rowgroup">2006JAN</th>
<th scope="row">Lost</th>
</tr>
<tr>
<th scope="row">Stolen</th>
</tr>
</tbody>
The relevant XSLT code is:
<xsl:for-each select="html/body/div/div/div/table/tbody/tr">
<Entry>
<xsl:choose>
<xsl:when test="th[#scope='rowgroup']">
<Year>
<xsl:value-of select="substring(th[#scope='rowgroup']/., 1, 4)" />
</Year>
<Month>
<xsl:value-of select="substring(th[#scope='rowgroup']/., 5, 3)" />
</Month>
</xsl:when>
<xsl:otherwise>
<Year>
<xsl:value-of select="substring(tr[1]/th[#scope='rowgroup']/., 1, 4)" />
</Year>
<Month>
<xsl:value-of select="substring(tr[1]/th[#scope='rowgroup']/., 5, 3)" />
</Month>
</xsl:otherwise>
</xsl:choose>
</Entry>
</xsl:for-each>
The XML code that is output is the following, which is not what I want as the second child node does not have the date value of the first child node and just shows up as an empty value tag for year and month:
<Entry>
<Year>2005</Year>
<Month>DEC</Month>
<Reason>Blemish on Card</Reason>
</Entry>
<Entry>
<Year/>
<Month/>
<Reason>Damaged</Reason>
</Entry>
<Entry>
<Year>2006</Year>
<Month>JAN</Month>
<Reason>Lost</Reason>
</Entry>
<Entry>
<Year/>
<Month/>
<Reason>Stolen</Reason>
</Entry>
What I need is:
<Entry>
<Year>2005</Year>
<Month>DEC</Month>
<Reason>Blemish on Card</Reason>
</Entry>
<Entry>
<Year>2005</Year>
<Month>DEC</Month>
<Reason>Damaged</Reason>
</Entry>
<Entry>
<Year>2006</Year>
<Month>JAN</Month>
<Reason>Lost</Reason>
</Entry>
<Entry>
<Year>2006</Year>
<Month>JAN</Month>
<Reason>Stolen</Reason>
</Entry>
How do I do that for my XSLT?

You have to navigate up first: substring(../tr[1]/th[#scope='rowgroup']/., 1, 4) if you want to select the data from the first row.
Based on your comments that is not what you want, if you want to select the data from the first preceding sibling row with a scope="rowgroup" attribute then one way, continuing with your approach is
<xsl:for-each select="//tbody/tr">
<Entry>
<xsl:choose>
<xsl:when test="th[#scope='rowgroup']">
<Year>
<xsl:value-of select="substring(th[#scope='rowgroup']/., 1, 4)" />
</Year>
<Month>
<xsl:value-of select="substring(th[#scope='rowgroup']/., 5, 3)" />
</Month>
</xsl:when>
<xsl:otherwise>
<Year>
<xsl:value-of select="substring(preceding-sibling::tr[th[#scope='rowgroup']][1]/th[#scope='rowgroup'], 1, 4)" />
</Year>
<Month>
<xsl:value-of select="substring(preceding-sibling::tr[th[#scope='rowgroup']][1]/th[#scope='rowgroup'], 5, 3)" />
</Month>
</xsl:otherwise>
</xsl:choose>
<xsl:for-each select="th[#scope='row']">
<Reason>
<xsl:value-of select="." />
</Reason>
</xsl:for-each>
</Entry>
</xsl:for-each>
although I might be easier to use for-each-group group-starting-with="tr[th[#scope = 'rowgroup']]" if you use an XSLT 2.0 processor.

Related

How to pass rgb value coming in soap xml converting it into xsl and pass the rgb value

In my soap response XML I am getting "options" parent node repeatedly.
Inside Parent node I have node"rgb" that contains some value. i am creating html with that data.
From Parent I am using description and when that description i m putting in the td I need to have the td color as rgb value in the option node present
Sample XML
<?xml version='1.0' encoding='UTF-8'?><S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
<S:Body>
<ToggleOptionResponse xmlns="urn:configcompare4g.kp.chrome.com">
<status>None</status>
<originatingChromeOptionCode>SM</originatingChromeOptionCode>
<originatingOptionAnAddition>true</originatingOptionAnAddition>
<requiresToggleToResolve>false</requiresToggleToResolve>
<configuration>
<options>
<headerName>PRIMARY PAINT</headerName>
<consumerFriendlyHeaderId>10</consumerFriendlyHeaderId>
<consumerFriendlyHeaderName>Exterior</consumerFriendlyHeaderName>
<optionKindId>68</optionKindId>
<descriptions>
<description>Shadow Black</description>
<type>PrimaryName</type>
</descriptions>
<uniqueTypeFilter>N</uniqueTypeFilter>
<rgbValue>0A0A0C</rgbValue>
</options>
<options>
<headerName>PRIMARY PAINT</headerName>
<consumerFriendlyHeaderId>10</consumerFriendlyHeaderId>
<consumerFriendlyHeaderName>Exterior</consumerFriendlyHeaderName>
<optionKindId>68</optionKindId>
<descriptions>
<description>Ruby Red Metallic Tinted Clearcoat</description>
<type>PrimaryName</type>
</descriptions>
<rgbValue>570512</rgbValue>
</options>
</configuration>
</ToggleOptionResponse>
</S:Body>
</S:Envelope>
Corresponding XSL
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:S="http://schemas.xmlsoap.org/soap/envelope/" xmlns:p="urn:configcompare4g.kp.chrome.com" version="1.0"
exclude-result-prefixes="p">
<xsl:template match="/">
<html>
<body>
<h2>My CD Collection</h2>
<table border="1">
<xsl:for-each select="//p:ToggleOptionResponse/p:configuration/p:options">
<tr bgcolor="#9acd32">
<xsl:for-each select="p:headerName[not(.=preceding::*)]">
<th><xsl:value-of select="." /></th>
</xsl:for-each>
</tr>
<tr>
<td><xsl:value-of select="p:consumerFriendlyHeaderName"/></td>
<xsl:if test="p:headerName != 'PRIMARY PAINT'">
<td><xsl:for-each select="p:descriptions/p:description">
<xsl:if test="position() > 1 ">, </xsl:if>
<xsl:value-of select="."/><xsl:text> </xsl:text></xsl:for-each></td>
</xsl:if>
<xsl:if test="p:headerName = 'PRIMARY PAINT'">
<td bgcolor ='#<xsl:value-of select="p:rgbValue">'>
<xsl:for-each select="p:descriptions/p:description">
<xsl:if test="position() > 1 ">, </xsl:if>
<xsl:value-of select="."/><xsl:text> </xsl:text></xsl:for-each></td>
</xsl:if>
</tr>
</xsl:for-each>
</table>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
I am trying xslt first time, need inputs
Thanks in Advance
You need to use an Attribute Value Template here....
Instead of doing this...
<td bgcolor ='#<xsl:value-of select="p:rgbValue">'>
Do this....
<td bgcolor ='#{p:rgbValue}'>
The curly braces indicate an expression to be evaluated, whose result will then be placed in the attribute.

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.

Using an XSL for-each on identical XML children

In brief, my problem is that I want to loop through the contents of one child among identical children, but when I try to do so, it loops through the contents of all the children, giving me as many duplicates of the data as there are children.
Example code:
Input.xml
<?xml version="1.0"?>
<base>
<item name="item1">
<foo>
<type1>1</type1>
<type2>2</type2>
</foo>
<bar>
<type3>3</type3>
</bar>
</item>
<item name="item2">
<foo>
<type1>4</type1>
<type2>5</type2>
</foo>
<bar>
<type3></type3>
<!-- NOTE! This value is missing. Therefore we must put a blank value in the table-->
</bar>
</item>
<item name="item3">
<foo>
<type1>7</type1>
<type2></type2>
<!-- NOTE! This value is missing. Therefore we must put a blank value in the table-->
</foo>
<bar>
<type3>9</type3>
</bar>
</item>
</base>
tableMaker.xsl
<?xml version="1.0"?>
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:cyg="http://CygNetSCADA.com/Schemas/CygNetEnterpriseObjects730">
<xsl:output method="html" encoding="UTF-8" />
<xsl:template match="/">
<html>
<body>
<table border="1">
<tr>
<th>Name</th>
<xsl:for-each select="base/item/*/*">
<th>
<xsl:value-of select="local-name()"/>
</th>
</xsl:for-each>
</tr>
<xsl:for-each select="base/item">
<tr>
<th>
<xsl:value-of select="#name"/>
</th>
<xsl:for-each select="foo/*">
<xsl:choose>
<xsl:when test=".[node()]">
<td>
<xsl:value-of select="." />
</td>
</xsl:when>
<xsl:otherwise>
<td />
<!-- This is for that empty value in item3 -->
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
<xsl:for-each select="bar/*">
<xsl:choose>
<xsl:when test=".[node()]">
<td>
<xsl:value-of select="." />
</td>
</xsl:when>
<xsl:otherwise>
<td />
<!-- This is for that empty value in item2 -->
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</tr>
</xsl:for-each>
</table>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
What happens in this example is that the generated HTML has 10 columns - one for "name", and "type1", "type2", "type3" three times (three times for the three elements; if there were 4 elements in my input there would be 3 more columns). I only want "type1", "type2", and "type3" to display once in the column. How might I go about doing this?
All help is appreciated and thanks in advance!
You're using XSL 2.0 so presumably you have access to the xsl:for-each-group element. It's what you need in this situation:
<tr>
<th>Name</th>
<xsl:for-each-group select="base/item/*/*" group-by="local-name()">
<th>
<xsl:value-of select="current-grouping-key()"/>
</th>
</xsl:for-each>
</tr>

XSLT counting "identical" elements based on a subset of descendants

I'm transforming XML to HTML. In part of the XML, I need to count elements that seem "identical" when specific descendants are compared, while other descendants and any attributes must be ignored.
Here's a simplified example of my XML. The real thing is much more complicated; I've just removed elements that aren't relevant to the counting:
<complaints>
<lodgedComplaint>
<defendant>First Person</defendant>
<charge>
<actionNumber>1</actionNumber>
<offence>
<offenceSection>
<legislation>SA1</legislation>
<description>Stealing</description>
</offenceSection>
<placeOfOffence>Sydney</placeOfOffence>
<dateOfOffence>2010-01-01</dateOfOffence>
<particular>value=20</particular>
</offence>
</charge>
<charge>
<actionNumber>2</actionNumber>
<offence>
<offenceSection>
<legislation>SA2</legislation>
<description>Theft</description>
</offenceSection>
<placeOfOffence>Sydney</placeOfOffence>
<dateOfOffence>2010-01-01</dateOfOffence>
</offence>
</charge>
<charge>
<actionNumber>3</actionNumber>
<offence>
<offenceSection>
<legislation>SA2</legislation>
<description>Theft</description>
</offenceSection>
<placeOfOffence>London</placeOfOffence>
<dateOfOffence>2010-01-01</dateOfOffence>
</offence>
</charge>
<charge>
<actionNumber>4</actionNumber>
<offence>
<offenceSection>
<legislation>SA1</legislation>
<description>Stealing</description>
</offenceSection>
<placeOfOffence>Sydney</placeOfOffence>
<dateOfOffence>2010-01-01</dateOfOffence>
<particular>value=50</particular>
</offence>
</charge>
<charge>
<actionNumber>5</actionNumber>
<offence>
<offenceSection>
<legislation>SA2</legislation>
<description>Theft</description>
</offenceSection>
<placeOfOffence>Sydney</placeOfOffence>
<dateOfOffence>2010-01-02</dateOfOffence>
</offence>
</charge>
</lodgedComplaint>
<lodgedComplaint>
<defendant>Second Person</defendant>
<charge>
<actionNumber>1</actionNumber>
<offence>
<offenceSection>
<legislation>SA1</legislation>
<description>Stealing</description>
</offenceSection>
<placeOfOffence>Sydney</placeOfOffence>
<dateOfOffence>2010-01-01</dateOfOffence>
<particular>value=35</particular>
</offence>
</charge>
</lodgedComplaint>
</complaints>
In each lodgedComplaint, any two charges should be considered identical if their legislation, description, placeOfOffence and dateOfOffence elements match. The actionNumber and particular elements must be ignored (the particular element is optional and unbounded in the schema I've been given).
The desired output should look something like this:
First Person
2 counts of Stealing at Sydney on 1/1/2010 under SA1
1 count of Theft at Sydney on 1/1/2010 under SA2
1 count of Theft at London on 1/1/2010 under SA2
1 count of Theft at Sydney on 2/1/2010 under SA2
Second Person
1 count of Stealing at Sydney on 1/1/2010 under SA1
Here's an XSLT I tried, based on things I've read on Stack Overflow and elsewhere. It doesn't work; the charge details don't appear at all. I think my use of concat() causes problems.
How can I do this?
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" encoding="UTF-8" indent="yes"/>
<xsl:key name="matchOffence" match="offence" use="concat(offenceSection/legislation,offenceSection/description,placeOfOffence,dateOfOffence)"/>
<xsl:template match="/">
<html>
<head>
<title>CRIMES Listings</title>
<link rel="stylesheet" type="text/css" href="global_styles.css"/>
<link rel="stylesheet" type="text/css" href="styles.css"/>
</head>
<body>
<xsl:apply-templates select="complaints/lodgedComplaint"/>
</body>
</html>
</xsl:template>
<xsl:template match="lodgedComplaint">
<br/>
<xsl:value-of select="defendant"/>
<xsl:for-each select="charge/offence[generate-id(concat(offenceSection/legislation,offenceSection/description,placeOfOffence,dateOfOffence))=generate-id(key('matchOffence',.))]">
<br/>
<xsl:value-of select="count(../../charge/offence[(offenceSection/legislation=current()/offenceSection/legislation) and (offenceSection/description=current()/offenceSection/description) and (placeOfOffence=current()/placeOfOffence) and (dateOfOffence=current()/dateOfOffence)])"/>
counts of <b><xsl:value-of select="offenceSection/description"/></b>
at <b><xsl:value-of select="placeOfOffence"/></b>
on <b><xsl:call-template name="date">
<xsl:with-param name="text" select="dateOfOffence"/>
</xsl:call-template></b>
under <b><xsl:value-of select="offenceSection/legislation"/></b>
</xsl:for-each>
</xsl:template>
<xsl:template name="date">
<xsl:param name="text" select="."/>
<xsl:choose>
<xsl:when test="contains($text,'-')">
<xsl:call-template name="date">
<xsl:with-param name="text" select="substring-after($text,'-')"/>
</xsl:call-template>/<xsl:value-of select="substring-before($text,'-')"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$text"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
There are two problems with your stylesheet.
You are trying to generate-id() using the results of the concat(), which produces a string result. generate-id() only works for nodes.
It should be re-written as: <xsl:for-each select="charge/offence[generate-id(.)=generate-id(key('matchOffence',concat(offenceSection/legislation,./offenceSection/description,placeOfOffence,dateOfOffence)))]">
The other problem is that the string value used for the key is not unique. The offence for the second person has the same values as one of the offense for the first person. This prevents you from generating output for the second person. You can make the key more unique by adding in the value of the defendant element in your key.
After adjusting the key, then you would obviously need to adjust the for-each.
Putting it all together in your stylesheet:
<?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" encoding="UTF-8" indent="yes"/>
<xsl:key name="matchOffence" match="offence" use="concat(../../defendant,offenceSection/legislation,offenceSection/description,placeOfOffence,dateOfOffence)"/>
<xsl:template match="/">
<html>
<head>
<title>CRIMES Listings</title>
<link rel="stylesheet" type="text/css" href="global_styles.css"/>
<link rel="stylesheet" type="text/css" href="styles.css"/>
</head>
<body>
<xsl:apply-templates select="complaints/lodgedComplaint"/>
</body>
</html>
</xsl:template>
<xsl:template match="lodgedComplaint">
<br/>
<xsl:value-of select="defendant"/>
<xsl:for-each select="charge/offence[generate-id(.)=generate-id(key('matchOffence',concat(../../defendant,offenceSection/legislation,./offenceSection/description,placeOfOffence,dateOfOffence)))]">
<br/>
<xsl:value-of select="count(../../charge/offence[(offenceSection/legislation=current()/offenceSection/legislation) and (offenceSection/description=current()/offenceSection/description) and (placeOfOffence=current()/placeOfOffence) and (dateOfOffence=current()/dateOfOffence)])"/>
counts of <b><xsl:value-of select="offenceSection/description"/></b>
at <b><xsl:value-of select="placeOfOffence"/></b>
on <b><xsl:call-template name="date">
<xsl:with-param name="text" select="dateOfOffence"/>
</xsl:call-template></b>
under <b><xsl:value-of select="offenceSection/legislation"/></b>
</xsl:for-each>
</xsl:template>
<xsl:template name="date">
<xsl:param name="text" select="."/>
<xsl:choose>
<xsl:when test="contains($text,'-')">
<xsl:call-template name="date">
<xsl:with-param name="text" select="substring-after($text,'-')"/>
</xsl:call-template>/<xsl:value-of select="substring-before($text,'-')"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$text"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>

How to transform xml and keep newlines?

I'm trying to preserve row breaks in an xml file when transforming it to html, but I cant find a way that works.
<meta>
<name>Message</name>
<value>Hi!
I need info!
Mr Test</value>
</meta>
And I use this xsl:
<xsl:if test="name='Message'">
<tr>
<th align="left" colspan="2">Message:</th>
</tr>
<tr>
<td colspan="2"><xsl:value-of select="value"/></td>
</tr>
</xsl:if>
But the new line (cr/lf) characters dissapear, and everything becomes one single line in html. Is is possible to match the cr/lf and replace them with html "<_br >", or any other method?
Add the following template to your XSL:-
<xsl:template name="LFsToBRs">
<xsl:param name="input" />
<xsl:choose>
<xsl:when test="contains($input, '
')">
<xsl:value-of select="substring-before($input, '
')" /><br />
<xsl:call-template name="LFsToBRs">
<xsl:with-param name="input" select="substring-after($input, '
')" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$input" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
Now replace where select the value with a call to this template:-
<td colspan="2">
<xsl:call-template name="LFsToBRs">
<xsl:with-param name="input" select="value"/>
</xsl:call-template>
</td>
Greetings, if you already have LF or CR in the source xml (looks like you have),
try putting in a with "white-space: pre" style.
i.e:
<div style="white-space: pre;">
<xsl:value-of select="value"/>
</div>