Setting an XSL variable to a node-set using conditions - xslt

This question is very similar to XSL store node-set in variable. The major difference is that if XPath does not find a node which matches the filter, I would like to return the first unfiltered result.
The code I have here works, but I feel like it is a hack and not good XSL style. In this case, each chapter node is identified by a string id. The variable showChapter is the string identifying a chapter. If no chapter is found with this id attribute, I want to return the first chapter.
Relevant code:
<xsl:param name="showChapter" />
<!-- if $showChapter does not match any chapter id attribute,
set validShowChapter to id of first chapter.
-->
<xsl:variable name="validShowChapter">
<xsl:choose>
<xsl:when test="/book/chapter[#id=string($showChapter)][position()=1]">
<xsl:value-of select="$showChapter" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="/book/chapter[position()=1]/#id" />
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<!-- I want $chapter to be a valid node-set so I can use it in
XPath select statements in my templates
-->
<xsl:variable
name="chapter"
select="/book/chapter[#id=string($validShowChapter)][position()=1]"
>
Is this approach as poor of a hack as I think it is, and if so could you point me to a better solution? I am using XSLT 1.0 processed by PHP5's XSLTProcessor, but XSLT 2.0 solutions are welcome.

The following should work. A lot of the usage of position() and string() in your example were unneeded, btw:
<xsl:param name="showChapter" />
<xsl:variable name="foundChapter" select="/book/chapter[#id = $showChapter]" />
<!-- Will select either the first chapter in $foundChapter, or
the first chapter available if $foundChapter is empty -->
<xsl:variable name="chapter"
select="($foundChapter | /book/chapter[not($foundChapter)])[1]" />

Related

Umbraco - Creating variable of nodes - how to check for missing value

I am using Umbraco 4.5 (yes, I know I should upgrade to 7 now!)
I have an XSLT transform which builds up a list of products which match user filters.
I am making an XSL:variable which is a collection of products from the CMS database.
Each product has several Yes/No properties (radio buttons). Some of these haven't been populated however.
As a result, the following code breaks occasionally if the dataset includes products which don't have one of the options populated with an answer.
The error I get when it transforms the XSLT is "Value was either too large or too small for an Int32". I assume this is the value being passed into the GetPreValueAsString method.
How do I check to see if ./option1 is empty and if so, use a specific integer, otherwise use ./option1
<xsl:variable name="nodes"
select="umbraco.library:GetXmlNodeById(1098)/*
[#isDoc and string(umbracoNaviHide) != '1' and
($option1= '' or $option1=umbraco.library:GetPreValueAsString(./option1)) and
($option2= '' or $option2=umbraco.library:GetPreValueAsString(./option2)) and
($option3= '' or $option3=umbraco.library:GetPreValueAsString(./option3)) and
($option4= '' or $option4=umbraco.library:GetPreValueAsString(./option4))
]" />
Note: you tagged your question as XSLT 2.0, but Umbraco does not use XSLT 2.0, it is (presently) stuck with XSLT 1.0.
$option1= '' or $option1=umbraco.library:GetPreValueAsString(./option1)
There can be multiple causes for your error. A processor is not required to process the or-expression left-to-right or right-to-left, and it is even allowed to always evaluate both expressions, even if the first is true (this is comparable with bit-wise operators (unordered) in other languages, whereas boolean operators (ordered) in those languages typically use early breakout).
Another error can be that your option value in the context node is not empty and is not an integer or empty, in which case your code will always return an error.
You could expand your expression by testing ./optionX, but then you still have the problem of order of evaluation.
That said, how can you resolve it and prevent the error from arising? In XSLT 1.0, this is a bit clumsy (i.e., you cannot define functions and cannot use sequences), but here's one way to do it:
<xsl:variable name="pre-default-option">
<default>1</default>
<default>2</default>
<default>3</default>
<default>4</default>
</xsl:variable>
<xsl:variable name="default-option"
select="exslt:node-set($pre-default-option)" />
<xsl:variable name="pre-selected-option">
<option><xsl:value-of select="$option1" /></option>
<option><xsl:value-of select="$option2" /></option>
<option><xsl:value-of select="$option3" /></option>
<option><xsl:value-of select="$option4" /></option>
</xsl:variable>
<xsl:variable name="selected-option" select="exslt:node-set($pre-selected-option)" />
<xsl:variable name="pre-process-nodes">
<xsl:variable name="selection">
<xsl:apply-templates
select="umbraco.library:GetXmlNodeById(1098)/*"
mode="pre">
<xsl:with-param name="opt-no" select="1" />
</xsl:apply-templates>
</xsl:variable>
<!-- your original code uses 'and', so is only true if all
conditions are met, hence there must be four found nodes,
otherwise it is false (i.e., this node set will be empty) -->
<xsl:if test="count($selection) = 4">
<xsl:copy-of select="$selection" />
</xsl:if>
</xsl:variable>
<!-- your original variable, should now contain correct set, no errors -->
<xsl:variable name="nodes" select="exslt:node-set($pre-process-nodes)"/>
<xsl:template match="*[#isDoc and string(umbracoNaviHide) != '1']" mode="pre">
<xsl:param name="opt-no" />
<xsl:variable name="option"
select="$selected-option[. = string($opt-no)]" />
<!-- gets the child node 'option1', 'option2' etc -->
<xsl:variable
name="pre-ctx-option"
select="*[local-name() = concat('option', $opt-no)]" />
<xsl:variable name="ctx-option">
<xsl:choose>
<!-- empty option param always allowed -->
<xsl:when test="$option = ''">
<xsl:value-of select="$option"/>
</xsl:when>
<!-- if NaN or 0, this will return false -->
<xsl:when test="number($pre-ctx-option)">
<xsl:value-of select="$default-option[$opt-no]"/>
</xsl:when>
<!-- valid number (though you could add a range check as well) -->
<xsl:otherwise>
<xsl:value-of select="umbraco.library:GetPreValueAsString($pre-ctx-option)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<!-- prevent eternal recursion -->
<xsl:if test="4 >= $opt-no">
<xsl:apply-templates select="self::*" mode="pre">
<xsl:with-param name="opt-no" select="$opt-no + 1" />
</xsl:apply-templates>
<!-- the predicate is now ctx-independent and just true/false
this copies nothing if the conditions are not met -->
<xsl:copy-of select="self::*[$option = $ctx-option]" />
</xsl:if>
</xsl:template>
<xsl:template match="*" mode="pre" />
Note (1): I have written the above code by hand, tested only for syntax errors, I couldn't test it because you didn't provide an input document to test it against. If you find errors, by all means, edit my response so that it becomes correct.
Note (2): the above code generalizes working with the numbered parameters. By generalizing it, the code becomes a bit more complicated, but it becomes easier to maintain and to extend, and less error-prone for copy/paste errors.

XSLT for-each, iterating through text and nodes

I'm still learning XSLT, and have a question about the for-each loop.
Here's what I have as far as XML
<body>Here is a great URL<link>http://www.somesite.com</link>Some More Text</body>
What I'd like is if the for-each loop iterate through these chunks
1. Here is a great URL
2. http://www.somesite.com
3. Some More Text
This might be simple, or impossible, but if anyone can help me out I'd appreciate it!
Thanks,
Michael
You should be able to do so with something like the following:
<xsl:for-each select=".//text()">
<!-- . will have the value of each chunk of text. -->
<someText>
<xsl:value-of select="." />
</someText>
</xsl:for-each>
or this may be preferable because it allows you to have a single template that you can invoke from multiple different places:
<xsl:apply-templates select=".//text()" mode="processText" />
<xsl:template match="text()" mode="processText">
<!-- . will have the value of each chunk of text. -->
<someText>
<xsl:value-of select="." />
</someText>
</xsl:for-each>

XSLT - Preserving disable-output-escaping in a copy-of

I seem to be having an issue preserving the disable-output-escaping when using that value inside of an xsl:copy-of.
Here's my code:
<xsl:call-template name="Display">
<xsl:with-param name="text">
<xsl:value-of select="content" disable-output-escaping="yes" />
</xsl:with-param>
</xsl:call-template>
<xsl:template name="Display">
<xsl:param name="text" />
<span><xsl:copy-of select="$text" /></span>
</xsl:template>
Any special characters that were kept as-is from the xsl:value-of statement are escaped when they're used in the xsl:copy-of statement.
For example:
<xsl:value-of select="$text" disable-output-escaping="yes"> will display this: è
<xsl:copy-of select="$text"> will display &#232
I'd like to know if there is any way around this?
As per Spec, the disable-output-escaping attribute can be specified only on <xsl:value-of> and the <xsl:text> instructions.
You need the DOE only on the xslt instruction that actually outputs the value, not on one that sets a parameter value.
Solution:
Replace:
<span><xsl:copy-of select="$text"/></span>
with:
<span><xsl:value-of select="$text" disable-output-escaping="yes"/></span>
Do note: Typically one should avoid using DOE, as it breaks the XSLT architectural model and usually isn't needed. Also, the DOE feature isn't mandatory and not all XSLT 1.0 processors support it.
Note 2: You don't actually need DOE in your case at all. The output from the XSLT transformation should be displayed by the browser as expected.
disable-output-escaping controls the action of the serializer when handed a text node. It's meaningless when the text node isn't being handed to a serializer, for example when it is added to a temporary tree.

XSLT: pass value from one for-each match to the next

I'm using the following to match all <section>s with a revision attribute set. <section>can appear at many different levels of the document tree, always contained within <chapter>s.
<xsl:for-each select="//section[#revision]">
<!-- Do one thing if this is the first section
matched in this chapter -->
<!-- Do something else if this section is in the same
chapter as the last section matched -->
</xsl:for-each>
As the comments say, I need to make each for-each iteration aware of the chapter to which the previous matched section belonged. I know that <xsl:variable>s are actually static once set, and that <xsl:param> only applies to calling templates.
This being Docbook, I can retreive a section's chapter number with:
<xsl:apply-templates select="ancestor::chapter[1]" mode="label.markup" />
but I think it can be done with purely XPath.
Any ideas? Thanks!
Not sure if I unterstood your requirements 100%, but…
<xsl:variable name="sections" select="//section[#revision]" />
<xsl:for-each select="$sections">
<xsl:variable name="ThisPos" select="position()" />
<xsl:variable name="PrevMatchedSection" select="$sections[$ThisPos - 1]" />
<xsl:choose>
<xsl:when test="
not($PrevMatchedSection)
or
generate-id($PrevMatchedSection/ancestor::chapter[1])
!=
generate-id(ancestor::chapter[1])
">
<!-- Do one thing if this is the first section
matched in this chapter -->
</xsl:when>
<xsl:otherwise>
<!-- Do something else if this section is in the same
chapter as the last section matched -->
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
However, I suspect this whole thing can be solved more elegantly with a <xsl:template> / <xsl:apply-templates> approach. But without seeing your input and expected output this is hard to say.
position() will return your position within the current for-each iteration. The first iteration, IIRC, will return 0, so testing for "position()=0" will tell you if you are in the first iteration.

XSL: How best to store a node in a variable and then use it in future xpath expressions?

I need to be able to store a node set in variable and then perform more filting/sorting on it afterward. All the examples I've seen of this involve either using XSL2 or extensions neither of which are really an option.
I've a list of hotels in my XML doc that can be sorted/filtered and then paged through 5 at a time. I'm finding though I'm repeating alot of the logic as currently I've not found a good way to store node-sets in xsl variable and then use xpath on them for further filtering/sorting.
This is the sort of thing I'm after (excuse the code written of the top of my head so might not be 100%):
<xsl:variable name="hotels" select="/results/hotels[active='true']" />
<xsl:variable name="3_star_or_less" select="/results/hotels[number(rating) <= 3]" />
<xsl:for-each select="3_star_or_less">
<xsl:sort select="rating" />
</xsl:for-each>
Has anyone got an example of how best to do this sort of thing?
Try this example:
<xsl:variable name="hotels" select="/results/hotels[active='true']" />
<xsl:variable name="three_star_or_less"
select="$hotels[number(rating) <= 3]" />
<xsl:for-each select="$three_star_or_less">
<xsl:sort select="rating" />
<xsl:value-of select="rating" />
</xsl:for-each>
There is no problem storing a node-set in a variable in XSLT 1.0, and no extensions are needed. If you just use an XPath expression in select attribute of xsl:variable, you'll end up doing just that.
The problem is only when you want to store the nodes that you yourself had generated in a variable, and even then only if you want to query over them later. The problem here is that nodes you output don't have type "node-set" - instead, they're what is called a "result tree fragment". You can store that to a variable, and you can use that variable to insert the fragment into output (or another variable) later on, but you cannot use XPath to query over it. That's when you need either EXSLT node-set() function (which converts a result tree fragment to a node-set), or XSLT 2.0 (in which there are no result tree fragments, only sequences of nodes, regardless of where they come from).
For your example as given, this doesn't seem to be a problem. Rubens' answer gives the exact syntax.
Another note, if you want to be able to use the variable as part of an XPath statement, you need to select into the variable with <xsl:copy-of select="."/> instead of <xsl:value-of select="."/>
value-of will only take the text of the node and you wont be able to use the node-set function to return anything meaningful.
<xsl:variable name="myStringVar">
<xsl:value-of select="."/>
</xsl:variable>
<!-- This won't work: -->
<Output>
<xsl:value-of select="node-set($myStringVar)/SubNode" />
</Output>
<xsl:variable name="myNodeSetVar">
<xsl:copy-of select="."/>
</xsl:variable>
<!-- This will work: -->
<Output>
<xsl:value-of select="node-set($myNodeSetVar)/SubNode" />
</Output>