How to pass as variable into .xslt 1.0 key expressions - xslt

Following on from my original question:
Filtering, Grouping, Counting and Selecting specific nodes in XML using XSLT 1.0
I've now run into another problem. Using this code that was kindly provided by michael.hor257k
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:key name="vehicle-by-series" match="Vehicle[Model='KA']" use="Series" />
<xsl:template match="/Dealer">
<xsl:for-each select="Vehicle[Model='KA'][count(. | key('vehicle-by-series', Series)[1]) = 1]">
<xsl:value-of select="Model"/>
<xsl:text>: </xsl:text>
<xsl:value-of select="Series"/>
<xsl:text>, </xsl:text>
<xsl:variable name="grp" select="key('vehicle-by-series', Series)" />
<xsl:value-of select="count($grp)"/>
<xsl:text> in stock, starting from </xsl:text>
<xsl:for-each select="$grp">
<xsl:sort select="Price" data-type="number" order="ascending"/>
<xsl:if test="position() = 1">
<xsl:value-of select="Price"/>
</xsl:if>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
I'm passing in the parameter model:
<xsl:param name="model" />
From an asp.NET URL variable.
I want to substitute the currently hardcoded KA for the model parameter. From the searching I've done you can't use parameters or variables in a key expression in XSLT 1.0. And I don't have the option to upgrade to 2.0. Mores the pity.
Would really appreciate some more help please.
Thanks in advance.

I would suggest you do:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:param name="model"/>
<xsl:key name="vehicle-by-series" match="Vehicle" use="concat(Model, '|', Series)" />
<xsl:template match="/Dealer">
<xsl:for-each select="Vehicle[Model=$model][count(. | key('vehicle-by-series', concat(Model, '|', Series))[1]) = 1]">
<xsl:value-of select="Model"/>
<xsl:text>: </xsl:text>
<xsl:value-of select="Series"/>
<xsl:text>, </xsl:text>
<xsl:variable name="grp" select="key('vehicle-by-series', concat(Model, '|', Series))" />
<xsl:value-of select="count($grp)"/>
<xsl:text> in stock, starting from </xsl:text>
<xsl:for-each select="$grp">
<xsl:sort select="Price" data-type="number" order="ascending"/>
<xsl:if test="position() = 1">
<xsl:value-of select="Price"/>
</xsl:if>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

Related

How can I convert all heading text to Title Case with XSLT?

I have a XML file containing a list of headings that I need to change to title case (words should begin with a capital letter except for most articles, conjunctions, and prepositions) with the help of XSLT.
Example:
"<h1>PERSONS OF THE DIALOGUE</h1>
in to
"<h1>Persons of the Dialogue</h1>
Please help...
Thanks
Here's a way you could do this:
<?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"
exclude-result-prefixes="xs"
version="2.0">
<xsl:output method="xml" indent="yes"/>
<xsl:variable name="words" select="'*a*,*an*,*the*,*and*,*but*,*for*,*nor*,*or*,*so*,*yet*,*as*,*at*,*by*,*if*,*in*,*of*,*on*,*to*,*with*,*when*,*where*'"/>
<xsl:template match="/">
<result>
<xsl:apply-templates select="document/h1"/>
</result>
</xsl:template>
<xsl:template match="h1">
<xsl:copy>
<xsl:variable name="elements" select="tokenize(lower-case(.), ' ')"/>
<xsl:for-each select="$elements">
<xsl:choose>
<!-- The first letter of the first word of a title is always Uppercase -->
<xsl:when test="position()=1">
<xsl:value-of select="upper-case(substring(.,1,1))"/>
<xsl:value-of select="substring(.,2)"/>
<xsl:if test="position()!=last()">
<xsl:text> </xsl:text>
</xsl:if>
</xsl:when>
<xsl:otherwise>
<xsl:choose>
<!-- If the word is contained in $words, leave it Lowercase -->
<xsl:when test="contains($words,concat('*',.,'*'))">
<xsl:value-of select="."/>
<xsl:if test="position()!=last()">
<xsl:text> </xsl:text>
</xsl:if>
</xsl:when>
<!-- If not, first letter is Uppercase -->
<xsl:otherwise>
<xsl:value-of select="upper-case(substring(.,1,1))"/>
<xsl:value-of select="substring(.,2)"/>
<xsl:if test="position()!=last()">
<xsl:text> </xsl:text>
</xsl:if>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
See it working here: https://xsltfiddle.liberty-development.net/93dFK9m/1

Unable to pass variable value in XSLT within a template

The below is my sgml file structure:
<em>
<pgblk revdate="20130901">
<task revdate='20140901'>
<p>random texts and few more inner tags</p>
</task>
<task revdate='20150901'>
<p>random texts and few more inner tags</p>
</task>
<task revdate='20160901'>
<p>random texts and few more inner tags</p>
</task>
</pgblk>
I have many tags with same name (task tag) and am trying to get the greatest revdate here. My XSLT is as follows:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output method="text" />
<xsl:template match="/pgblk">
<xsl:variable name="updatedrevdate">
<xsl:for-each select="task">
<xsl:sort select="#revdate" data-type="number" order="descending" />
<xsl:if test="position() = 1">
<xsl:value-of select="#revdate" />
</xsl:if>
</xsl:for-each>
</xsl:variable>
</xsl:template>
</xsl:stylesheet>
Now i am getting the greatest task tag value..but when i use the condition inside the same template to compare the greatest tag and the pageblock value
"Operations to PRINT THE GREATEST REVDATE"
The comparison is always getting the first revdate from task and comparing with pageblock revdate. It is not getting the greatest from the task.
Any help is much appreciated. Thanks.
Please check if the following XSLT code helps you make changes to your code. Need to change the template matching to
<xsl:template match="pgblk">
The solution does the comparison and outputs the dates.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output method="text" />
<xsl:strip-space elements="*"/>
<xsl:template match="pgblk">
<xsl:variable name="updatedrevdate">
<xsl:for-each select="task">
<xsl:sort select="#revdate" data-type="number" order="descending" />
<xsl:if test="position() = 1">
<xsl:value-of select="#revdate" />
</xsl:if>
</xsl:for-each>
</xsl:variable>
<!-- output the dates -->
<xsl:value-of select="concat('Page Block Date: ', #revdate)" />
<xsl:text>
</xsl:text>
<xsl:value-of select="concat('Max Task Date: ', $updatedrevdate)" />
<xsl:text>
</xsl:text>
<xsl:choose>
<xsl:when test="#revdate > $updatedrevdate">
<xsl:value-of select="concat('Max date after comparison: ', #revdate)" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="concat('Max date after comparison: ', $updatedrevdate)" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Output
Page Block Date: 20130901
Max Task Date: 20160901
Max date after comparison: 20160901

Combine space-separated attributes in XSLT

The transform I'm working on mergers two templates that has attributes that are space-separated.
An example would be:
<document template_id="1">
<header class="class1 class2" />
</document>
<document template_id="2">
<header class="class3 class4" />
</document>
And after the transform I want it to be like this:
<document>
<header class="class1 class2 class3 class4" />
</document>
How to achieve this?
I have tried (writing from memory):
<xsl:template match="/">
<header>
<xsl:attribute name="class">
<xsl:for-each select=".//header">
<xsl:value-of select="#class"/>
</xsl:for-each>
</xsl:attribute>
</header>
</xsl:template>
But that appends them all together, but I need them separated... and would be awesome if uniqued as well.
Thank you
Try this template, which simply adds a space before all the headers, apart from the first
<xsl:template match="/">
<header>
<xsl:attribute name="class">
<xsl:for-each select=".//header">
<xsl:if test="position() > 1">
<xsl:text> </xsl:text>
</xsl:if>
<xsl:value-of select="#class"/>
</xsl:for-each>
</xsl:attribute>
</header>
</xsl:template>
If you want your classes without repetitions, then use the following XSLT:
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:transform 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" encoding="UTF-8" indent="yes" />
<xsl:template match="/">
<document>
<xsl:variable name="cls1">
<xsl:for-each select=".//header/#class">
<xsl:call-template name="tokenize">
<xsl:with-param name="txt" select="."/>
</xsl:call-template>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="cls" select="exsl:node-set($cls1)"/>
<header>
<xsl:attribute name="class">
<xsl:for-each select="$cls/*[not(preceding-sibling::* = .)]">
<xsl:value-of select="."/>
<xsl:if test="position() < last()">
<xsl:text> </xsl:text>
</xsl:if>
</xsl:for-each>
</xsl:attribute>
</header>
</document>
</xsl:template>
<xsl:template name="tokenize">
<xsl:param name="txt"/>
<xsl:if test="$txt">
<xsl:if test="contains($txt, ' ')">
<cls>
<xsl:value-of select="substring-before($txt, ' ')"/>
</cls>
<xsl:call-template name="tokenize">
<xsl:with-param name="txt" select="substring-after($txt, ' ')"/>
</xsl:call-template>
</xsl:if>
<xsl:if test="not(contains($txt, ' '))">
<cls>
<xsl:value-of select="$txt"/>
</cls>
</xsl:if>
</xsl:if>
</xsl:template>
</xsl:transform>
In XSLT 1.0 it is much more difficult than in XSLT 2.0, but as you see,
it is possible.
Edit
A revised version using Muenchian grouping (inspired by comment from Michael):
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:transform 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" encoding="UTF-8" indent="yes" />
<xsl:variable name="cls1">
<xsl:for-each select=".//header/#class">
<xsl:call-template name="tokenize">
<xsl:with-param name="txt" select="."/>
</xsl:call-template>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="cls2" select="exsl:node-set($cls1)"/>
<xsl:key name="classKey" match="cls" use="."/>
<xsl:template match="/">
<document>
<header>
<xsl:attribute name="class">
<xsl:for-each select="$cls2/*[generate-id() = generate-id(key('classKey', .)[1])]">
<xsl:value-of select="."/>
<xsl:if test="position() < last()">
<xsl:text> </xsl:text>
</xsl:if>
</xsl:for-each>
</xsl:attribute>
</header>
</document>
</xsl:template>
<xsl:template name="tokenize">
<xsl:param name="txt"/>
<xsl:if test="$txt">
<xsl:if test="contains($txt, ' ')">
<cls>
<xsl:value-of select="substring-before($txt, ' ')"/>
</cls>
<xsl:call-template name="tokenize">
<xsl:with-param name="txt" select="substring-after($txt, ' ')"/>
</xsl:call-template>
</xsl:if>
<xsl:if test="not(contains($txt, ' '))">
<cls>
<xsl:value-of select="$txt"/>
</cls>
</xsl:if>
</xsl:if>
</xsl:template>
</xsl:transform>

How do a for-each in this xsl?

I get this result from a service in a Bpel process.
How I do a For-each with OSTypeOutput and inside of it
test the value of "serv"?
I'am trying to do a transformation.
<siOutPut>
-<OOPreOutput xmlns="http://my.web.com/test/types">
-<OSTypes xmlns:ns1="http://my.web.com/test/servtp">
-<ns1:OSTypeOutput>
<ns1:serv>A1</ns1:serv>
<ns1:serv>A2</ns1:serv>
<ns1:serv>A3</ns1:serv>
</ns1:OSTypeOutput>
</OSTypes>
</OOPreOutput>
</siOutPut>
I have tried this way but I can't get nothing:
<xsl:param name="Param">
<xsl:text disable-output-escaping="no">A3</xsl:text>
</xsl:param>
<xsl:template match="/">
<ns1:GetTestResponse>
<ns1:MyId>
<xsl:for-each select="/ns1:OOPreOutput/ns1:OSTypes/ns1:OSTypeOutput">
<xsl:if test="(./serv = $Param)">
<xsl:value-of select="'Found'"/>
</xsl:if>
</xsl:for-each>
</ns1:MyId>
</ns1:GetTestResponse>
</xsl:template>
I've made this test:
<ns1:MyId>
<xsl:for-each select="/ns1:OOPreOutput/ns1:OSTypes">
<xsl:value-of select="current()"/>
</xsl:for-each>
</ns1:MyId>
And get this result :
A1A2A3
I also did this test but without result :
<xsl:param name="Param">
<xsl:text disable-output-escaping="no">A3</xsl:text>
</xsl:param>
<xsl:template match="/">
<ns1:GetTestResponse>
<ns1:MyId>
<xsl:if test="/ns1:OOPreOutput/ns1:OSTypes/ns1:OSTypeOutput/ns1:serv = $Param">
<xsl:value-of select="'Found'"/>
</xsl:if>
</ns1:MyId>
</ns1:GetTestResponse>
</xsl:template>
I changed a little bit the output:
-<siOutPut>
-<OOPreOutput xmlns="http://int.clear.com/types">
+<Deliv></Deliv>
<OSTypes>
<ns1:serv xmlns:ns1="http://my.test.com/web/services">A1</ns1:serv>
<ns1:serv xmlns:ns1="http://my.test.com/web/services">A2</ns1:serv>
<ns1:serv xmlns:ns1="http://my.test.com/web/services">A3</ns1:serv>
</OSTypes>
</OOPreOutput>
</siOutPut>
How I should verify if ns1:serv has "A3" value?
You don't have one of the namespaces declared in the stylesheet. Re-writing the stylesheet:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:ns1="http://my.web.com/test/servtp" xmlns:ns2="http://my.web.com/test/types" version="1.0">
<xsl:param name="Param">
<xsl:text disable-output-escaping="no">A3</xsl:text>
</xsl:param>
<xsl:template match="/">
<ns1:GetTestResponse>
<ns1:MyId>
<xsl:for-each select="/siOutPut/ns2:OOPreOutput/ns2:OSTypes/ns1:OSTypeOutput">
<xsl:if test="(./ns1:serv = $Param)">
<xsl:value-of select="'Found'"/>
</xsl:if>
</xsl:for-each>
</ns1:MyId>
</ns1:GetTestResponse>
</xsl:template>
</xsl:stylesheet>
But, there is no need for doing a for-each as your just want to check the existence of "serv" with some value.
It can be done this way too:
<xsl:template match="/">
<ns1:GetTestResponse>
<ns1:MyId>
<xsl:if test="siOutPut/ns2:OOPreOutput/ns2:OSTypes/ns1:OSTypeOutput/ns1:serv = $Param">
<xsl:value-of select="'Found'"/>
</xsl:if>
</ns1:MyId>
</ns1:GetTestResponse>
</xsl:template>
I'd try to simplify the logic by using apply templates. I think the problem may be < xsl:value-of select="'Found'"/>. I'd say it's < xsl:text>'Found'< /xsl:text>. And I think you didn't match < siOutPut>. I don't think < xsl:template match="/"> solves it.
<xsl:param name="Param">
<xsl:text disable-output-escaping="no">A3</xsl:text>
</xsl:param>
<xsl:template match="/">
<ns1:GetTestResponse>
<ns1:MyId>
<xsl:apply-templates />
</ns1:MyId>
</ns1:GetTestResponse>
</xsl:template>
<xsl:template match="ns1:OSTypeOutput">
<xsl:for-each select="ns1:serv">
<xsl:if test=".= $Param">
<xsl:text>'Found'</xsl:text>
</xsl:if>
</xsl:for-each>
</xsl:template>
After changing the output I made this for-each and it Worked:
<xsl:for-each select="/ns2:OOPreOutput/ns2:OSTypes">
<xsl:if test="(./ns1:serv = $Param)">
<xsl:value-of select="'Found'"/>
</xsl:if>
</xsl:for-each>
And also doing the Direct test as suggested Lingamurthy
<xsl:if test="ns2:OOPreOutput/ns2:OSTypes/ns1:serv = $Param">
<xsl:value-of select="'Found'"/>
<xsl:if>
Thank you all.

How to apply a function to a sequence of nodes in XSLT

I need to write an XSLT function that transforms a sequence of nodes into a sequence of strings. What I need to do is to apply a function to all the nodes in the sequence and return a sequence as long as the original one.
This is the input document
<article id="4">
<author ref="#Guy1"/>
<author ref="#Guy2"/>
</article>
This is how the calling site:
<xsl:template match="article">
<xsl:text>Author for </xsl:text>
<xsl:value-of select="#id"/>
<xsl:variable name="names" select="func:author-names(.)"/>
<xsl:value-of select="string-join($names, ' and ')"/>
<xsl:value-of select="count($names)"/>
</xsl:function>
And this is the code of the function:
<xsl:function name="func:authors-names">
<xsl:param name="article"/>
<!-- HELP: this is where I call `func:format-name` on
each `$article/author` element -->
</xsl:function>
What should I use inside func:author-names? I tried using xsl:for-each but the result is a single node, not a sequence.
<xsl:sequence select="$article/author/func:format-name(.)"/> is one way, the other is <xsl:sequence select="for $a in $article/author return func:format-name($a)"/>.
I am not sure you would need the function of course, doing
<xsl:value-of select="author/func:format-name(.)" separator=" and "/>
in the template of article should do.
If only a sequence of #ref values should be generated there is no need for a function or xsl version 2.0.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html" />
<xsl:template match="article">
<xsl:apply-templates select="author" />
</xsl:template>
<xsl:template match="author">
<xsl:value-of select="#ref"/>
<xsl:if test="position() !=last()" >
<xsl:text>,</xsl:text>
</xsl:if>
</xsl:template>
</xsl:styleshee
This will generate:
#Guy1,#Guy2
Update:
Do have the string join by and and have a count of items. Try this:
<xsl:template match="article">
<xsl:text>Author for </xsl:text>
<xsl:value-of select="#id"/>
<xsl:apply-templates select="author" />
<xsl:value-of select="count(authr[#ref])"/>
</xsl:template>
<xsl:template match="author">
<xsl:value-of select="#ref"/>
<xsl:if test="position() !=last()" >
<xsl:text> and </xsl:text>
</xsl:if>
</xsl:template>
With this output:
Author for 4#Guy1 and #Guy20