I am transforming xml data to html page wiith the help of xslt . I want to eliminate duplicate data where appears like this in the following way .
xml data
<calendar>
<event>
<date>May 11</date>
<description>Mother's Day</description>
</event>
<event>
<date>May 12</date>
<description>Birthday</description>
</event>
<event>
<date>May 12</date>
<description>Board Meeting</description>
</event>
</calendar>
My xslt code
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<html>
<body>
<h2>Event Dates </h2>
<table border="1">
<tr bgcolor="#9acd32">
<th>date</th>
<th>description</th>
</tr>
<xsl:for-each select="calendar/event">
<tr>
<td><xsl:value-of select="date"/></td>
<td><xsl:value-of select="description"/></td>
</tr>
</xsl:for-each>
</table>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
My output
date description
May 11 Mother's Day
May 12 Birthday
May 12 Board Meeting
Desired Output.
date description
May 11
Mother's Day
May 12
Birthday
Board Meeting
Please suggest me the XSLT code to modify .
Thanks in advance .
I found this solution and applied to your problem.
Jenni Tennison wrote a nice and short explanation of the method.
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
<xsl:output method="text" indent="yes"/>
<xsl:key name="distinct-date" match="/calendar/event/date" use="./text()"/>
<xsl:template match="calendar">
<xsl:text>date description
</xsl:text>
<xsl:for-each select="event/date[generate-id(.) = generate-id(key('distinct-date',.)[1])]">
<xsl:value-of select="./text()"/>
<xsl:text>
</xsl:text>
<xsl:apply-templates select="//event[date/text() = current()/text()]"/>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
<xsl:template match="event">
<xsl:text> </xsl:text><xsl:value-of select="description/text()"/>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
This short transformation:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:key name="kDateByVal" match="date" use="."/>
<xsl:template match="/">
<xsl:text>date description</xsl:text>
<xsl:apply-templates/>
</xsl:template>
<xsl:template match=
"date[generate-id()=generate-id(key('kDateByVal',.)[1])]">
<xsl:value-of select="concat('
',.)"/>
<xsl:for-each select="key('kDateByVal',.)">
<xsl:value-of select="concat('
',' ', ../description)"/>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:template>
<xsl:template match="text()"/>
</xsl:stylesheet>
uses the classic Muenchian grouping method to transform the provided XML document:
<calendar>
<event>
<date>May 11</date>
<description>Mother's Day</description>
</event>
<event>
<date>May 12</date>
<description>Birthday</description>
</event>
<event>
<date>May 12</date>
<description>Board Meeting</description>
</event>
</calendar>
into the wanted, correct result:
date description
May 11
Mother's Day
May 12
Birthday
Board Meeting
The only way to solve your problem is a so called "Muenchian Grouping". Please refer to
Muenchian Grouping - group within a node, not within the entire document which is pretty much the same as your question, only with names instead of days.
Related
I am trying to merge two XML files "a.xml" and "b.xml" into a HTML table using XSLT 1.0. Both files contain elements called "event" that each have a "time" attribute with a dateTime value attached to them. I want the HTML table to be sorted chronologically. Whereas the time attributes of file "a.xml" are formated correctly (CCYY-MM-DDTHH:MM:SS.msmsms), the time attributes of "b.xml" are not (CCYY-DDDTHH:MM:SS.msmsmsZ) and thus I am using some concats and substring functions to construct the correct format for the "time" attributes of the "b.xml" elements. My question is now: How can I use the original "time" attributes of "a.xml" and the corrected attributes of "b.xml" for sorting the rows of the HTML table?
I already tried using parameters for storing the correctly formated "time" attributes. I also tried using node-sets to tackle the issue in two steps (i.e. converting "b.xml" attributes first, saving result and then creating the HTML from the intermediate file), but neither of these two ways worked for me. Lastly I tried sorting the HTML table on load of the page with a JavaScript, but the table is too big for doing it this way on each page load.
I am happy about every hint on a functionality of XSLT that could help me. I have to stick with XSLT1.0, though and can't use XSLT2.0 for this project.
a.xml
<?xml version="1.0" encoding="UTF-8"?>
<data>
<event time="2019-02-03T06:00:00.000"></event>
<event time="2019-02-01T06:00:00.000"></event>
</data>
b.xml before formating
<?xml version="1.0" encoding="UTF-8"?>
<data>
<event time="2019-035T06:00:00.000"></event>
<event time="2019-033T06:00:00.000"></event>
</data>
b.xml after formating
<?xml version="1.0" encoding="UTF-8"?>
<data>
<event time="2019-02-04T06:00:00.000"></event>
<event time="2019-02-02T06:00:00.000"></event>
</data>
current transform.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="/">
<html>
<body>
<table>
<xsl:apply-templates select="/Adata/event|document('b.xml')/Bdata/event">
<xsl:sort select="#time"/>
</xsl:apply-templates>
</table>
</body>
</html>
</xsl:template>
<xsl:template match="Bdata/event">
<!--Here I have some long operation to change the date format and save it as parameter "correctFormat"-->
<xsl:attribute name="time">
<xsl:value-of select="$correctFormat"/>
</xsl:attribute>
</xsl:template>
<xsl:template match="//event">
<tr>
<td><xsl:value-of select="#time"/></td>
</tr>
</xsl:template>
</xsl:stylesheet>
expected output out.html
<html>
<body>
<table>
<tr><td>2019-02-01T06:00:00.000</td></tr>
<tr><td>2019-02-02T06:00:00.000</td></tr>
<tr><td>2019-02-03T06:00:00.000</td></tr>
<tr><td>2019-02-04T06:00:00.000</td></tr>
</table>
</body>
</html>
EDIT
Date conversion code
As requested I share as well my code for the conversion. I am using the exslt dates and time namespace by adding inside the header
<xsl:template match="data/event">
<xsl:param name="daysToAdd" select="concat('P',substring(#time,6,3),'D')"/>
<xsl:param name="startOfYear" select="concat(substring(#time,1,4),'-01-00')"/>
<xsl:param name="formatedDate">
<xsl:call-template name="date:add">
<xsl:with-param name="date-time" select="$startOfYear" />
<xsl:with-param name="duration" select="$daysToAdd" />
</xsl:call-template>
<xsl:value-of select="substring(#time,9,13)"/>
</xsl:param>
<xsl:copy>
<xsl:attribute name="time">
<xsl:value-of select="$formatedDate"/>
</xsl:attribute>
</xsl:copy>
</xsl:template>
Consider the following example (minimized to the problem presented in your question):
XML
<data>
<event time="2019-02-03T06:00:00.000"></event>
<event time="2019-02-01T06:00:00.000"></event>
</data>
b.xml
<data>
<event time="2019-035T06:00:00.000"></event>
<event time="2019-033T06:00:00.000"></event>
</data>
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/data">
<!-- CONVERT B -->
<xsl:variable name="b">
<xsl:for-each select="document('b.xml')/data/event">
<xsl:copy>
<xsl:attribute name="time">
<!-- the missing conversion part goes here -->
</xsl:attribute>
</xsl:copy>
</xsl:for-each>
</xsl:variable>
<!-- OUTPUT -->
<xsl:copy>
<xsl:for-each select="event | exsl:node-set($b)/event">
<xsl:sort select="#time" data-type="text" order="ascending"/>
<xsl:copy-of select="."/>
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Result
<?xml version="1.0" encoding="UTF-8"?>
<data>
<event time="2019-02-01T06:00:00.000"/>
<event time="2019-02-02T06:00:00.000"/>
<event time="2019-02-03T06:00:00.000"/>
<event time="2019-02-04T06:00:00.000"/>
</data>
I am facing an xslt/xpath problem and hope someone could help, in a few words here is what I try to achieve.
I have to transform an XML document where some nodes may be missing, these missing nodes are mandatory in the final result. I have the set of mandatory node names available in an xsl:param.
The base document is:
<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="TRANSFORM.xslt"?>
<BEGIN>
<CLIENT>
<NUMBER>0021732561</NUMBER>
<NAME1>John</NAME1>
<NAME2>Connor</NAME2>
</CLIENT>
<PRODUCTS>
<PRODUCT_ID>12</PRODUCT_ID>
<DESCRIPTION>blah blah</DESCRIPTION>
</PRODUCTS>
<PRODUCTS>
<PRODUCT_ID>13</PRODUCT_ID>
<DESCRIPTION>description ...</DESCRIPTION>
</PRODUCTS>
<OPTIONS>
<OPTION_ID>1</OPTION_ID>
<DESCRIPTION>blah blah blah ...</DESCRIPTION>
</OPTIONS>
<PROMOTIONS>
<PROMOTION_ID>1</PROMOTION_ID>
<DESCRIPTION>blah blah blah ...</DESCRIPTION>
</PROMOTIONS>
</BEGIN>
Here is the stylesheet so far:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:fn="http://www.w3.org/2005/xpath-functions">
<xsl:output method="xml" encoding="UTF-8" indent="yes"/>
<xsl:param name="mandatoryNodes" as="xs:string*" select=" 'PRODUCTS', 'OPTIONS', 'PROMOTIONS' "/>
<xsl:template match="/">
<xsl:apply-templates select="child::node()"/>
</xsl:template>
<xsl:template match="node()">
<xsl:copy>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template match="BEGIN">
<xsl:element name="BEGIN">
<xsl:for-each select="$mandatoryNodes">
<!-- If there is no node with this name -->
<xsl:if test="count(*[name() = 'current()']) = 0">
<xsl:element name="{current()}" />
</xsl:if>
</xsl:for-each>
<xsl:apply-templates select="child::node()"/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
I tried the transformation in XML Spy, the xsl:iftest failed saying that 'current item is PRODUCTS of type xs:string.
I've tried the same xsl:if outside of a for-each and it seems to work ... what am I missing ?
Inside of <xsl:for-each select="$mandatoryNodes"> the context item is a string but you want to access the primary input document and its nodes so you need to store that document or the template's context node in a variable and use that e.g.
<xsl:template match="BEGIN">
<xsl:variable name="this" select="."/>
<xsl:element name="BEGIN">
<xsl:for-each select="$mandatoryNodes">
<!-- If there is no child node of `BEGIN` with this name -->
<xsl:if test="count($this/*[name() = current()]) = 0">
<xsl:element name="{current()}" />
</xsl:if>
</xsl:for-each>
<xsl:apply-templates select="child::node()"/>
</xsl:element>
</xsl:template>
Excuse my ignorance. I am just beginning XSL and XML transformations.
I receive xml data from a vendor.
I only need to include certain "ids" in my transformation.
I also need to add a "display name" based on the ID to the final output.
I would be able to manual add the ID and Display names necessary into the XSL.
XML ex.
<root>
<DATA>
<ID>rd_bl</ID>
<travel>15</travel<
<delay>7</delay>
</DATA>
<DATA>
<ID>yl_gr</ID>
<travel>18</travel<
<delay>9</delay>
</DATA>
<DATA>
<ID>pu_gr</ID>
<travel>17</travel<
<delay>6</delay>
</DATA>
</root>
I would like to write a list of IDs and "display names" in the xsl - only the records with the listed IDs would be included.
ID - Display Name
rd_bl - Red to Blue
pu_gr - Purple to Green
In this example the data from yl_gr would be ignored and not show up in the transformation.
Any help is greatly appreciated.
Thanks!
Here’s a simple stylesheet that checks whether an ID is within an approved list of IDs and uses a “display name” for it in the output.
<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:variable name="desired-ids">
<id name="rd_bl">Red to Blue</id>
<id name="pu_gr">Purple to Green</id>
</xsl:variable>
<xsl:template match="root">
<root>
<xsl:apply-templates />
</root>
</xsl:template>
<xsl:template match="DATA">
<xsl:variable name="current-id" select="ID/text()" />
<xsl:if test="$desired-ids/id[#name=$current-id]">
<entry>
<name>
<xsl:value-of select="$desired-ids/id[#name=$current-id]" />
</name>
<travel>
<xsl:value-of select="travel" />
</travel>
<delay>
<xsl:value-of select="delay" />
</delay>
</entry>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Output using your example XML after correcting the closing tag errors:
<root>
<entry>
<name>Red to Blue</name>
<travel>15</travel>
<delay>7</delay>
</entry>
<entry>
<name>Purple to Green</name>
<travel>17</travel>
<delay>6</delay>
</entry>
</root>
EDIT: in case you’re stuck with XSL 1.0:
<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:variable name="desired-ids">rd_bl="Red to Blue" pu_gr="Purple to Green"</xsl:variable>
<xsl:template match="root">
<root>
<xsl:apply-templates />
</root>
</xsl:template>
<xsl:template match="DATA">
<xsl:variable name="current-id" select="ID/text()" />
<xsl:variable name="id-with-equals" select="concat($current-id, '=')" />
<xsl:if test="contains($desired-ids, $id-with-equals)">
<xsl:variable name="id-with-open-quote" select="concat($id-with-equals, '"')" />
<xsl:variable name="display-name" select="substring-before(substring-after($desired-ids, $id-with-open-quote), '"')" />
<entry>
<name>
<xsl:value-of select="$display-name" />
</name>
<travel>
<xsl:value-of select="travel" />
</travel>
<delay>
<xsl:value-of select="delay" />
</delay>
</entry>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
You can see this is much less elegant, it uses awkward string-matching to check for a valid ID and extract the display name.
Will this stylesheet help?
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output indent="yes"/>
<xsl:template match="/">
<table>
<thead>
<th>ID</th>
<th>Display Name</th>
</thead>
<tbody>
<xsl:apply-templates select="root/DATA"/>
</tbody>
</table>
</xsl:template>
<xsl:template match="DATA">
<xsl:choose>
<xsl:when test="ID='rd_bl'">
<tr>
<td><xsl:value-of select="ID"/></td>
<td>Red to Blue</td>
</tr>
</xsl:when>
<xsl:when test="ID='pu_gr'">
<tr>
<td><xsl:value-of select="ID"/></td>
<td>Purple to Green</td>
</tr>
</xsl:when>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
I have below scenario for my XML.
<content>
<para>text-1 <emphasis type="bold">text-2</emphasis> text-3</para>
</content>
I want to parse it like below
<content>
<p>text-1 <b>text-2</b> text-3</p>
</content>
I have created my XSLT as below
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output method="xml" encoding="ISO-8859-1" indent="no"/>
<xsl:template name="para">
<p>
<xsl:value-of select="text()" disable-output-escaping="yes"/>
<xsl:for-each select="child::*">
<xsl:if test="name()='emphasis'">
<xsl:call-template name="emphasis"/>
</xsl:if>
</xsl:for-each>
</p>
</xsl:template>
<xsl:template name="emphasis">
<xsl:if test="attribute::type = 'bold'">
<b>
<xsl:value-of select="text()" disable-output-escaping="yes"/>
</b>
</xsl:if>
</xsl:template>
<xsl:template match="/">
<content>
<xsl:for-each select="content/child::*">
<xsl:if test="name()='para'">
<xsl:call-template name="para"/>
</xsl:if>
</xsl:for-each>
</content>
</xsl:template>
</xsl:stylesheet>
XSLT provided above is generating output like below
<content>
<p>text-1 text-3<b>text-2 </b></p>
</content>
Please guide me with your suggestions, how can I get my desire output?
To do this, you just need to extend the standard Identity Transform with special cases for matching your para and emphasis elements. For example, for para elements you would the following to replace para with p and then continue matching all the child nodes
<xsl:template match="para">
<p>
<xsl:apply-templates select="#*|node()"/>
</p>
</xsl:template>
So, given the following XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" />
<!-- This is the Identity Transform -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<!-- Replace para with p -->
<xsl:template match="para">
<p>
<xsl:apply-templates select="#*|node()"/>
</p>
</xsl:template>
<!-- Replace emphasis with b -->
<xsl:template match="emphasis[#type='bold']">
<b>
<xsl:apply-templates select="node()"/>
</b>
</xsl:template>
</xsl:stylesheet>
When applied to the following input XML
<content>
<para>text-1 <emphasis type="bold">text-2</emphasis> text-3</para>
</content>
The following is output
<content>
<p>text-1 <b>text-2</b> text-3</p>
</content>
You should be able to see how easy it is to extend to other cases should you input XML have more tags to transform.
do it like this ;)
<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:template match="content">
<content><xsl:apply-templates select="para" /></content>
</xsl:template>
<xsl:template match="emphasis [#type='bold']">
<b><xsl:value-of select="." /></b>
</xsl:template>
</xsl:stylesheet>
when you do it like this the default template will catch text-1 and text-3
I have XML like this:
<items>
<item>
<products>
<product>laptop</product>
<product>charger</product>
</products>
</item>
<item>
<products>
<product>laptop</product>
<product>headphones</product>
</products>
</item>
</items>
I want it to output like
laptop
charger
headphones
I was trying to use distinct-values() but I guess i m doing something wrong. Can anyone tell me how to achieve this using distinct-values()? Thanks.
<xsl:template match="/">
<xsl:for-each select="//products/product/text()">
<li>
<xsl:value-of select="distinct-values(.)"/>
</li>
</xsl:for-each>
</xsl:template>
but its giving me output like this:
<li>laptop</li>
<li>charger</li>
<li>laptop></li>
<li>headphones</li>
An XSLT 1.0 solution that uses key and the generate-id() function to get distinct values:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" encoding="UTF-8" indent="yes"/>
<xsl:key name="product" match="/items/item/products/product/text()" use="." />
<xsl:template match="/">
<xsl:for-each select="/items/item/products/product/text()[generate-id()
= generate-id(key('product',.)[1])]">
<li>
<xsl:value-of select="."/>
</li>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Here's an XSLT 1.0 solution that I've used in the past, I think it's more succinct (and readable) than using the generate-id() function.
<xsl:template match="/">
<ul>
<xsl:for-each select="//products/product[not(.=preceding::*)]">
<li>
<xsl:value-of select="."/>
</li>
</xsl:for-each>
</ul>
</xsl:template>
Returns:
<ul xmlns="http://www.w3.org/1999/xhtml">
<li>laptop</li>
<li>charger</li>
<li>headphones</li>
</ul>
You don't want "output (distinct-values)", but rather "for-each (distinct-values)":
<xsl:template match="/">
<xsl:for-each select="distinct-values(/items/item/products/product/text())">
<li>
<xsl:value-of select="."/>
</li>
</xsl:for-each>
</xsl:template>
I came to this problem while working with a Sitecore XSL rendering. Both the approach that used key() and the approach that used the preceding axis performed very slowly. I ended up using a method similar to key() but that did not require using key(). It performs very quickly.
<xsl:variable name="prods" select="items/item/products/product" />
<xsl:for-each select="$prods">
<xsl:if test="generate-id() = generate-id($prods[. = current()][1])">
<xsl:value-of select="." />
<br />
</xsl:if>
</xsl:for-each>
distinct-values(//product/text())
I found that you can do what you want with XSLT 1.0 without generate-id() and key() functions.
Here is Microsoft-specific solution (.NET's XslCompiledTransform class, or MSXSLT.exe or Microsoft platfocm COM-objects).
It is based on this answer. You can copy sorted node set to variable ($sorted-products in the stylesheet below), then convert it to node-set using ms:node-set function. Then you able for-each second time upon sorted node-set:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0" xmlns:ms="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="ms">
<xsl:output method="html" indent="yes" />
<xsl:template match="/">
<xsl:variable name="sorted-products">
<xsl:for-each select="//products/product">
<xsl:sort select="text()" />
<xsl:copy-of select=".|#*" />
</xsl:for-each>
</xsl:variable>
<xsl:variable name="products" select="ms:node-set($sorted-products)/product" />
<xsl:for-each select="$products">
<xsl:variable name='previous-position' select="position()-1" />
<xsl:if test="normalize-space($products[$previous-position]) != normalize-space(./text())">
<li>
<xsl:value-of select="./text()" />
</li>
</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
output:
<li>charger</li>
<li>headphones</li>
<li>laptop</li>
You can try it out in online playground.