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.
Related
I have searched, but might have missed something obvious just because I don't know what to search for. And I found it hard to explain my question as a simple question, so let me explain: I am using the following code (XSLT 1.0 & XPath) to check if the parent to this node belongs to the last grand-parent node or not:
<xsl:when test="count(parent::*/preceding-sibling::*)+1 = count(parent::*/parent::*/*)">
It works exactly like I want. What I would like though is to make it more general in one way or the other, to make it work with even more parent nodes. I can add another template match and add another test:
<xsl:when test ="count(parent::*/parent::*/preceding-sibling::*)+1 = count(parent::*/parent::*/*)">
Is there a way to add "parent::*/" in a recursive template loop instead of creating a lot of specific template matches? Or should I work out a better XPath code altogether?
Please note: I want to do a check for every level of parent nodes. Is the parent the last of the parents, is the grand-parent the last of the grand-parents, etc.
For clarity's sake, I use it like this:
<xsl:choose>
<xsl:when test="count(parent::*/preceding-sibling::*)+1 = count(parent::*/parent::*/*)">
<!-- show image A -->
</xsl:when>
<xsl:otherwise>
<!-- show image B -->
</xsl:otherwise>
</xsl:choose>
<xsl:when test="count(parent::*/preceding-sibling::*)+1 = count(parent::*/parent::*/*)">
can be simplified to:
<xsl:when test="not(../following-sibling::*)">
In plain English "my parent does not have a following element sibling".
That would be easily modifiable to:
<xsl:when test="not(../../following-sibling::*)">
In plain English "my parent's parent does not have a following element sibling". Etc.
To check all ancestors at the same time:
<xsl:when test="not(ancestor::*/following-sibling::*)">
In plain English "none of my ancestors has a following element sibling".
To check parent and grandparent at the same time:
<xsl:when test="not(ancestor::*[position() <= 2]/following-sibling::*)">
In plain English "none of my two closest ancestors has a following element sibling".
EDIT To check all ancestors individually, either use a recursive template (advantage: the position of the inner <xsl:apply-templates> determines if you effectively go up or down the list of ancestors):
<xsl:template match="*" mode="line-img">
<xsl:if test="following-sibling::*">
<!-- show image A -->
</xsl:if>
<xsl:if test="not(following-sibling::*)">
<!-- show image B -->
</xsl:if>
<xsl:apply-templates select=".." mode="line-img" />
</xsl:template>
<!-- and later... -->
<xsl:apply-templates select=".." mode="line-img" />
...or a simple for-each loop (always works in document order):
<xsl:for-each select="ancestor::*">
<xsl:if test="following-sibling::*">
<!-- show image A -->
</xsl:if>
<xsl:if test="not(following-sibling::*)">
<!-- show image B -->
</xsl:if>
</xsl:for-each>
...or, to be completely idiomatic (always works in document order):
<xsl:template match="*" mode="line-img">
<xsl:if test="following-sibling::*">
<!-- show image A -->
</xsl:if>
<xsl:if test="not(following-sibling::*)">
<!-- show image B -->
</xsl:if>
</xsl:template>
<!-- and later... -->
<xsl:apply-templates select="ancestor::*" mode="line-img" />
I have a parameter in my XSLT which usually is a proper set of nodes that I apply templates on.
<apply-templates select="$conferences" />
However, sometimes something goes wrong and it comes in as a string. In this case I just want to skip applying templates. But what is the proper way of checking this? I could check that it's not a string of course, but how can I check that the parameter is... "templatable"?
<if test=" ? ">
<apply-templates select="$conferences" />
</if>
Since you're in XSLT 2.0 you can simply do
<xsl:if test="$conferences instance of node()*">
You can do:
<apply-templates select="$conferences/*" />
Which will only apply if there is an XML in it. Strings will not be applied.
If you want to do a condition up front, do something like:
<xsl:choose>
<xsl:when test="count($conferences/*) > 0"> <!-- it is XML -->
<xsl:apply-templates select="$conferences/*" />
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="$conferences" /> <!-- it is not XML -->
</xsl:otherwise>
</xsl:choose>
I'm trying to understand XSL's handling of for-each. Initially I wrote a recursive choose-values function (filters out all strings that do not match a regexp pattern) as follows:
<!-- Ver.1 -->
<xsl:function name="choose-values">
<xsl:param name="values" />
<xsl:param name="pattern" as="xs:string" />
<xsl:choose>
<xsl:when test="count($values) = 0" >
<xsl:sequence select="()" />
</xsl:when>
<xsl:otherwise>
<xsl:variable name="tail"
select="choose-values(subsequence($values, 2), $pattern)" />
<xsl:variable name="value" select="$values[1]" />
<xsl:sequence select="if (matches($value, $pattern))
then ($value, $tail)
else $tail" />
</xsl:otherwise>
</xsl:choose>
</xsl:function>
Then I came across <xsl:for-each> and rewrote it as follows:
<!-- Ver.2 -->
<xsl:function name="choose-values">
<xsl:param name="values" />
<xsl:param name="pattern" as="xs:string" />
<xsl:for-each select="$values">
<xsl:if test="matches(., $pattern)">
<xsl:sequence select="." />
</xsl:if>
</xsl:for-each>
</xsl:function>
Are these two versions equivalent? (my tests indicate so). Am I missing some edge-cases in Ver.2?
Just to make things clear, this is not a homework question. I'm just trying to understand the differences (if any) by using a simple example.
Yes, the two specimens appear to be equivalent. Generally, you don't need recursion in XSLT unless the processing of one item in the sequence depends in some way on the processing of previous items. If each item is processed independently of the others, then you can use filter expressions or mapping expressions, of which xsl:for-each is one example. An advantage of doing it this way (apart from readability of the code) is that you don't impose an order of processing, which gives the optimiser more freedom to work its magic.
Are these two versions equivalent? (my tests indicate so). Am I
missing some edge-cases in Ver.2?
They seem to produce the same results -- it is difficult to say, because both code snippets are unnecessarily complicated.
This can be done simply by:
<xsl:sequence select="$values[matches(., $pattern)]"/>
Apart from rewriting a lot of XSLT code (which I'm not going to do), is there a way to find the position of an element within its parent, when the context is arbitrarily set to something else? Here's an example:
<!-- Here are my records-->
<xsl:for-each select="/path/to/record">
<xsl:variable name="record" select="."/>
<!-- At this point, I could use position() -->
<!-- Set the context to the current record -->
<xsl:for-each select="$record">
<!-- At this point, position() is meaningless because it's always 1 -->
<xsl:call-template name="SomeTemplate"/>
</xsl:for-each>
</xsl:for-each>
<!-- This template expects the current context being set to a record -->
<xsl:template name="SomeTemplate">
<!-- it does stuff with the record's fields -->
<xsl:value-of select="SomeRecordField"/>
<!-- How to access the record's position in /path/to or in any other path? -->
</xsl:template>
NOTE: This is a simplified example. I have several constraints keeping me from implementing obvious solutions, such as passing new parameters to SomeTemplate, etc. I can really only modify the internals of SomeTemplate.
NOTE: I'm using Xalan 2.7.1 with EXSLT. So those tricks are available
Any ideas?
You could use
<xsl:value-of select="count(preceding-sibling::record)" />
or even, generically,
<xsl:value-of select="count(preceding-sibling::*[name() = name(current())])" />
Of course this approach will not work if you process a list of nodes that is not uniform, i.e.:
<xsl:apply-templates select="here/foo|/somewhere/else/bar" />
Position information is lost in such a case, unless you store it in a variable and pass that to the called template:
<xsl:variable name="pos" select="position()" />
<xsl:for-each select="$record">
<xsl:call-template name="SomeTemplate">
<xsl:with-param name="pos" select="$pos" />
</xsl:call-template>
</xsl:for-each>
but obviously that would mean some code rewriting, which I realize you want to avoid.
Final hint: position() does not tell you the position of the node within its parent. It tells you the position of the current node relative to the list of nodes you are processing right now.
If you only process (i.e. "apply templates to" or "loop over") nodes within one parent, this happens to be the same thing. If you don't, it's not.
Final hint #2: This
<xsl:for-each select="/path/to/record">
<xsl:variable name="record" select="."/>
<xsl:for-each select="$record">
<xsl:call-template name="SomeTemplate"/>
</xsl:for-each>
</xsl:for-each>
is is equivalent to this:
<xsl:for-each select="/path/to/record">
<xsl:call-template name="SomeTemplate"/>
</xsl:for-each>
but the latter works without destroying the meaning of position(). Calling a template does not change context, so . will refer to the correct node withing the called template.
I working on a site which some if/or statements in XSL and being a little unfamilar with the language i'm not certain how to accomplish:
if [condion one is met] or [condition two is met] then do [action] otherwise do [alternative action]
can anyone offer some examples?
Thanks in advance!
Conditionals in XSLT are either an unary "if":
<xsl:if test="some Boolean condition">
<!-- "if" stuff (there is no "else" here) -->
</xsl:if>
or more like the switch statement of other languages:
<xsl:choose>
<xsl:when test="some Boolean condition">
<!-- "if" stuff -->
</xsl:when>
<xsl:otherwise>
<!-- "else" stuff -->
</xsl:otherwise>
</xsl:choose>
where there is room for as many <xsl:when>s as you like.
Every XPath expression can be evaluated as a Boolean according to a set of rules. These (for the most part) boil down to "if there is something -> true" / "if there is nothing -> false"
the empty string is false
0 is false (so is NaN)
the empty node set is false
the result of false() is false
every other literal value is true (most notably: 'false' is true and '0' is true)
the result of expressions is evaluated with said rules (no surprise here)
Edit: There is of course a more advanced (and more idiomatic) method to control program flow, and that's template matching:
<xsl:template match="node[contains(., 'some text')]">
<!-- output X -->
</xsl:template>
<xsl:template match="node[not(contains(., 'some text'))]">
<!-- output Y -->
</xsl:template>
<xsl:template match="/">
<xsl:apply-templates select=".//node" />
</xsl:template>
Writing templates that match specific nodes and using <xsl:apply-templates> to make the XSLT processor choose the appropriate ones is superior to writing complex <xsl:if> or <xsl:choose> constructs.
The above sample is equivalent to the imperative style:
<xsl:template match="/">
<xsl:for-each select=".//node">
<xsl:choose>
<xsl:when test="contains(., 'some text')">
<!-- output X -->
</xsl:when>
<xsl:when test="not(contains(., 'some text'))">
<!-- output Y -->
</xsl:when>
<xsl:choose>
<xsl:for-each>
</xsl:template>
XSLT beginners tend to pick the latter form for its familiarity, but examining template matching instead of using conditionals is worthwhile. (also see.)
XSL has an <xsl:if>, but you're probably looking more for a <xsl:choose> / <xsl:when> / <xsl:otherwise> sequence. Some examples here (near the bottom). Maybe:
<xsl:choose>
<xsl:when test="[conditionOne] or [conditionTwo]">
<!-- do [action] -->
</xsl:when>
<xsl:otherwise>
<!-- do [alternative action] -->
</xsl:otherwise>
</xsl:choose>
The general if statement syntax is
<xsl:if test="expression">
...some output if the expression is true...
</xsl:if>
Not sure if XSL has the else condition but you should be able to test if true then test if false or the other way around.
In this case you would have to use a xsl:choose. It's like using if/else with a final else.
<xsl:choose>
<xsl:when test="condition one or condition two">
<!-- action -->
</xsl:when>
<xsl:otherwise>
<!-- alternative action -->
</xsl:otherwise>
</xsl:choose>