In XSLT 2.0 I am handling a string delimited by ~. There are times that the tokenized results contain an instance of 'nothing' between two ~. I try to test for this using empty()
<xsl:for-each select="tokenize($list_of_items,'~')">
<xsl:if test="not(empty(.))">
...do something here...
</xsl:if>
</xsl:for-each>
...which doesn't work. What is the correct way to test for nothing/empty/blank value in a tokenized list?
tokenize gives you a sequence of strings, if you have an input with two adjacent separator characters (e.g. tokenize('foo~~bar', '~')) then you get an empty string so tokenize($list_of_items,'~')[not(. = '')] should do to exclude empty strings.
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.
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
I am generating an ID using generate-id(), I convert this ID into a numbers using string-to-codepoints.
How can I remove the spaces from the resulting number?
So e.g. the resulting codepoints are "17 28 39 28", but I need "17283928".
Translate doesn't work, because it expects a string. And I cant convert the number to a string, because string() cannot handle the spaces.
How can I achieve this?
Based on your comment, you used string-to-codepoints(generate-id()) in an attribute value template, in an attribute value template if the expression evaluates to a sequence then a space separated list of values of the sequence is inserted. You either need to use string-join on the sequence to construct a single string or you need to construct the string outside of an attribute value template, as <xsl:value-of select="string-to-codepoints(generate-id())" separator=""/> allows.
Here is an example using string-join:
<xsl:template match="foo">
<bar id="{string-join(for $n in string-to-codepoints(generate-id()) return string($n), '')}" />
</xsl:template>
which will construct a result element alike <bar id="1004910149"/> without spaces in the attribute value.
(Not an answer but too long for a comment).
Interesting supplementary: does this algorithm guarantee unique keys?
Could there be two distinct strings such that the function string-join(string-to-codepoints($x)!string(), '') produces the same string of digits?
I think you're in luck, because generate-id() is guaranteed to produce strings consisting only of ASCII alphanumeric characters. This means that all the characters must have codepoints in the range 48 to 122, which means that every codepoint converts to either 2 or 3 digits, and by looking at the first digit you can tell whether it's a 2-digit or 3-digit sequence.
Before I worked this out, I was going to suggest that you pad the codepoints with zeroes to make all the sequences uniform length.
I'm using XSL's convenience functions for comparisons, gt, lt, ge, le, eq.
I understand these functions won't promote a string to a numerical value when performing comparisons, however I need that cast to be made, and I don't want to clutter my code with lines like
<xsl:when test="xs:integer($variable) lt 250" >
I'd rather make that cast like this (hypothetical of course)
<xsl:variable name="variable" type="xs:integer">
So, is there a means of explicitly casting variable as an numerical type when it is declared/created ?
<xsl:when test="xs:integer($variable) lt 250" >
I'd rather make that cast like this (hypothetical of course)
<xsl:variable name="variable" type="xs:integer">
Use the as attribute -- its purpose is exactly to specify the type of a variable, parameter, template or a function:
<xsl:variable name="variable" as="xs:integer"
select="some-integer-type-expression">
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]"/>