XSLT Concatenate content of consecutive nodes - xslt

I’m new to XSLT and work on this once a year only. Here a really Basic XSLT question I can’t figure out.
I need to concatenate all the same nodes into a single text output separated by a space. Any help appreciated.
Sample of XML
<?xml version="1.0" encoding="UTF-8"?>
<root>
<properties>
<plus>Agile en conduite tout-terrain et dans la neige.</plus>
<plus>Habitacle logeable et commandes logiques.</plus>
<plus>Bonne visibilité.</plus>
<minus>Retrait du constructeur du marché canadien en 2014.</minus>
<minus>Roulement assez sec.</minus>
</properties>
</root>

Well, it's dead easy in XSLT 2.0: string-join(properties/*, ' ').
So I guess you're not using XSLT 2.0, or you wouldn't be asking. And if you're only working on it once a year, it's probably easier for you to put up with the limitations of 1.0 than to upgrade. The 1.0 solution looks something like
<xsl:for-each select="properties/*">
<xsl:if test="position() != 1"><xsl:text> </xsl:text></xsl:if>
<xsl:value-of select="."/>
</xsl:for-each>

I figured it out... Since my output was text Was able to get it with.
<xsl:for-each select="plus">
<xsl:value-of select="."/><xsl:text>
</xsl:text>

Related

Using an xsl param as argument to XPath function

I've been trying to figure out a way to use a param/variable as an argument to a function.
At the very least, I'd like to be able to use basic string parameters as arguments as follows:
<xsl:param name="stringValue" default="'abcdef'"/>
<xsl:value-of select="substring(string($stringValue),1,3)"/>
The above code generates no output.
I feel like I'm missing a simple way of doing this. I'm happy to use exslt or some other extension if an xslt 1.0 processor does not allow this.
Edit:
I am using XSL 1.0 and transforming using Nokogiri, which supports XPATH 1.0 . Here is a more complete snippet of what I am trying to do:
I want to pass column numbers as parameters using nokogiri as follows
document = Nokogiri::XML(File.read('table.xml'))
template = Nokogiri::XSLT(File.read('extractTableData.xsl'))
transformed_document = template.transform(document,
["tableName","'Problems'", #Table Heading
"tablePath","'Table'", #Takes an absolute XPATH String
"nameColumnIndex","2", #column number
"valueColumnIndex","3"]) #column number
File.open('FormattedOutput.xml', 'w').write(transformed_document)
My xsl then wants to access every TD[valueColumnIndex] and and retrieve the first 3 characters at that position, which is why I am using a substring function. So I want to do something like:
<xsl:value-of select="substring(string(TD[$valueColumnIndex]),1,3)"/>
Since I was unable to do that, I tried to extract TD[$valueColumnIndex] to another param valueCode and then do substring(string(valueCode),1,3)
That did not work either (which is to say, no text was output, whereas <xsl:value-of select="$valueCode"/> gave me the expected output).
As a result, i decided to understand how to use parameters better, I would just use a hard coded string, as mentioned in my earlier question.
Things I have tried:
using single quotes around abcdef (and not) while
using string() around the param name (and not)
Based on the comments below, it seems I am handicapped in my ability to understand the error because Nokogiri does not report an error for these situations. I am in the process of installing xsltproc right now and seeing if I receive any errors.
Finally, here is my entire xsl. I use a separate template forLoop because of the valueCode param I am creating. The lines of interest are the last 5 or so. I cannot include the xml as there are data use issues involved.
<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ext="http://exslt.org/common"
xmlns:dyn="http://exslt.org/dynamic"
exclude-result-prefixes="ext dyn">
<xsl:param name="tableName" />
<xsl:param name="tablePath" />
<xsl:param name= "nameColumnIndex" />
<xsl:param name= "valueColumnIndex"/>
<xsl:template match="/">
<xsl:param name="tableRowPath">
<xsl:value-of select="$tablePath"/><xsl:text>/TR</xsl:text>
</xsl:param>
<!-- Problems -->
<section>
<name>
<xsl:value-of select="$tableName" />
</name>
<!-- <xsl:for-each select="concat($tablePath,'/TR')"> -->
<xsl:for-each select="dyn:evaluate($tableRowPath)">
<!-- Encode record section -->
<xsl:call-template name="forLoop"/>
</xsl:for-each>
</section>
</xsl:template>
<xsl:template name="forLoop">
<xsl:param name="valueCode">
<xsl:value-of select="./TD[number($valueColumnIndex)][text()]"/>
</xsl:param>
<xsl:param name="RandomString" select="'Try123'"/>
<section>
<name>
<xsl:value-of select="./TD[number($nameColumnIndex)]"/>
</name>
<code>
<short>
<xsl:value-of select="substring(string($valueCode),1,3)"/>
</short>
<long>
<xsl:value-of select="$valueCode"/>
</long>
</code>
</section>
</xsl:template>
</xsl:stylesheet>
Use it this way:
<xsl:param name="stringValue" select="'abcdef'"/>
<xsl:value-of select="substring($stringValue,1,3)"/>

How to get CDATA value from XML in XSL

I need to transform the XML inside the CDATA of the XML using the XSLT.
Input:
<pre>
<![CDATA[<p><strong>Guidance</strong> about simplifying medication in <em>patients<em> with end-stage CHF who appear to be imminently dying.</p>]]>
</pre>
Output:
<ce:section-title>Pre</ce:section-title>
<ce:para><ce:bold>Guidance</ce:bold> about simplifying medication in <ce:italic>patients</ce:italic> with <ce:inter-ref xlink:type="simple" xlink:href="/formulary/en/drug-treatment-in-the-imminently-dying.html#heart-failure">end-stage CHF who appear to be imminently dying</ce:inter-ref>.</ce:para>
Could you please suggest me on this. Thanks in advance.
Which XSLT processor and which version of XSLT can you use? With the commercial versions of Saxon 9 you can use XSLT 3.0 and
<xsl:template match="pre">
<ce:section-title>Pre</ce:section-title>
<xsl:apply-templates select="parse-xml(.)"/>
</xsl:template>
<xsl:template match="p">
<ce:para>
<xsl:apply-templates/>
</ce:para>
</xsl:template>
<!-- now add similar templates here for transformation of strong, em etc -->

Xslt - How do you check for a grandchild node with a certain path name. (xpath 1.0)

What I want to do is given an element as context, I want to determine if it has a child with a given name and determine if that child has a node with a given name so I can do operations with it. It is important that I do this in XPath 1.0 syntax.
The code that I've gotten so far is this.
<xsl:for-each select="child::*">
<xsl:if test="contains(name(), 'description')">
<xsl:for-each select="child::*">
<xsl:if test="contains(name(), 'text')">
<xsl:value-of select="node()"/>
</xsl:if>
</xsl:for-each>
</xsl:if>
</xsl:for-each>
It works, but it's big and ugly and I know that there's a way to condense it. The for-eachs there are unnecessary, since I'm only expecting one child node to be named description, and for it to only have one text node.
I feel like this solution should work
<xsl:for-each select="./description/text">
..
</xsl:for-each>
But it isn't, and I'm not really good enough with XPath Syntax to know why.
The reason I'm asking is because though I've found answers that detect whether a child node has a name, and I've found answers that can get to that child node's context, I haven't found an answer that combines the two, though maybe I just haven't been searching hard enough, in which case I apologize.
Edit: Woops, sorry yeah I forgot to mention that the contains() part of the code was also just a hack because I wasn't sure how to compare their values with equality.
Also as long as the answer is there, <xsl:for-each select="description/text"> does not work either.
A sample of the XML in question is this
<leaf>
<description>
<text> Various Words
</text>
</description>
</leaf>
where the context is the leaf and I am trying to get to the text node.
Edit: The Second Coming:
The problem for me was that my XSLT file was using a default namespace (in my case named a). If I had added that then Borodin's answer would have been correct.
To be specific, this is the code which ended up working for me in the end, in case anyone wants to know.
<xsl:for-each select="a:description/a:text>
<xsl:value-of select="node()"/>
</xsl:for-each>
Thanks Guys ^-^
Do you really want to check whether the element names contain those strings? Or, as your narrative says, do you want elements with that exact name?
To do something like what you have already written, use
<xsl:for-each select="*[contains(name(), 'description')]/*[contains(name(), 'text')]">
<xsl:value-of select="node()"/>
</xsl:for-each>
But if you know the complete names it is a lot neater:
<xsl:for-each select="description/text">
<xsl:value-of select="node()"/>
</xsl:for-each>
If that doesn't work then we need to see more of your source XML and your transform.
Update
If I use this XML
<leaf>
<description>
<text>Various Words</text>
</description>
<description>
<text>More Words</text>
</description>
<description>
<text>Other Words</text>
</description>
</leaf>
and apply this 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="text"/>
<xsl:template match="/leaf">
<xsl:for-each select="description/text">
<xsl:value-of select="."/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
the output is the expected Various WordsMore WordsOther Words. I don't know how to help you unless you describe your situation better, except to say that transforms should be written with another template rather than for-each wherever possible. Like this variation which produces the same output as above.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/leaf">
<xsl:apply-templates select="description/text"/>
</xsl:template>
<xsl:template match="text">
<xsl:value-of select="."/>
</xsl:template>
</xsl:stylesheet>

XSLT <xsl: value-of> generates extra line-breaks

In XSLT, using , it generates a line break before the rendered value and another one after it.
Here comes an example:
<xsl:when test="name(.) = 'Item'">
"<xsl:value-of select="./Item/Data[last()]/text()"/>"
</xsl:when>
And the rendered result is:
"
09/07/2012
"
As you can see, it puts two line breaks before and after the result value, while the desired result is:
"09/07/2012"
The original input is :
Here comes the original input, sorry for that.
<Item>
<Item>
<Data>105</Data>
<Data>09/07/2012</Data>
</Item>
</Item>
I'm executing this XSLT within an Oracle Server Bus
Any help will be appreciated.
The extra space is could also be coming from the selected text. Use normalize-space() to remove this.
<xsl:value-of select="normalize-space(./Item/Data[last()]/text())"/>
Edit Overnuts is correct in using <xsl:text> around the quotes, otherwise the Xslt processor will preserve the newline before the opening / after the closing quotes. However, I still can't see why a newline could get in between the quotes and your xsl:value-of?
I've tried the following
<?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" indent="yes" omit-xml-declaration="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/xml" xml:space="default">
<xsl:apply-templates select="*" />
</xsl:template>
<xsl:template match="*" xml:space="default">
<xsl:choose>
<xsl:when test="name(.) = 'Item'">
<xsl:text>"</xsl:text>
<xsl:value-of select="normalize-space(./Item/Data[last()]/text())"/>
<xsl:text>"</xsl:text>
</xsl:when>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
When run with this XML:
<xml>
<Item>
<Item>
<Data>105</Data>
<Data>09/07/2012</Data>
</Item>
</Item>
</xml>
Produces "09/07/2012"
I think you can try:
everything on a single line (quick and dirty):
<xsl:when test="name(.) = 'Item'">"<xsl:value-of select="./Item/Data[last()]/text()"/>"</xsl:when>
or use tags like this (best practice):
<xsl:when test="name(.) = 'Item'">
<xsl:text>"</xsl:text>
<xsl:value-of select="./Item/Data[last()]/text()"/>
<xsl:text>"</xsl:text>
</xsl:when>
Possibly the original XML soure contains these newlines (indentation), try something like:
<xsl:value-of select="concat('~', normalize-space(./Item/Data[last()]/text()), '~')"/>
Perhaps an implementation specific bug?
Using xsltproc all the above work as expected, although the expected results of bare newline+whitespace+quote+date+quote+newline+space is for the /external/ white-space to be copied as well. All the other examples produce the same 13 bytes, including a trailing newline.
Using libxml 20706, libxslt 10124 and libexslt 813
xsltproc was compiled against libxml 20701, libxslt 10124 and libexslt 813
libxslt 10124 was compiled against libxml 20701
libexslt 813 was compiled against libxml 20701

How to comment in XSLT and not HTML

I'm writing XSL and I want to make comments throughout the code that will be stripped when it's processed, like PHP, however I'm not sure how.
I'm aware of the comment object, but it prints out an HTML comment when processed. :\
<xsl:comment>comment</xsl:comment>
You use standard XML comments:
<!-- Comment -->
These are not processed by the XSLT transformer.
Just make sure that you put your <!-- comments --> AFTER the opening XML declaration (if you use one, which you really don't need):
BREAKS:
<!-- a comment -->
<?xml version="1.0"?>
WORKS:
<?xml version="1.0"?>
<!-- a comment -->
I scratched my head on this same issue for a bit while debugging someone else's XSLT... seems obvious, but easily overlooked.
Note that white space on either side of the comments can end up in the output stream, depending on your XSLT processor and its settings for handling white-space. If this is an issue for your output, make sure the comment is bracketed by xslt tags.
EG
<xsl:for-each select="someTag">
<xsl:text>"</xsl:text>
<!-- output the id -->
<xsl:value-of select="#id"/>
<xsl:text>"</xsl:text>
</xsl:for-each>
Will output " someTagID" (the indent tab/spaces in front of the comment tag are output).
To remove, either unindent it flush with left margin, or bracket it like
<xsl:text>"</xsl:text><!-- output the id --><xsl:value-of select="#id"/>
This is the way to do it in order to create a comment node that won't be displayed in html
<xsl:comment>
<!-- Content:template -->
</xsl:comment>
Sure. Read http://www.w3.org/TR/xslt#built-in-rule and then it should be apparent why this simple stylesheet will (well, should) do what you want:
<?xml version="1.0"?>
<xsl:stylesheet xmlns="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="comment()">
<xsl:copy/>
</xsl:template>
<xsl:template match="text()|#*"/>
</xsl:stylesheet>
Try :
<xsl:template match="/">
<xsl:for-each select="//comment()">
<SRC_COMMENT>
<xsl:value-of select="."/>
</SRC_COMMENT>
</xsl:for-each>
</xsl:template>
or use a <xsl:comment ...> instruction for a more literal duplication of the source document content in place of my <SRC_COMMENT> tag.