xpath with xsl:when test - html

I need of display BIG on planets with diameter higher than average diameter or SMALL without using avg function (XSLT 1.0)
I have tried to use xsl:when with a condition like diameter > sum(....) div count(nom), but it doesn't work :
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html"/>
<xsl:template match="/systeme_solaire">
<html lang="fr">
<head>
<title>Les planètes</title>
</head>
<body>
<xsl:apply-templates select="planete[nom!='Terre']">
<xsl:sort select ="diametre" order="descending" data-type="number" />
</xsl:apply-templates>
</body>
</html>
</xsl:template>
<xsl:template match="planete" >
<ul>
<p><b><xsl:value-of select="nom"/> : </b></p>
<li>Distance au soleil: <xsl:value-of select="distance"/><xsl:value-of select="distance/#unit"/></li>
<li>Masse: <xsl:value-of select="masse"/> <xsl:value-of select="masse/#unit"/></li>
<li>
<xsl:choose>
<xsl:when test="diametre > ((sum(diametre[unit='diamètre terrestre']*sum(diametre[unit='km']))+sum(diametre[unit='km'])) div count(nom))">
BIG
</xsl:when>
<xsl:otherwise>
SMALL
</xsl:otherwise>
</xsl:choose> Diamètre: <xsl:value-of select="diametre"/> <xsl:value-of select="diametre/#unit"/></li>
<xsl:if test="satellite>0"><li>Nombre de satellites: <xsl:value-of select="satellite"/></li></xsl:if>
</ul>
</xsl:template>
</xsl:stylesheet>
XML file used (diameter of planets differents from earth are defined according to earth diameter ratio) :
<?xml version="1.0" encoding="ISO-8859-1" ?>
<systeme_solaire>
<planete type="tellurique">
<nom>Vénus</nom>
<distance unit="UA" >0.7</distance>
<masse unit="masse terrestre">0.8</masse>
<diametre unit="diamètre terrestre">0.9</diametre>
</planete>
<planete type="tellurique">
<nom>Terre</nom>
<distance unit="km" >149600000</distance>
<masse unit="kg">5.98e24</masse>
<diametre unit="km">12756</diametre>
<satellite>1</satellite>
</planete>
<planete type="tellurique">
<nom>Mars</nom>
<distance unit="UA" >1.5</distance>
<masse unit="masse terrestre">0.1</masse>
<diametre unit="diamètre terrestre">0.5</diametre>
<satellite>2</satellite>
</planete>
</systeme_solaire>

You have two problems :
First you need to calculate you average diameter outside the
template planete in order to reach all planets then pass this
average to you template
Then you xpath is incorrect : wrong parenthesis, unit is an attribute so you need to use #. You need something like this :
((sum(//diametre[#unit='diamètre
terrestre'])*//diametre[#unit='km'])+//diametre[#unit='km']) div
count(//nom)
Edit : You also need to calculate the actual diameter of your current planet base on earth diameter, you can do this by adding another parameter <xsl:with-param name="terre" select="//diametre[#unit='km']"/> and using it <xsl:when test="diametre*$terre > $avg">
I've updated you XSLT like so :
Solution 1
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html"/>
<xsl:template match="/systeme_solaire">
<html lang="fr">
<head>
<title>Les planètes</title>
</head>
<body>
<xsl:apply-templates select="planete[nom!='Terre']">
<xsl:sort select ="diametre" order="descending" data-type="number" />
<xsl:with-param name="avg" select="((sum(//diametre[#unit='diamètre terrestre'])*//diametre[#unit='km'])+//diametre[#unit='km']) div count(//nom)"/>
<xsl:with-param name="terre" select="//diametre[#unit='km']"/>
</xsl:apply-templates>
</body>
</html>
</xsl:template>
<xsl:template match="planete" >
<xsl:param name="avg"/>
<xsl:param name="terre"/>
<ul>
<p><b><xsl:value-of select="nom"/> : </b></p>
<li>Distance au soleil: <xsl:value-of select="distance"/><xsl:value-of select="distance/#unit"/></li>
<li>Masse: <xsl:value-of select="masse"/> <xsl:value-of select="masse/#unit"/></li>
<li>
<xsl:choose>
<xsl:when test="diametre*$terre > $avg">
BIG
</xsl:when>
<xsl:otherwise>
SMALL
</xsl:otherwise>
</xsl:choose> Diamètre: <xsl:value-of select="diametre"/> <xsl:value-of select="diametre/#unit"/></li>
<xsl:if test="satellite>0"><li>Nombre de satellites: <xsl:value-of select="satellite"/></li></xsl:if>
</ul>
</xsl:template>
</xsl:stylesheet>
Edit to add michael suggestion (calculate the average diameter as the ratio) :
Solution 2
<xsl:apply-templates select="planete[nom!='Terre']">
<xsl:sort select ="diametre" order="descending" data-type="number" />
<xsl:with-param name="avg" select="(sum(//diametre[#unit='diamètre terrestre'])+1) div count(//nom)"/>
</xsl:apply-templates>
...
<xsl:template match="planete" >
<xsl:param name="avg"/>
...
<xsl:when test="diametre > $avg">
...

Related

Handle excluded element in a foreach

In first template, I am intentionally excluding an element ('milk') because the parsed data map is relatively flat and I would like to use XSLT to categorize and structure the data. The aim is to process the excluded element ('milk') in the second template. The both templates works running them one at a time. Running the templates together will not show the result of the excluded element ('milk') which should set another attribute name and attribute value.
JSON:
<data>
{
"storage": {
"pencils": 12,
"milk": 8,
"rulers": 4
}
}
</data>
XSL:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:transform
version="3.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:storage="http://www.exammple.com/1"
xmlns:office="http://www.exammple.com/2"
xmlns:item="http://www.exammple.com/3"
expand-text="yes">
<xsl:output method="xml" indent="yes"/>
<xsl:mode on-no-match="shallow-skip"/>
<!-- Parse JSON to XML -->
<xsl:template match="data">
<storage:one>
<xsl:apply-templates select="json-to-xml(.)"/>
</storage:one>
</xsl:template>
<!-- Print map -->
<!-- <xsl:template match="*[#key = 'storage']"> <xsl:copy-of select=".."/> </xsl:template> -->
<xsl:template match="*[#key='storage']">
<xsl:for-each select="*[not(#key='milk')]">
<xsl:element name="item:{#key}">
<xsl:attribute name="office">plant-1</xsl:attribute>
<xsl:value-of select="text()"/>
</xsl:element>
</xsl:for-each>
</xsl:template>
<xsl:template match="*[#key='milk']">
<xsl:for-each select=".">
<xsl:element name="item:{#key}">
<xsl:attribute name="beverage">plant-2</xsl:attribute>
<xsl:value-of select="text()"/>
</xsl:element>
</xsl:for-each>
</xsl:template>
</xsl:transform>
Result:
<?xml version="1.0" encoding="UTF-8"?>
<storage:one xmlns:item="http://www.exammple.com/3"
xmlns:office="http://www.exammple.com/2"
xmlns:storage="http://www.exammple.com/1">
<item:pencils office="plant-1">12</item:pencils>
<item:rulers office="plant-1">4</item:rulers>
</storage:one>
Wanted result:
<?xml version="1.0" encoding="UTF-8"?>
<storage:one xmlns:item="http://www.exammple.com/3"
xmlns:office="http://www.exammple.com/2"
xmlns:storage="http://www.exammple.com/1">
<item:pencils office="plant-1">12</item:pencils>
<item:rulers office="plant-1">4</item:rulers>
<item:milk beverage="plant-2">8</item:milk>
</storage:one>
Your second template is never matched, because it is never reached. All elements are processed by <xsl:template match="*[#key='storage']"> - which doesn't have an <xsl:apply-templates ...> to reach further templates.
Your first template does not recurse into its children. So add an <xsl:apply-templates select="*" /> to the end of the first template:
<xsl:template match="*[#key='storage']">
<xsl:for-each select="*[not(#key='milk')]">
<xsl:element name="item:{#key}">
<xsl:attribute name="office">plant-1</xsl:attribute>
<xsl:value-of select="text()"/>
</xsl:element>
</xsl:for-each>
<xsl:apply-templates select="*" />
</xsl:template>
This will try to apply further templates at the level of "storage" and therefore match the second template.
I would write templates for each different output type and if the order of the output is different from the order of the input throw in an xsl:sort or an XPath 3.1 sort call to change the order:
<xsl:template match="data">
<storage:one>
<xsl:apply-templates select="json-to-xml(.)"/>
</storage:one>
</xsl:template>
<xsl:template match="*[#key = 'storage']">
<xsl:apply-templates select="sort(*, (), function($el) { $el/#key = 'milk' })"/>
</xsl:template>
<xsl:template match="*[#key='storage']/*[not(#key='milk')]">
<xsl:element name="item:{#key}">
<xsl:attribute name="office">plant-1</xsl:attribute>
<xsl:value-of select="text()"/>
</xsl:element>
</xsl:template>
<xsl:template match="*[#key='storage']/*[#key='milk']">
<xsl:element name="item:{#key}">
<xsl:attribute name="beverage">plant-2</xsl:attribute>
<xsl:value-of select="text()"/>
</xsl:element>
</xsl:template>

structure, under certain conditions

I'm trying my hand at html but I don't know how to do it. That's my but doesnt work:(:
<xsl:template match="*[contains(local-name(), '.')]">
<xsl:element name="{translate(local-name(), '.', '_')}" namespace="{namespace-uri()}">
<xsl:apply-templates select="#* | node()"/>
</xsl:element>
</xsl:template>
Does anyone have an idea how to deal with it?
It seems a recursive grouping problem though the single example doesn't really spell out when to wrap and/or nest items as lists; nevertheless with XSLT 2 or 3 it could be tackled with a recursive function using for-each-group:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:mf="http://example.com/mf"
exclude-result-prefixes="#all"
version="3.0">
<xsl:mode on-no-match="shallow-copy"/>
<xsl:output method="html" indent="yes" html-version="5"/>
<xsl:function name="mf:wrap" as="element()*">
<xsl:param name="elements" as="element()*"/>
<xsl:param name="level" as="xs:integer"/>
<xsl:for-each-group select="$elements" group-adjacent="boolean(self::*[matches(local-name(), '^li[' || $level || '-9]+$')])">
<xsl:choose>
<xsl:when test="current-grouping-key()">
<ul class="li{$level}">
<xsl:sequence select="mf:wrap(current-group(), $level + 1)"/>
</ul>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="current-group()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</xsl:function>
<xsl:template match="*[matches(local-name(), '^li[0-9]+$')]">
<li>
<xsl:apply-templates/>
</li>
</xsl:template>
<xsl:template match="*[*[matches(local-name(), '^li[0-9]+$')]]">
<div>
<xsl:apply-templates select="mf:wrap(*, 1)"/>
</div>
</xsl:template>
<xsl:template match="uz">
<h5>
<xsl:apply-templates/>
</h5>
</xsl:template>
<xsl:template match="/">
<html>
<head>
<title>.NET XSLT Fiddle Example</title>
</head>
<body>
<xsl:apply-templates/>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
https://xsltfiddle.liberty-development.net/bnnZWK
Note that the list structure the above creates is a bit different, all liX items of the same X level are wrapped into a single ul class="liX" wrapper while your wanted sample at some places seems to wrap several items and at other places wrap only single items.

XSLT - How to add new tag each X characters

I want to add a new tag <br\> to my XML each 10 characters. (without including inner nodes)
For example if my input is:
<main>
01234567890123
<a>
link
</a>
45678901234567901234
</main>
I expect is to be somthing like:
<main>
0123456789<br\>0123
<a>
link
</a>
456789<br\>012345679<br\>01234
</main>
I wrote this function:
<!-- Add new "HTML Break" Every X chars -->
<xsl:template name="Add-HTML-Break-After-X-Chars">
<xsl:param name="text" />
<xsl:param name="max-length" />
<xsl:choose>
<xsl:when test="string-length($text) > $max-length">
<!-- Show the X length of the text -->
<xsl:copy-of select="substring($text,1,$max-length)" />
<!-- Adding the special tag -->
<br/>
<!-- continue to add the rest of the text -->
<xsl:call-template name="Add-HTML-Break-After-X-Chars">
<xsl:with-param name="text" select="substring($text, $max-length + 1)" />
<xsl:with-param name="max-length" select="$max-length" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<!-- Show the text (length is less then X) -->
<xsl:copy-of select="$text" />
</xsl:otherwise>
</xsl:choose>
The problem is that I'm getting the output without the inner tags:
<main>
0123456789<br\>0123
link
45<br\>6789012345<br\>67901234
</main>
I'm using this code to call the template:
<xsl:call-template name="Add-HTML-Break-After-X-Chars">
<xsl:with-param name="text" select="main" />
<xsl:with-param name="max-length" select="10" />
</xsl:call-template>
I tried also "value-of" instead of "copy-of".
Is there a way I can keep the tags?
If so, how can I keep them and make a "safe" insert of the new tag?
This is difficult to do in XSLT, because it processes each text node individually, with no information being carried over from the preceding text nodes.
Here's a possible approach:
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="*"/>
<!-- identity transform -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="main/text()" name="wrap">
<xsl:param name="text" select="."/>
<xsl:param name="line-length" select="10"/>
<xsl:param name="carry">
<xsl:variable name="lengths">
<xsl:for-each select="preceding-sibling::text()">
<length>
<xsl:value-of select="string-length()"/>
</length>
</xsl:for-each>
</xsl:variable>
<xsl:value-of select="sum(exsl:node-set($lengths)/length) mod $line-length"/>
</xsl:param>
<xsl:value-of select="substring($text, 1, $line-length - $carry)"/>
<br/>
<xsl:if test="$carry + string-length($text) > $line-length">
<!-- recursive call -->
<xsl:call-template name="wrap">
<xsl:with-param name="text" select="substring($text, $line-length - $carry + 1)"/>
<xsl:with-param name="carry" select="0"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
When this is applied to the following test input:
XML
<main>A123456789B123<a>link1</a>456789C123456789D12345678<a>link2</a>9E123456789F1234567</main>
the result will be:
<?xml version="1.0" encoding="UTF-8"?>
<main>A123456789<br/>B123<a>link1</a>456789<br/>C123456789<br/>D12345678<a>link2</a>9<br/>E123456789<br/>F1234567</main>

How this style sheet can produce HTML output

How to complete the following style sheet that should produce HTML output in figure 1.
I have styled some part of it but could not make it exactly the same as in figure 1. I have tried "copy element" in XSL but gave me duplicate results.
This is my XML:
<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="research.xsl"?>
<ResearchGroups xmlns="http://www.sam.com/sam.html">
<Group name="Intelligent Systems Group" id="ISG"> The Intelligent
Systems Group pursues internationally-leading research in a wide
range of intelligent systems. </Group>
<Group name="Robotics" id="RBT"> The Essex robotics group is one of
the largest mobile robotics groups in the UK. </Group>
<Staff name="Callaghan, Vic" title="Professor" groups="ISG RBT">
Intelligent environments and robotics. </Staff>
<Staff name="Gu, Dongbing" title="Dr" groups="RBT"> Multi-agent
and distributed control systems. </Staff>
</ResearchGroups>
My XSLT:
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:rg="http://www.sam.com/sam.html"
xmlns="http://wwww.w3.org/1999/xhtml"
version="2.0"> <xsl:template match="/">
<html>
<head> <title>Research Groups</title> </head>
<body> <xsl:apply-templates select="//rg:Group"/> </body>
</html> </xsl:template> <xsl:template match="rg:Group">
<xsl:variable name="ID" select="#id"/>
<h3> <a name="{$ID}"> <xsl:value-of select="#name"/> </a> </h3>
<p> <xsl:value-of select="text()"/> </p>
</xsl:template>
</xsl:stylesheet>
HTML Figure 1
the XSLT style sheet should output the following HTML
Something like this I think:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:rg="http://www.sam.com/sam.html" xmlns="http://wwww.w3.org/1999/xhtml" version="2.0">
<xsl:template match="/">
<html>
<head>
<title>Research Groups</title>
</head>
<body>
<xsl:apply-templates select="//rg:Group"/>
</body>
</html>
</xsl:template>
<xsl:template match="rg:Group">
<xsl:variable name="ID" select="#id"/>
<h3>
<a name="{$ID}">
<xsl:value-of select="#name"/>
</a>
</h3>
<p>
<xsl:value-of select="text()"/>
</p>
<ul>
<xsl:apply-templates select="//rg:Staff[contains(#groups, current()/#id)]">
<xsl:with-param name="curGroup"><xsl:value-of select="#id"/></xsl:with-param>
</xsl:apply-templates>
</ul>
</xsl:template>
<xsl:template match="rg:Staff">
<xsl:param name="curGroup"/>
<li>
<xsl:value-of select="#name"/>
<xsl:text>: </xsl:text>
<xsl:value-of select="text()"/>
<xsl:if test="//rg:Group[(#id != $curGroup) and contains(current()/#groups, #id)]">
<xsl:text> ( </xsl:text>
<xsl:apply-templates select="//rg:Group[(#id != $curGroup) and contains(current()/#groups, #id)]" mode="otherGroups"/>
<xsl:text> ) </xsl:text>
</xsl:if>
</li>
</xsl:template>
<xsl:template match="rg:Group" mode="otherGroups">
<a href="#{#id}">
<xsl:value-of select="#id"/>
</a>
</xsl:template>
</xsl:stylesheet>

Formatting XSLT document to display multiple nicknames on a single line

My xml documents contain a list of people, and these people can have 0 or more nicknames. I am having trouble trying to display all the nicknames properly in my xslt document.
I can have all the nicknames listed by using:
<xsl:for-each select="name/nickname">
Nickname: <xsl:value-of select="." />
</xsl:for-each>
The output of this is something like:
Nickname: nickname1
Nickname: nickname2
Which is a problem as I would like to get an output without Nickname: being listed so many times, i.e.
Nickname: nickname1, nickname2.
What I currently have is:
<p>
Nickname:
<xsl:for-each select="name/nickname">
<xsl:value-of select="." />,
</xsl:for-each>
</p>
Problems with this are:
Nickname will always be printed at least once even if a nickname doesn't exist.
There will always be a left over comma (,).
I am hoping there are suggestions to get around these two issues, I tried to use != "" but I'm not sure if this is allowed if an person doesn't contain a nickname.
Thanks :)
This transformation:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:template match="name[nickname]">
<xsl:text>
Nicknames: </xsl:text>
<xsl:apply-templates select="nickname"/>
</xsl:template>
<xsl:template match="nickname">
<xsl:if test="not(position() = 1)">
<xsl:text>, </xsl:text>
</xsl:if>
<xsl:apply-templates/>
</xsl:template>
</xsl:stylesheet>
when applied on this XML document:
<t>
<person>
<name trueName="John">
<nickname>X1</nickname>
<nickname>X2</nickname>
<nickname>X3</nickname>
</name>
</person>
<person>
<name trueName="Peter">
<nickname>Y1</nickname>
<nickname>Y2</nickname>
<nickname>Y3</nickname>
</name>
</person>
</t>
produces the wanted, correct result:
Nicknames: X1, X2, X3
Nicknames: Y1, Y2, Y3
Or,
<xsl:for-each select="name/nickname">
<xsl:if test="position() = 1">Nickname: </xsl:if>
<xsl:value-of select="." />
<xsl:if test="not(position()=last())">, </xsl:if>
</xsl:for-each>
Something like that (untested):
<xsl:when test="name/nickname">
Nickname:
<xsl:for-each select="name/nickname">
<xsl:value-of select="." />
<xsl:if test="count(following-sibling::nickname)">,</xsl:if>
</xsl:for-each>
</xsl:when>
Input :
<?xml version="1.0" encoding="UTF-8"?>
<test>
<nickname>1</nickname>
<nickname>2</nickname>
<nickname>3</nickname>
</test>
Transform :
<xsl:template match='/'>
<xsl:if test='count(//nickname) > 0'>
<result>
<xsl:for-each select='//nickname'>
<xsl:choose>
<xsl:when test='position() = 1'>
Nickname : <xsl:value-of select="."/><xsl:if test="not(position() = last())">,</xsl:if>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="."/>
<xsl:if test="not(position() = last())">,</xsl:if>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</result>
</xsl:if>
</xsl:template>
Output :
<?xml version="1.0" encoding="UTF-8"?>
<result>Nickname : 1,2,3</result>
Or in XSLT 2.0:
Input :
<?xml version="1.0" encoding="UTF-8"?>
<test>
<nickname>1</nickname>
<nickname>2</nickname>
<nickname>3</nickname>
</test>
Transform :
<xsl:template match='/'>
Nickname: <xsl:value-of select="/test/nickname" separator=", "/>
</xsl:template>