Populate a variable with a subtree - xslt

in a version="2.0" stylesheet:
the following code produces the correct output
<xsl:variable name="obj">
<xsl:choose>
<xsl:when test="t:ReferencedObjectType='Asset'">
<xsl:value-of select="/t:Flow/t:FHeader/t:Producer/t:Repository" />
</xsl:when>
</xsl:choose>
</xsl:variable>
<xsl:choose>
<xsl:value-of select="$obj"/>
but this one does not
<xsl:variable name="obj">
<xsl:choose>
<xsl:when test="t:ReferencedObjectType='Asset'">
<xsl:value-of select="/t:Flow/t:FHeader/t:Producer" />
</xsl:when>
</xsl:choose>
</xsl:variable>
<xsl:choose>
<xsl:value-of select="$obj/t:Repository"/>
How can I get the second code to run as expected ?
If needed, is there a solution in v3 ?
this code does not run either
<xsl:variable name="obj">
<xsl:choose>
<xsl:when test="t:ReferencedObjectType='Asset'">
<xsl:copy-of select="/t:Flow/t:FHeader/t:Producer" />
</xsl:when>
</xsl:choose>
</xsl:variable>
<xsl:choose>
<xsl:value-of select="$obj/t:Repository"/>
relevant xml input
<Flow>
<FHeader>
<Producer>
<Repository>tests.com</Repository>
</Producer>
</FHeader>
</Flow>

You can simply select <xsl:variable name="obj" select="/t:Flow/t:FHeader/t:Producer/t:Repository[current()/t:ReferencedObjectType='Asset']"/>. Or, as Tim already commented, use xsl:copy-of, also taking into account that you then later on need e.g. $obj/t:Producer/t:Repository to select the right level.
Or learn about the as attribute and use e.g. <xsl:variable name="obj" as="element()*">...<xsl:copy-of select="/t:Flow/t:FHeader/t:Producer"/> ...</xsl:variable>, then you later on can use e.g. $obj/t:Repository.
There is also xsl:sequence to select input nodes instead of copying them, in particular with xsl:variable if you use the as attribute. This might consume less memory.
Furthermore XPath 2 and later have if (condition-expression) then expression else expression conditional expressions at the expression level so you might not need XSLT with xsl:choose/xsl:when but could use the <xsl:variable name="obj" select="if (t:ReferencedObjectType='Asset']) then /t:Flow/t:FHeader/t:Producer else if (...) then ... else ()"/>, that way you would select e.g. an input t:Producer element anyway and if you use the variable you can directly select the t:Repository child.

Related

Duplicates in a map

I currently have an XSLT function that loads key=value pairs from a text file into a map.
<xsl:function name="myns:loadMapping" as="map(*)">
<xsl:variable name="mapping" as="map(xs:string, xs:string)">
<xsl:map>
<xsl:for-each select="unparsed-text-lines($inputFile,$fileEncoding)">
<!-- Takes only lines which are in the form abc=xyz and are not comments (does not start with #) -->
<xsl:if test="contains(.,'=') and not(starts-with(.,'#'))">
<xsl:map-entry key="substring-before(.,'=')" select="substring-after(.,'=')"/>
</xsl:if>
</xsl:for-each>
</xsl:map>
</xsl:variable>
<xsl:sequence select="$mapping"/>
</xsl:function>
The function works fine unless the user tries to load a file containing duplicates, in which case the XSLT transform fails with an error (expected behaviour):
Error evaluating (map:merge(...)) on line xyz column xy of xyz.xsl:
XTDE3365: Duplicate key in constructed map: {keyInError}
Is there a way I could catch this case and keep the transformation from aborting, something like this :
<xsl:function name="myns:loadMapping" as="map(*)">
<xsl:variable name="mapping" as="map(xs:string, xs:string)">
<xsl:map>
<xsl:for-each select="unparsed-text-lines($inputFile,$fileEncoding)">
<!-- Takes only lines which are in the form abc=xyz and are not comments (does not start with #) -->
<xsl:if test="contains(.,'=') and not(starts-with(.,'#'))">
<xsl:choose>
<xsl:when test="...map contains key...">
<xsl:message>Map already contains key. Please check input file.</xsl:message>
</xsl:when>
<xsl:otherwise>
<xsl:map-entry key="substring-before(.,'=')" select="substring-after(.,'=')"/>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:for-each>
</xsl:map>
</xsl:variable>
<xsl:sequence select="$mapping"/>
</xsl:function>
I see that there is something implemented for a future XSLT 4.0 release (Saxon - Controlling duplicates on xsl:map) but I would like to stick to XSLT 3.0 for the time being.
Thanks.
To add to Martin Honnen's suggestions, you could use xsl:iterate instead of xsl:for-each, passing the map as a parameter, which would allow you to inspect the map before adding another entry to it.
<xsl:iterate select="...">
<xsl:param name="map" select="map{}"/>
<xsl:choose>
<xsl:when test="map:contains($map, ...)">...</xsl:when>
<xsl:otherwise>
<xsl:next-iteration>
<xsl:with-param name="map" select="map:put($map, ..., ...)"/>
Well, both map:merge in XPath 3.1 or of course grouping with e.g.
<xsl:for-each-group select="unparsed-text-lines($inputFile,$fileEncoding)[contains(.,'=') and not(starts-with(.,'#'))]" group-by="substring-before(., '=')">
<xsl:map-entry key="current-grouping-key()" select="substring-after(., '=')"/>
<xsl:if test="current-group()[2]">
<xsl:message>..</xsl:message>
</xsl:if>
</xsl:for-each-group>
allow you more control than your approach without having to wait for XSLT 4 or trying to use experimental extensions.

idiomatic alternative to choose -> test -> value-of (XSLT 1.0)

In the work I do I seem to see a lot of code liek this..
<xsl:choose>
<xsl:when test="long_xpath_to_optional/#value1">
<xsl:value-of select="long_xpath_to_optional/#value"/>
</xsl:when>
<xsl:when test="another_long_xpath_to_optional/#value">
<xsl:value-of select="another_long_xpath_to_optional/#value"/>
</xsl:when>
<etc>
</etc>
<otherwise>
<xsl:value-of select="default_long_xpath_to_value"/>
</otherwise>
</xsl:choose>
its very long and very repetitive.
When I'm were working in some other (psuedo) language I would go
let values = concat(list(long_xpath_to_optional_value),list(another_long_xpath_to_optional_value))
let answer = tryhead(values,default_long_xpath_to_value)
i.e. create a list of values in priority order, and then take the head.
I only evaluate each path once
how would you do something similar in XSLT 1.0 (we can use node-sets).
I was wondering if you can create a node-set somehow
You can - but it's not going to be any shorter:
<xsl:variable name="values">
<xsl:apply-templates select="long_xpath_to_optional/#value" mode="values"/>
<xsl:apply-templates select="another_long_xpath_to_optional/#value" mode="values"/>
<xsl:apply-templates select="default_long_xpath_to_value/#value" mode="values"/>
</xsl:variable>
<xsl:value-of select="exsl:node-set($values)/value[1]" xmlns:exsl="http://exslt.org/common"/>
and then:
<xsl:template match="#value" mode="values">
<value>
<xsl:value-of select="."/>
</value>
</xsl:template>
But at least the repetition is eliminated.
Alternatively, you could do:
<xsl:template match="#value" mode="values">
<xsl:value-of select="."/>
<xsl:text>|</xsl:text>
</xsl:template>
and then:
<xsl:value-of select="substring-before($values, '|')"/>
To use variables you write
<xsl:variable name="value1" select="long_xpath_to_optional/#value1"/>
<xsl:variable name="value2" select="another_long_xpath_to_optional/#value"/>
<xsl:variable name="value3" select="default_long_xpath_to_value"/>
and then in XPath 2 or 3 all you would need is ($value1, $value2, $value3)[1] or head(($value1, $value2, $value3)) but in XSLT 1 with XPath 1 all you can write as a single expression is ($value1 | $value2 | $value3)[1] which sorts in document order so unless the document order is the same as your test order this wouldn't work to check the values; rather you would need to maintain the
<xsl:choose>
<xsl:when test="$value1">
<xsl:value-of select="$value1"/>
</xsl:when>
<xsl:when test="$value2">
<xsl:value-of select="$value2"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$value3"/>
</xsl:otherwise>
</xsl:choose>
Of course in XPath 2 you wouldn't really need the variables and could use (long_xpath_to_optional/#value1, another_long_xpath_to_optional/#value, default_long_xpath_to_value)[1] as well directly.

Choose with for-each inside?

I have a parameterignoreAttributes which is a comma separated list of things to look for. I want to set a variable copyAttrib to be equal to whether any of them are exactly matched by name().
If xsl were a procedural language where variables could be reassigned, I'd use something like this:
<xsl:variable name="copyAttrib" select="true()">
<xsl:for-each select="tokenize($ignoreAttributes,',')">
<xsl:if test="compare(., name()) != 0">
<xsl:variable name="copyAttrib" select="false()"/>
</xsl:if>
</xsl:for-each>
Unfortunately, I can't do that, because xsl is functional (so says this other answer). So variables can only be assigned once.
I think the solution would look something like:
<vsl:variable name="copyAttrib">
<xsl:choose>
<xsl:when>
<xsl:for-each select="tokenize($ignoreAttributes, ',')">
<xsl:if test="compare(., name()) != 0"/>
</xsl:for-each>
<xsl:otherwise>
<xsl:value-of select="false()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
Obviously not exactly that (otherwise I wouldn't be asking.)
I know that I could bypass the tokenize and for-each loop by just using replaces on ignoreAttributes and changing all the , to | and then using matches, but I'd like to avoid that if possible because then I need to deal with the possibility that ignoreAttributes (which the user provides) might contain some special characters that will change the regex pattern and escape them all.
I have a parameterignoreAttributes which is a comma separated list of things to look for. I want to set a variable copyAttrib to be equal to whether any of them are exactly matched by name().
That sounds to me like
<xsl:variable name="copyAttrib" as="xs:boolean"
select="tokenize($parameterignoreAttributes, ',') = name()"/>
You say:
Unfortunately, I can't do that, because xsl is functional
when what you mean is: "Fortunately, I don't need to do that, because XSLT is functional".
An XSLT-1.0 way of doing this is by using a recursive, named template:
<xsl:template name="copyAttrib">
<xsl:param name="attribs" />
<xsl:choose>
<xsl:when test="normalize-space(substring-before($attribs,',')) = normalize-space(name(.))">
<xsl:value-of select="'true'" />
</xsl:when>
<xsl:when test="normalize-space($attribs) = ''">
<xsl:value-of select="'false'" />
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="copyAttrib">
<xsl:with-param name="attribs" select="substring-after($attribs,',')" />
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
Apply this template onto the current, the selected, node and wrap it in a <xsl:variable>:
<xsl:variable name="copyAttribResult">
<xsl:call-template name="copyAttrib">
<xsl:with-param name="attribs" select="'a,b,c,...commaSeparatedValues...'" />
</xsl:call-template>
</xsl:variable>
to get either true or false as a result.

build node-set variable from result tree fragment using <xsl:choose>

Is it possible to create a node-set variable from an rtf using xsl:choose (for use in MSXML engine)?
I have the following construct:
<xsl:choose>
<xsl:when test="function-available('msxsl:node-set')">
<xsl:variable name="colorList" select="msxsl:node-set($std:colorList)"/>
<xsl:for-each select="$colorList/color">
tr.testid<xsl:value-of select="#testid"/> {
color:<xsl:value-of select="."/>;
}
</xsl:for-each>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="colorList" select="$std:colorList"/>
<xsl:for-each select="$colorList/color">
tr.testid<xsl:value-of select="#testid"/> {
color:<xsl:value-of select="."/>;
}
</xsl:for-each>
</xsl:otherwise>
</xsl:choose>
std:colorList being the tree fragment of course.
The above works fine, and is OK because the code is the same for the two alternatives but is not that large.
But for larger code fragments I wonder whether it is possible to avoid duplicating code by first declaring the variable based on the rtf, and then perform the code; something like
<xsl:variable name="colorList">
<xsl:choose>
<xsl:when test="function-available('msxsl:node-set')">
<xsl:copy-of select="msxsl:node-set($std:colorList)"/>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="$std:colorList"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:for-each select="$colorList/color">
tr.testid<xsl:value-of select="#testid"/> {
color:<xsl:value-of select="."/>;
}
</xsl:for-each>
But this does not work properly: MSXML complains about colorList not being a node-set, so it cannot be used in the xsl:for-each.
XSL transformation failed due to following error:
Expression must evaluate to a node-set.
-->$colorList<--/color
Note that in the working example, this error did not occur because of "copying" std:colorList into the colorList variable. Apparently it is an xsl parsing error, not a runtime one.
Should I use something else than xsl:copy-of? Or is there another way to achieve the same?
In case you wonder, std:colorList contents are as follows:
<std:colorList>
<color testid="111">#FF0000</color>
<color testid="999">#FFFF00</color>
</std:colorList>
Unfortunately in XSLT 1.0, when xsl:variable has contained instructions rather than a select attribute, the result is always an RTF. So your careful attempts to convert the RTF to a node-set come to nothing, because it's converted straight back again.
I'm afraid there's no clean workaround (other than moving to XSLT 2.0, of course). I would suggest structuring the code like this:
<xsl:choose>
<xsl:when test="function-available('msxsl:node-set')">
<xsl:apply-templates select="msxsl:node-set($std:colorList)/color" mode="z"/>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="$std:colorList/color" mode="z"/>
</xsl:otherwise>
</xsl:choose>
<xsl:template match="color" mode="z">
tr.testid<xsl:value-of select="#testid"/> {
color:<xsl:value-of select="."/>;
}
</xsl:template>
Just for the record, I add the final solution below. It is slightly different from what Michael proposed, adding a copy of the RTF to a variable before applying the template.
This is because otherwise MSXML still errors during xsl parsing (apparently it checks the apply-templates select value and concludes it is not correct when it is an RTF instead of a node-set. And, as Michael said, xsl:variable select attribute does just that: converting an RTF to a node-set.
<xsl:choose>
<xsl:when test="function-available('msxsl:node-set')">
<xsl:apply-templates select="msxsl:node-set($std:colorList)/color" mode="addTRclassToCSS"/>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="colorList" select="$std:colorList"/>
<xsl:apply-templates select="$colorList" mode="addTRclassToCSS"/>
</xsl:otherwise>
</xsl:choose>
<xsl:template match="color" mode="addTRclassToCSS">
tr.testid<xsl:value-of select="#testid"/> {
color:<xsl:value-of select="."/>;
}
</xsl:template>

How to store the current path in xsl?

I would like to store the path of the current node so I can reused it in an expression in XSLT. Is it possible?
<!-- . into $path? -->
<xsl:value-of select="$path" />
Hi, I would like to store the path of
the current node so I can reused it in
an expression in XSLT. Is it possible?
It is possible for any given node to construct an XPath expression that, when evaluated, selects exactly this node. In fact more than one XPath expression exists that selects the same node.
See this answer for the exact XSLT code that constructs such an XPath expression.
The problem is that this XPath expression cannot be evaluated during the same transformation in XSLT 1.0 or XSLT 2.0, unless the EXSLT extension function dyn:evaluate is used (and very few XSLT 1.0 processors implement dyn:evaluate() ).
What you want can be achieved in an easier way in XSLT using the <xsl:variable> instruction:
<xsl:variable name="theNode" select="."/>
This variable can be referenced anywhere in its scope as $theNode, and can be passed as parameter when applying or calling templates.
No, this is not possible with vanilla XSLT 1.0. There is no easy way to retrieve an XPath expression string for a given node, and there is definitely no way to evaluate a string that looks like XPath as if it was XPath.
There are extensions that support dynamic evaluation of XPath expressions, but these are not compatible with every XSLT processor.
In any case, if you provide more detail around what you are actually trying to do, there might be another way to do it.
As #Dimitre and #Tomalak have point out, I don't think it has some value in the same transformation to obtain a string representing an XPath expression for a given node, and then select the node "parsing" such string. I could see some value in performing those operations in different transformations.
Besides that, this stylesheet:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes"/>
<xsl:template match="/">
<xsl:for-each select=".|//node()|//#*">
<xsl:variable name="vPath">
<xsl:apply-templates select="." mode="getPath"/>
</xsl:variable>
<xsl:value-of select="concat($vPath,'
')"/>
<xsl:call-template name="select">
<xsl:with-param name="pPath" select="$vPath"/>
</xsl:call-template>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
<xsl:template match="/|node()|#*" mode="getPath" name="getPath">
<xsl:apply-templates select="parent::*" mode="getPath"/>
<xsl:text>/</xsl:text>
<xsl:choose>
<xsl:when test="self::*">
<xsl:value-of select="concat(name(),'[',
count(preceding-sibling::*
[name() =
name(current())]) + 1,
']')"/>
</xsl:when>
<xsl:when test="count(.|../#*)=count(../#*)">
<xsl:value-of select="concat('#',name())"/>
</xsl:when>
<xsl:when test="self::text()">
<xsl:value-of
select="concat('text()[',
count(preceding-sibling::text()) + 1,
']')"/>
</xsl:when>
<xsl:when test="self::comment()">
<xsl:value-of
select="concat('comment()[',
count(preceding-sibling::comment()) + 1,
']')"/>
</xsl:when>
<xsl:when test="self::processing-instruction()">
<xsl:value-of
select="concat('processing-instruction()[',
count(preceding-sibling::
processing-instruction()) + 1,
']')"/>
</xsl:when>
</xsl:choose>
</xsl:template>
<xsl:template name="select">
<xsl:param name="pPath"/>
<xsl:param name="pContext" select="/"/>
<xsl:param name="pInstruction" select="'value-of'"/>
<xsl:variable name="vPosition"
select="number(
substring-before(
substring-after($pPath,
'['),
']'))"/>
<xsl:variable name="vTest"
select="substring-before(
substring-after($pPath,
'/'),
'[')"/>
<xsl:variable name="vPath" select="substring-after($pPath,']')"/>
<xsl:choose>
<xsl:when test="$vPath">
<xsl:call-template name="select">
<xsl:with-param name="pPath" select="$vPath"/>
<xsl:with-param name="pContext"
select="$pContext/*[name()=$vTest]
[$vPosition]"/>
<xsl:with-param name="pInstruction"
select="$pInstruction"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="vContext"
select="$pContext/node()
[self::*[name()=$vTest]|
self::comment()[$vTest='comment()']|
self::text()[$vTest='text()']|
self::processing-instruction()
[$vTest =
'processing-instruction()']]
[$vPosition]|
$pContext[$pPath='/']|
$pContext/#*[name() =
substring($pPath,3)]
[not($vTest)]"/>
<xsl:choose>
<xsl:when test="$pInstruction='value-of'">
<xsl:value-of select="$vContext"/>
</xsl:when>
<xsl:when test="$pInstruction='copy-of'">
<xsl:copy-of select="$vContext"/>
</xsl:when>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
With this input:
<?somePI pseudoAttributes?>
<root>
<!-- This is a comment -->
<node attribute="Value">text</node>
</root>
Output:
/
text
/processing-instruction()[1]
pseudoAttributes
/root[1]
text
/root[1]/comment()[1]
This is a comment
/root[1]/node[1]
text
/root[1]/node[1]/#attribute
Value
/root[1]/node[1]/text()[1]
text