This is not really a question but an astonishing xslt2 experience that I like to share.
Take the snippet (subtract one set from another)
<xsl:variable name="v" as="node()*">
<e a="a"/>
<e a="b"/>
<e a="c"/>
<e a="d"/>
</xsl:variable>
<xsl:message select="$v/#a[not(.=('b','c'))]"/>
<ee>
<xsl:sequence select="$v/#a[not(.=('b','c'))]"/>
</ee>
What should I expect to get?
I expected a d at the console and
<ee>a d</ee>
at the output.
What I got is
<?attribute name="a" value="a"?><?attribute name="a" value="d"?>
at the console and
<ee a="d"/>
at the output. I should have known to take $v/#a as a sequence of attribute nodes to predict the output.
In order to get what I wanted, I had to convert the sequence of attributes to a sequence of strings like:
<xsl:variable name="w" select="$v/#a[not(.=('b','c'))]" as="xs:string*"/>
Questions:
Is there any use of sequences of attributes (or is it just an interesting effect of the node set concept)?
If so, would I be able to enter statically a sequence of attributes like I am able to enter a sequence of strings: ('a','b','c','d')
Is there any inline syntax to convert a sequence of attributes to a sequence of strings? (In order to achieve the same result omitting the variable w)
It seems to be an elegant way for creating attributes using xsl:sequence. Or would that be a misuse of xslt2, not covered by the standard?
As for "Is there any inline syntax to convert a sequence of attributes to a sequence of strings", you can simply add a step $v/#a[not(.=('b','c'))]/string(). Or use a for $a in $v/#a[not(.=('b','c'))] return string($a) and of course in XPath 3 $v/#a[not(.=('b','c'))]!string().
I am not sure what the question about the "use of sequences of attributes" is about, in particular as it then mentions the XPath 1 concept of node sets. If you want to write a function or template to return some original attribute nodes from an input then xsl:sequence allows that. Of course, inside a sequence constructor like the contents of an element, if you look at 10) in https://www.w3.org/TR/xslt20/#constructing-complex-content, in the end a copy of the attribute is created.
As for creating a sequence of attributes, you can't do that in XPath which can't create new nodes, you can however do that in XSLT:
<xsl:variable name="att-sequence" as="attribute()*">
<xsl:attribute name="a" select="1"/>
<xsl:attribute name="b" select="2"/>
<xsl:attribute name="c" select="3"/>
</xsl:variable>
then you can use it elsewhere, as in
<xsl:template match="/*">
<xsl:copy>
<element>
<xsl:sequence select="$att-sequence"/>
</element>
<element>
<xsl:value-of select="$att-sequence"/>
</element>
</xsl:copy>
</xsl:template>
and will get
<example>
<element a="1" b="2" c="3"/>
<element>1 2 3</element>
</example>
http://xsltfiddle.liberty-development.net/jyyiVhg
XQuery has a more compact syntax and in contrast to XPath allows expressions to create new nodes:
let $att-sequence as attribute()* := (attribute a {1}, attribute b {2}, attribute c {3})
return
<example>
<element>{$att-sequence}</element>
<element>{data($att-sequence)}</element>
</example>
http://xqueryfiddle.liberty-development.net/948Fn56
Related
I have a simple XSLT variable setup like this:
<xsl:variable name="testId" select="1"/>
Then I try to setup the same variable conditionally like this:
<xsl:variable name="testId">
<xsl:choose>
<xsl:when test="$var='true'"><xsl:value-of select="1"/></xsl:when>
<xsl:when test="$var='false'"><xsl:value-of select="2"/></xsl:when>
</xsl:choose>
</xsl:variable>
I use this variable to address some specific table cells like this td[$testId] (i.e. td[1], td[2]).
I don't know what I am doing wrong but with static declaration it works just fine, while dynamic declaration always returns an empty value (just td) without numbers. What is wrong with the second option?
You can use <xsl:variable name="testId" select="if ($var) then 1 else 2"/> or, if that $var is really bound to a string and not a boolean value then <xsl:variable name="testId" select="if ($var = 'true') then 1 else 2"/>.
Your approach is flawed as it uses xsl:value-of which always creates a text node with the value(s) selected in the select expression and because your use of <xsl:variable name="varName">... content ...</xsl:variable> creates a document fragment node containing the content created inside.
To avoid creating a document node you can use the as attribute with a sequence type e.g. <xsl:variable name="testId" as="xs:integer"><xsl:choose><xsl:when test="$var"><xsl:sequence select="1"/></xsl:when><xsl:otherwise><xsl:sequence select="2"/></xsl:otherwise></xsl:choose></xsl:variable> gives you a variable with an integer value.
The use of select with an if () then .. else .. XPath expression seems much more succinct.
Following on from my earlier question, the p elements I want to apply the answer to are actually in a result tree fragment.
How do I make the key function:
<xsl:key name="kRByLevelAndParent" match="p"
use="generate-id(preceding-sibling::p
[not(#ilvl >= current()/#ilvl)][1])"/>
match against p elements in a result tree fragment?
In that answer the key is used via apply-templates:
<xsl:template match="/*">
<list>
<item>
<xsl:apply-templates select="key('kRByLevelAndParent', '')[1]" mode="start">
<xsl:with-param name="pParentLevel" select="$pStartLevel"/>
<xsl:with-param name="pSiblings" select="key('kRByLevelAndParent', '')"/>
</xsl:apply-templates>
</item>
</list>
</xsl:template>
I'd like to pass my result tree fragment as a parameter, and have the key match p elements in that.
Is this the right way to think about it?
There are no result tree fragments in XSLT 2.0 and later, you simply have temporary trees. As for keys, they apply to each document and the key function simply has a third argument to pass in the root node or subtree to search so assuming you have your temporary tree $var you can use key('keyname', key-value-expression, $var) to find elements in $var.
Given this source document:
<things>
<thing><duck>Eider</duck></thing>
<thing><duck>Mallard</duck></thing>
<thing><duck>Muscovy</duck></thing>
</things>
I require the following output
Fat Eider, Fat Mallard, Fat Muscovy
which I can indeed get with this XSL transform:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema" >
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:value-of separator=", ">
<xsl:apply-templates select="//duck"/>
</xsl:value-of>
</xsl:template>
<xsl:template match="duck" as="xs:string">
<xsl:value-of select="concat('Fat ', .)"/>
</xsl:template>
</xsl:stylesheet>
However, I have three questions:
Question 1. (specific)
If I remove as="xs:string" from the duck template, I get the following output:
Fat EiderFat MallardFat Muscovy
Why? My understanding is that in XSLT 2.0 the result of xsl:apply-templates is always a sequence, and that xsl:value-of inserts its separator between the items in the sequence. So why does the sequence seem to "collapse" when the template has no as attribute? Bonus points for pointing me towards appropriate pages of Michael Kay's excellent "XSLT 2.0 and XPath 2.0, 4th Edition" book.
Question 2. (vague!)
As a novice user of XSLT, it seems to me that there are probably many ways to solve this problem. Can you put forward a good solution that takes a different approach? How do you choose between approaches?
Question 3.
Debugging. Can you recommend how to dump out intermediate results that would indicate the difference between the presence and the absence of the as attribute to the template?
See http://www.w3.org/TR/xslt20/#value-of which says
The string value of the new text node may be defined either by using
the select attribute, or by the sequence constructor (see 5.7 Sequence
Constructors) that forms the content of the xsl:value-of element.
These are mutually exclusive, and one of them must be present. The way
in which the value is constructed is specified in 5.7.2 Constructing
Simple Content.
So we need to look at http://www.w3.org/TR/xslt20/#constructing-simple-content and that says "2. Adjacent text nodes in the sequence are merged into a single text node.". So that is what is happening without the as="xs:string", the sequence constructor inside the xsl:value-of creates adjacent text nodes which are merged into a single text node. If you have as="xs:string" or did <xsl:sequence select="concat('Fat ', .)"/> the sequence constructor is of a sequence of primitive string values.
I'm using xpath2's index-of value to return the index of current() within a sorted sequence of nodes. Using SAXON, the sorted sequence of nodes are unique, yet index-of returns a sequence of two values.
This does not happen all the time, just very occasionally, but not for any reason I can find. Can someone please explain what is going on?
I have worked up a minimal example based on an example of data that routines gives this odd behavior.
The source data is:
<data>
<student userID="1" userName="user1"/>
<session startedOn="01/16/2012 15:01:18">
</session>
<session startedOn="11/16/2011 13:31:33">
</session>
</data>
My xsl document puts the session nodes into a sorted sequence $orderd at the very top of the root template:
<xsl:template match="/">
<xsl:variable name="nodes" as="node()*" select="/data/session"></xsl:variable>
<xsl:variable name="orderd" as="node()*">
<xsl:for-each select="$nodes">
<xsl:sort select="xs:dateTime(xs:dateTime(concat(substring(normalize-space(#startedOn),7,4),'-',substring(normalize-space(#startedOn),1,2),'-',substring(normalize-space(#startedOn),4,2),'T',substring(normalize-space(#startedOn),12,8)))
)" order="ascending"/>
<xsl:sequence select="."/>
</xsl:for-each>
</xsl:variable>
Since the nodes were already ordered by #startOn but in the opposite order, the sequence $orderd should be the same as document-ordered sequence $nodes, except in reverse order.
When I create output using a for-each statement, I find that somehow the two nodes are seen as identical when tested using index-of.
The code below is used to output data (and comes immediately after the chunk above):
<output>
<xsl:for-each select="$nodes">
<xsl:sort select="position()" order="descending"></xsl:sort>
<xsl:variable name="index" select="index-of($orderd,current())" as="xs:integer*"></xsl:variable>
<xsl:variable name="pos" select="position()"></xsl:variable>
<session reverse-documentOrder="{$pos}" sortedOrder="{$index}"/>
</xsl:for-each>
</output>
As the output (shown below) indicates, the index-of function is returning the sequence (1,2), meaning that it sees both nodes as identical. I have checked the expression used to sort the values, and it produces distinct and well-formed date-Time strings.
<output>
<session reverse=documentOrder="1"
sortedOrder="1 2"/>
<session reverse-documentOrder="2"
sortedOrder="1 2"/>
</output>
Not relying on the generate-id() function, which is XSLT function, but not XPath function, one can write a simple index-of() function that operates on node identity:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:my="my:my">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:variable name="vNum3" select="/*/*[3]"/>
<xsl:variable name="vSeq" select="/*/*[1], /*/*[3], /*/*[3]"/>
<xsl:template match="/">
<xsl:sequence select="my:index-of($vSeq, $vNum3)"/>
</xsl:template>
<xsl:function name="my:index-of" as="xs:integer*">
<xsl:param name="pSeq" as="node()*"/>
<xsl:param name="pNode" as="node()"/>
<xsl:for-each select="$pSeq">
<xsl:if test=". is $pNode">
<xsl:sequence select="position()"/>
</xsl:if>
</xsl:for-each>
</xsl:function>
</xsl:stylesheet>
when this transformation is applied on the following XML document:
<nums>
<num>01</num>
<num>02</num>
<num>03</num>
<num>04</num>
<num>05</num>
<num>06</num>
<num>07</num>
<num>08</num>
<num>09</num>
<num>10</num>
</nums>
the wanted, correct result is returned:
2 3
Explanation: Use of the is operator.
The documentation http://www.w3.org/TR/xpath-functions/#func-index-of of index-of says "The items in the sequence $seqParam are compared with $srchParam under the rules for the eq operator. Values of type xs:untypedAtomic are compared as if they were of type xs:string.". So you are trying to compare untyped element nodes and that means they are compared as strings and both session elements have the same white space only string contents. That way both are compared as equal.
I am not sure what to suggest as I am not sure what you want to achieve but I hope the above explains the result you get.
I'm trying to create an xslt function that dynamically 'matches' for an element. In the function, I will pass two parameters - item()* and a comma delimited string. I tokenize the comma delimited string in a <xsl:for-each> select statement and then do the following:
select="concat('$di:meta[matches(#domain,''', current(), ''')][1]')"
Instead of the select statement 'executing' the xquery, it is just returning the string.
How can I get it to execute the xquery?
Thanks in advance!
The problem is that you are wrapping too much of the expression in the concat() function. When that evaluates, it returns a string that would be the XPath expression, rather than evaluating the XPath expression that uses the dynamic string for the REGEX match expression.
You want to use:
<xsl:value-of select="$di:meta[matches(#domain
,concat('.*('
,current()
,').*')
,'i')][1]" />
Although, since you are now evaluating each term separately,rather than having each of those terms in a single regex pattern and selecting the first one, it will now return the first result from each match, rather than the first one from the sequence of matched items. That may or may not be what you want.
If you want the first item from the sequence of matched items, you could do something like this:
<!--Create a variable and assign a sequence of matched items -->
<xsl:variable name="matchedMetaSequence" as="node()*">
<!--Iterate over the sequence of names that we want to match on -->
<xsl:for-each select="tokenize($csvString,',')">
<!--Build the sequence(list) of matched items,
snagging the first one that matches each value -->
<xsl:sequence select="$di:meta[matches(#domain
,concat('.*('
,current()
,').*')
,'i')][1]" />
</xsl:for-each>
</xsl:variable>
<!--Return the first item in the sequence from matching on
the list of domain regex fragments -->
<xsl:value-of select="$matchedMetaSequence[1]" />
You could also put this into a custom function like this:
<xsl:function name="di:findMeta">
<xsl:param name="meta" as="element()*" />
<xsl:param name="names" as="xs:string" />
<xsl:for-each select="tokenize(normalize-space($names),',')">
<xsl:sequence select="$meta[matches(#domain
,concat('.*('
,current()
,').*')
,'i')][1]" />
</xsl:for-each>
</xsl:function>
and then use it like this:
<xsl:value-of select="di:findMeta($di:meta,'foo,bar,baz')[1]"/>