xsl for-each to create an iterable node list - xslt

I need to iterate over the elements of an XML file in sorted order of their language field many times. What I try is to get an iterable list of the languages as follows:
<xsl:variable name="languages">
<xsl:for-each select="elem/FIELD[#NAME='language']">
<xsl:sort select="."/>
<xsl:value-of select="."/>
</xsl:for-each>
</xsl:variable>
While I can verify with a
<xsl:value-of select="$languages"/>
that the sorting works, I cannot iterate like
<xsl:for-each select="$langauges">...</xsl:for-each>
because the XSL processor complains that select expression does not evaluate to a node set.
Edit: Not sure whether this is important, but I have
<xsl:output encoding="UTF-8"
method="xml"
media-type="text/xml"
indent="yes" />
What do I have to insert in the loop to make the result into a node set? Is this at all possible?

Given you say that
XSL processor complains that select expression does not evaluate to a node set.
I assume you're using XSLT 1.0 rather than 2.0. In XSLT 1.0 when you declare a variable with content rather than a select attribute, the resulting variable contains something called a "result tree fragment" rather than a node set. You can apply value-of and copy-of to a RTF to send it to the output but you can't navigate into it using XPath expressions.
Most XSLT processors provide some sort of extension function to convert a RTF into a real node set - msxsl for the Microsoft processor or exslt for most others.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
xmlns:exsl="http://exslt.org/common"
exclude-result-prefixes="exsl">
<!-- .... -->
<xsl:variable name="languagesRTF">
<xsl:for-each select="elem/FIELD[#NAME='language']">
<xsl:sort select="."/>
<lang><xsl:value-of select="."/></lang>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="languages" select="exsl:node-set($languagesRTF)/lang" />
In XSLT 2.0 there is no distinction between result tree fragments and node sets - they're both treated as sequences - so you don't need the extension function in that version.

Try the following:
To declare variable:
<xsl:variable name="languages">
<xsl:for-each select="elem/FIELD[#NAME='language']">
<xsl:sort select="."/>
<xsl:copy-of select="."/>
</xsl:for-each>
</xsl:variable>
And to loop:
<xsl:for-each select="$languages/*">

You need to convert the result tree fragment in your variable into a node-set, using the EXSLT node-set() function.

XSLT 1.0 allows you to process a node-set in sorted order, but it doesn't allow you to save a sorted sequence in a variable (the data model only has sets, not sequences). The only way you can save sorted data in 1.0 is to construct a new tree containing copies of the original elements in a different order, and then use the node-set() extension to make this tree processable.
This changes in XSLT 2.0, which has a data model based on sequences. In 2.0 you can save a sorted sequence of nodes in a variable without copying the nodes into a new tree.

Related

How to find smallest string in result tree fragment(RTF) containing two strings without using extension function e.g. node-set in xslt 1.0

I have two strings variables basically storing currency codes which can have values like USD or EUR or JPY etc.
Examples:
If variable boughtccy contains 'JPY' and variable soldccy contains 'USD' then it should return a string 'BOUGHTCCY' signifying that it is the variable named boughtccy actually contains the smallest currency.
Similarly, if variable boughtccy contains 'EUR' and variable soldccy contains 'AUD' then it should return a string 'SOLDCCY' signifying that it is the variable named soldccy actually contains the smallest currency.
I have written following code which works perfectly fine except for in Altova XMLSpy. I have a requirement that it should also work in XMLSpy.
Here is the code:
xsl:variable name="smallerccy">
<xsl:variable name="nodes">
<node>
<xsl:value-of select="$boughtccy"/>
</node>
<node>
<xsl:value-of select="$soldccy"/>
</node>
</xsl:variable>
<xsl:for-each select="common:node-set($nodes)/*">
<xsl:sort select="."/>
<xsl:choose>
<xsl:when test="position()=1 and .=$boughtccy">BOUGHTCCY</xsl:when>
<xsl:when test="position()=1 and .=$soldccy">SOLDCCY</xsl:when>
</xsl:choose>
</xsl:for-each>
</xsl:variable>
How do I achieve this without using exslt:node-set() function? I cannot use xslt 2.0.
I believe the underlying problem you are having is that in XSLT 1.0 strings can only compared only for equality (or inequality), and so you can simply do <xsl:when test="$boughtccy > $soldccy">
In you particular case, where you are dealing with a finite number of strings, you could define a variable containing all possible currency codes, and use string handling to find one is first.
Try this XSLT (You would, of course, have to amend $AllCurrencies to have all possible currency codes).
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:variable name="AllCurrencies" select="'AUS|EUR|JPY|USD'" />
<xsl:param name="boughtccy" select="'JPY'" />
<xsl:param name="soldccy" select="'USD'" />
<xsl:template match="/">
<xsl:choose>
<xsl:when test="string-length(substring-before($AllCurrencies, $boughtccy)) < string-length(substring-before($AllCurrencies, $soldccy))">BOUGHTCCY</xsl:when>
<xsl:otherwise>SOLDCCY</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
I would write conditional logic, using system-property() and function-available(), that implements this in different ways for different processors:
if it's XSLT 2.0, use min(($boughtccy, $soldccy))
if it's XSLT 1.0 with a node-set extension, use the sorting technique that you're already using
otherwise give up. (There is no way to compare two strings for "<" in XSLT 1.0 other than creating a node-set and sorting it, and that requires the node-set extension).

XSLT 1.0 - conditional node assignment

using pure XSLT 1.0, how can I conditionally assign the node. I am trying something like this but it's not working.
<xsl:variable name="topcall" select="//topcall"/>
<xsl:variable name="focusedcall" select="//focusedcall" />
<xsl:variable name="firstcall" select="$topcall | $focusedcall"/>
For variable firstcall, I am doing the conditional node selection. if there is a topcall then assign it to firstcall, othersie assign firstcall to the focusedcall.
This should work:
<xsl:variable name="firstcall" select="$topcall[$topcall] |
$focusedcall[not($topcall)]" />
In other words, select $topcall if $topcall nodeset is non-empty; $focusedcall if $topcall nodeset is empty.
Re-Update regarding "it can be 5-6 nodes":
Given that there may be 5-6 alternatives, i.e. 3-4 more besides $topcall and $focusedcall...
The easiest solution is to use <xsl:choose>:
<xsl:variable name="firstcall">
<xsl:choose>
<xsl:when test="$topcall"> <xsl:copy-of select="$topcall" /></xsl:when>
<xsl:when test="$focusedcall"><xsl:copy-of select="$focusedcall" /></xsl:when>
<xsl:when test="$thiscall"> <xsl:copy-of select="$thiscall" /></xsl:when>
<xsl:otherwise> <xsl:copy-of select="$thatcall" /></xsl:otherwise>
</xsl:choose>
</xsl:variable>
However, in XSLT 1.0, this will convert the output of the chosen result to a result tree fragment (RTF: basically, a frozen XML subtree). After that, you won't be able to use any significant XPath expressions on $firstcall to select things from it. If you need to do XPath selections on $firstcall later, e.g. select="$firstcall[1]", you then have a few options...
Put those selections into the <xsl:when> or <xsl:otherwise> so that they happen before the data gets converted to an RTF. Or,
Consider the node-set() extension, which converts an RTF to a nodeset, so you can do normal XPath selections from it. This extension is available in most XSLT processors but not all. Or,
Consider using XSLT 2.0, where RTFs are not an issue at all. In fact, in XPath 2.0 you can put normal if/then/else conditionals inside the XPath expression if you want to.
Implement it in XPath 1.0, using nested predicates like
:
select="$topcall[$topcall] |
($focusedcall[$focusedcall] | $thiscall[not($focusedcall)])[not($topcall)]"
and keep on nesting as deep as necessary. In other words, here I took the XPath expression for 2 alternatives above, and replaced $focusedcall with
($focusedcall[$focusedcall] | $thiscall[not($focusedcall)])
The next iteration, you would replace $thiscall with
($thiscall[$thiscall] | $thatcall[not($thiscall)])
etc.
Of course this becomes hard to read, and error-prone, so I would not choose this option unless the others aren't feasible.
Does <xsl:variable name="firstcall" select="($topcall | $focusedcall)[1]"/> do what you want? That is usually the way to take the first node in document order of different types of nodes.
I. XSLT 1.0 Solution This short (30 lines), simple and parameterized transformation works with any number of node types/names:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:param name="pRatedCalls">
<call type="topcall"/>
<call type="focusedcall"/>
<call type="normalcall"/>
</xsl:param>
<xsl:variable name="vRatedCalls" select=
"document('')/*/xsl:param[#name='pRatedCalls']/*"/>
<xsl:variable name="vDoc" select="/"/>
<xsl:variable name="vpresentCallNames">
<xsl:for-each select="$vRatedCalls">
<xsl:value-of select=
"name($vDoc//*[name()=current()/#type][1])"/>
<xsl:text> </xsl:text>
</xsl:for-each>
</xsl:variable>
<xsl:template match="/">
<xsl:copy-of select=
"//*[name()
=
substring-before(normalize-space($vpresentCallNames),' ')]"/>
</xsl:template>
</xsl:stylesheet>
When applied to this XML document (do note the document order doesn't coincide with the specified priorities in the pRatedCalls parameter):
<t>
<normalcall/>
<focusedcall/>
<topcall/>
</t>
produces exactly the wanted, correct result:
<topcall/>
when the same transformation is applied to the following XML document:
<t>
<normalcall/>
<focusedcall/>
</t>
again the wanted and correct result is produced:
<focusedcall/>
Explanation:
The names of the nodes that are to be searched for (as many as needed and in order of priority) are specified by the global (typically externally specified) parameter named $pRatedCalls.
Within the body of the variable $vpresentCallNames we generate a space-separated list of names of elements that are both specified as a value of the type attribute of a call elementin the$pRatedCalls` parameter and also are names of elements in the XML document.
Finally, we determine the first such name in this space-separated list and select all elements in the document, that have this name.
II. XSLT 2.0 solution:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:param name="pRatedCalls" select=
"'topcall', 'focusedcall', 'normalcall'"/>
<xsl:template match="/">
<xsl:sequence select=
"//*
[name()=$pRatedCalls
[. = current()//*/name()]
[1]
]"/>
</xsl:template>
</xsl:stylesheet>

how to make an element inside a variable

XSLT 1.0
I will need a variable with the following structure, basically I need to construct a variable which actually is a element. I know it looks silly but I need such thing because of the limitation of other stuff.
<xsl:variable name="options">
<xsl:element name="option">
<xsl:attribute name="value">
<xsl:text>test1</xsl:text>
</xsl:attribute>
<xsl:text>test1</xsl:text>
</xsl:element>
</xsl:variable>
Now the problem is, when I call it later in the template with
<xsl:value-of select="$options"/>
the output html only have test1 instead of what I want
<option>test1</option>
So it means the tag is missing. What's the right syntax to do this? Thanks in advance!
You need to make difference between <xsl:value-of> and <xsl:copy-of>
In XSLT 1.0 the <xsl:value-of> instruction creates a text node that contains the string value of the result of evaluating the XPath expression, specified in the select attribute. By definition, the string value of an element is the concatenation (in document order) of all of its descendent text nodes -- this is how you get the string "test1" output.
By contrast:
<xsl:copy-of>
outputs a copy of every node of the node-set that is selected by the XPath expression specified in the select attribute.
Therefore, in order to copy the complete contents of $options, you need to specify:
<xsl:copy-of select="$options" />
Here is a complete example:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:variable name="vOptions">
<option value="test1">test1</option>
</xsl:variable>
<xsl:template match="/">
<xsl:copy-of select="$vOptions"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on any XML document (not used), the wanted, correct result is produced:
<option value="test1">test1</option>
try:
<xsl:copy-of select="$options" />

Automating exslt:node-set?

Not sure if this is possible, but trying set up something that doesn't make me have to type exslt:node-set when pulling values from a dynamically created node block. I am storing the entire set of nodes in a variable, and wrapping it in exslt:node-set, but why does it not work when I then try to pull from it. Is this possible?
<xsl:variable name="LANG">
<xsl:variable name="tmp">
<xsl:element name="foo">
<xsl:element name="bar">Hello</xsl:element>
</xsl:element>
</xsl:variable>
<xsl:value-of select="exslt:node-set($tmp)"/>
</xsl:variable>
<!-- Love to be able to do this -->
<xsl:value-of select="$LANG/foo/bar"/>
<!-- This does work -->
<xsl:value-of select="exslt:node-set($LANG)/foo/bar"/>
In XSLT 1.0, the variable defined as in your example are called result tree fragments (RTF) and you can only use xsl:copy-of to copy the entire fragment to the result tree or xsl:value-of to copy the entire content. Example
<xsl:copy-of select="$LANG"/>
If you want treat the variable as a temporary tree you need the node-set() extension.
The common way to deal with static tree fragments (like lookup tables) in XSLT 1.0 is to define them as children of the stylesheet root elements (using a custom namespace). Then you can use the document() function to retrieve the wanted value.
Note If you are using Saxon (v>6.5), you could simply set the stylesheet version to 1.1 and you will be able to manage the RTF without any node-set extension.
[XSLT 1.0]
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:empo="http://stackoverflow.com/users/253811/empo">
<empo:LANG>
<empo:foo>
<empo:bar>Hello</empo:bar>
</empo:foo>
</empo:LANG>
<xsl:template match="/">
<xsl:variable name="LANG" select="document('')/*/empo:LANG"/>
<xsl:value-of select="$LANG/empo:foo/empo:bar"/>
</xsl:template>
</xsl:stylesheet>

XSLT: need alternative to document()-function for multi-source processing

I'm adapting an XSLT from a third party which transforms an arbitrary number of XMLs into a single HTML document. It's a pretty complex script and it will be revised in the future, so I'm trying to do a minimal adaptation in order to get it to work for our needs.
The following is a stripped down version of the XSLT (containing the essentials):
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns="http://www.w3.org/1999/xhtml">
<xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes"/>
<xsl:param name="files" select="document('files.xml')//File"/>
<xsl:param name="root" select="document($files)"/>
<xsl:template match="/">
<xsl:for-each select="$root/RootNode">
<xsl:apply-templates select="."/>
</xsl:for-each>
</xsl:template>
<xsl:template match="RootNode">
<xsl:for-each select="//Node">
<xsl:text>Node: </xsl:text><xsl:value-of select="."/><xsl:text>, </xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Now files.xml contains a list of all the URLs of the files to be included (in this case the local files file1.xml and file2.xml). Because we want to read XMLs from memory rather than from disk, and because the invocation of the XSLT only allows for a single XML source, I have combined the two files in a single XML document. The following is a combination of two files (there may be more in a real situation)
<?xml version="1.0" encoding="UTF-8"?>
<TempNode>
<RootNode>
<Node>1</Node>
<Node>2</Node>
</RootNode>
<RootNode>
<Node>3</Node>
<Node>4</Node>
</RootNode>
</TempNode>
where the first RootNode originally resided in file1.xml and the second in file2.xml.
Due to the complexity of the actual XSLT, I've figured that my best shot is to try to alter the $root-param. I've tried the following:
<xsl:param name="root" select="/TempNode"/>
The problem is this. In the case of <xsl:param name="root" select="document($files)"/>, the XPath expression "//Node" in <xsl:for-each select="//Node"> selects the Node's from file1.xml and file2.xml independently, i.e. producing the following (desired) list:
Node: 1, Node: 2, Node: 3, Node: 4,
However, when I combine the content of the two files into a single XML and parse this (and use the suggested $root-definition), the expression "//Node" will select all Node's that are children of the TempNode. (In other words, the desired list, as represented above, is produced twice due to the combination with the outer <xsl:for-each select="$root/RootNode"> loop.)
(A side note: as observed in comment a) in this page, document() apparently changes the root node, perhaps explaining this behavior.)
My question becomes:
How can I re-define $root, using the combined XML as source instead of a multi-source through document(), so that the list is only produced once, without touching the remainder of the XSLT? It's like if $root defined using the document()-function, there is no common root node in the param. Is it possible to define a param with two "separate" node trees?
Btw: I've tried defining a document like this
<xsl:param name="root">
<xsl:for-each select="/TempNode/*">
<xsl:document>
<xsl:copy-of select="."/>
</xsl:document>
</xsl:for-each>
</xsl:param>
thinking it might solve the problem, but the "//Node" expression still fetches all the Nodes. Is the context node in the <xsl:template match="RootNode">-template actually somewhere in the input document and not the param? (Honestly, I'm pretty confused when it comes to context nodes.)
Thanks in advance!
(Updated more)
OK, some of the problem is becoming clear. First, just to make sure I understand, you aren't actually passing parameters for $files and $root to the XSLT processor invocation, right? (They might as well be variables rather than params?)
Now to the main issues... In XPath, when you evaluate an expression that begins with "/" (including "//"), the context node is ignored [mostly]. Therefore, when you have
<xsl:template match="RootNode">
<xsl:for-each select="//Node">
the matched RootNode is ignored. Maybe you wanted
<xsl:template match="RootNode">
<xsl:for-each select=".//Node">
in which the for-each would select Node elements that are descendants of the matched RootNode? This would fix your problem of generating the desired node list twice.
I inserted [mostly] above because I recalled that an "absolute location path" starts from "the root node of the document containing the context node". So the context node does affect what document is used for "//Node". Maybe that's what you intended all along? I guess I was slow to catch on to that.
(A side note: as observed in comment
a) in this page, document() apparently
changes the root node, perhaps
explaining this behavior.)
Or more precisely,
An absolute location path ["/..."]
followed by a relative location
path... selects the set of nodes that
would be selected by the relative
location path relative to the root
node of the document containing the
context node.
document() doesn't actually change anything, in the sense of side effects; rather, it returns a set of nodes contained (usually) by different documents than the primary source document. XSLT instructions like xsl:apply-templates and xsl:for-each establish new values for the context node inside the scope of their template bodies. So if you use xsl:apply-templates and xsl:for-each with select="document(...)/...", the context node inside the scope of those instructions will belong to an external document, so any use of "/..." as an XPath will start from that external document.
Updated again
How can I re-define $root, using the
combined XML as source instead of a
multi-source through document(), so
that the list is only produced once,
without touching the remainder of the
XSLT?
As #Alej hinted, it's really not possible given the above constraint. If you're selecting "//Node" in each iteration of the loop over "$root/RootNode", then in order for each iteration not to select the same nodes as the other iterations, each value of "$root/RootNode" must be in a different document. Since you're using the combined XML source, instead of a multi-source, this is not possible.
But if you don't insist that your <xsl:for-each select="//..."> XPath expression cannot change, it becomes very easy. :-) Just put a "." before the "//".
It's like if $root defined using the document()-function, there is no common root node
in the param.
The value of the param is a node-set. All nodes in the set may be contained in the same document, or they may not, depending on whether the first argument to document() is a nodeset or just a single node.
Is it possible to define a param with two "separate" node trees?
I believe by "separate", you mean "belonging to different documents"? Yes it is, but I don't think you can do it in XSLT 1.0 unless you're selecting nodes that belong to different documents in the first place.
You mentioned trying
<xsl:param name="root">
<xsl:for-each select="/TempNode/*">
<xsl:document>
<xsl:copy-of select="."/>
</xsl:document>
</xsl:for-each>
</xsl:param>
but <xsl:document> is not defined in XSLT 1.0, and your stylesheet says version="1.0". Do you have XSLT 2.0 available? If so, let us know and we can pursue this option. To be honest, <xsl:document> is not familiar territory for me. But I'm happy to learn along with you.
You can apply only nodes you need:
Input:
<?xml version="1.0" encoding="UTF-8"?>
<TempNode>
<RootNode>
<Node>1</Node>
<Node>2</Node>
</RootNode>
<RootNode>
<Node>3</Node>
<Node>4</Node>
</RootNode>
</TempNode>
<?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="html" indent="yes"/>
<xsl:template match="/">
<xsl:copy>
<xsl:apply-templates select="TempNode/RootNode"/>
</xsl:copy>
</xsl:template>
<xsl:template match="RootNode">
<xsl:value-of select="concat('RootNode-', generate-id(.), '
')"/>
<xsl:apply-templates select="Node"/>
</xsl:template>
<xsl:template match="Node">
<xsl:value-of select="concat('Node', ., '
')"/>
</xsl:template>
</xsl:stylesheet>
Output:
RootNode-N65540
Node1
Node2
RootNode-N65549
Node3
Node4