Check any level of parent nodes with XPath and XSLT - xslt

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" />

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.

Multi layer conditional wrap HTML with XSLT

In my Umbraco CMS site, I'm making a list of node "widgets" for content editors to use, with a list of many options they can toggle to change the display. This often involves wrapping an element with an anchor, div, or something else.
Using XSLT to display these from the XML output, I've put together what a kludge approach as I'm a very new XSLT beginner.
What I've come to as a solution is multiple nested apply-templates. This creates a large list of conditions, often asking repeat checks, which trees out pretty huge. It's a bit of a nightmare to manage.
It looks as such (but with more than two options in each choose):
<xsl:template match="/">
<xsl:choose>
<xsl:when test="type='1'">
<xsl:apply-templates select="widgetContent" mode="type_1" />
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="widgetContent" mode="type_default" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="wigetContent" mode="type_1">
<xsl:choose>
<xsl:when test="./wrap_with_hyperlink != 0">
<xsl:element name="a">
<xsl:apply-templates select="." mode="hyperlink_wrapped" />
</xsl:element>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="widgetContent" mode="not_hyperlink_wrapped" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
What can I do to reduce this tangled mess? I've structured the conditions to be as top down as much as possible, but there are definitely repeated checks where type_2 has to ask the same questions as type_1 all over again.
(edit: clarity) Because the design is basically an onion, type_1 is wrapped by certain tags, type_2 is wrapped by different tags. Next layer in, both could be wrapped by the same tags, and so forth. What would be perfect is:
<xsl:if test="this_wrap_style = 1"><xsl:element name="abc"></xsl:if>
<xsl:if test="this_wrap_style = 2"><xsl:element name="xyz"></xsl:if>
(everything else)
</abc> //if it exist.
</xyz> //etc
Which definitely doesn't work.
Some of this has been reduced by using Umbraco Doc Types for different widget controls, but part of the nature is that to the ideal structure for content editors is selecting a box widget will give them 5 different types of widget boxes (or more) to choose from, and a coherent back end isn't as important.
Thank you all for your time.
<!--Store data in processing instruction nodes in a separate XML file-->
<?xml version='1.0' encoding="utf-8"?>
<root>
<?_1 div?>
<?_2 p?>
</root>
type_1 is wrapped by certain tags, type_2 is wrapped by different tags.
<xsl:variable name="divider" select="document('condition.xml')//processing-instruction(concat('_', $type) )" />
<xsl:variable name="equalizer" select="'span'"/>
<xsl:element name="{$divider}">
...
</xsl:element>
Next layer in, both could be wrapped by the same tags
<xsl:if test="contains('1,2',$type)">
<xsl:element name="{$equalizer}">
...
</xsl:element>
</xsl:if>

Find the position of an element within its parent with XSLT / XPath

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.

XSL two for-each loops for same node

Problem I have is I want to loop round the parents making them bold then get the children via the id:pid (parent id) and list them. My second loop doesn't work.
XML
XSL
<xsl:choose>
<xsl:when test="#PARENT_OBH_ID">
<b><xsl:value-of select="#TITLE"/></b>
<xsl:for-each select="FOOTER">
-<xsl:value-of select="#TITLE"/>
</xsl:for-each>
</xsl:when>
</xsl:choose>
</xsl:for-each>
Thanks
You're probably better off restructuring this to use templates, the system you're using at the moment means that the context data is becoming confused (you're xslt parser isn't sure which element it should read attributes from inside the second loop)
<xsl:choose>
<xsl:when test="#PARENT_OBH_ID">
<b><xsl:value-of select="#TITLE"/></b>
<xsl:apply-templates select="FOOTER" />
</xsl:when>
</xsl:choose>
<xsl:template match="FOOTER">
<xsl:value-of select="#TITLE"/>
</xsl:template>
apply-templates restarts the context with the footer element as the main focus (so #TITLE refers to the title attribute on footer, which is what you were aiming for I am guessing?)

Unique ID and multiple classes with XPath

I'm using XSLT for displaying a ul menu containing li and a.
I want the following:
Find the first li a element and add the .firstitem class.
Find the last li a element and add the .lastitem class.
Find the active li a element and add the .active class.
Add an unique ID to each li a element. (I.e. URL friendly menu text as ID).
I've managed to make step 1-3 work. Except that when I try to add the classes, it actually replaces the other classes rather than adding to them.
Here's the code:
<li>
<a>
<!-- Add .firstitem class -->
<xsl:if test="position() = 1">
<xsl:attribute name="class">firstitem</xsl:attribute>
</xsl:if>
<!-- Add .lastitem class -->
<xsl:if test="postition() = count(//Page)">
<xsl:attribute name="class">lastitem</xsl:attribute>
</xsl:if>
<!-- Add .active class -->
<xsl:if test="#Active='True'">
<xsl:attribute name="class">active</xsl:attribute>
</xsl:if>
<!-- Add link URL -->
<xsl:attribute name="href"><xsl:value-of select="#FriendlyHref" disable-output-escaping="yes"/></xsl:attribute>
<!-- Add link text -->
<xsl:value-of select="#MenuText" disable-output-escaping="yes"/>
</a>
</li>
In realtity, the a element could contain all those three classes. But as is goes through the code, it replaces everything in the class attribute. How can I add the classes instead of replacing them?
And step number 4 on my list, is to get a unique ID, preferably based on #MenuText. I know there is a replace() function, but I can't get it to work and my editor says that replace() isn't a function.
The menu item text contains spaces, dashes and other symbols that are not valid for using in the id attribute. How can I replace those symbols?
<a>
<xsl:attribute name="class">
<!-- Add .firstitem class -->
<xsl:if test="position() = 1">
<xsl:text> firstitem</xsl:text>
</xsl:if>
<!-- Add .lastitem class -->
<xsl:if test="postition() = count(//Page)">
<xsl:text> lastitem</xsl:text>
</xsl:if>
<!-- Add .active class -->
<xsl:if test="#Active='True'">
<xsl:text> active</xsl:text>
</xsl:if>
</xsl:attribute>
<!-- Add link URL -->
<xsl:attribute name="href"><xsl:value-of select="#FriendlyHref" disable-output-escaping="yes"/></xsl:attribute>
<!-- Add link text -->
<xsl:value-of select="#MenuText" disable-output-escaping="yes"/>
</a>
replace() is an XSLT2.0 function. When using XSLT1.0 you need a custom template to do most string manipulations.
I'm adding this to Martijn Laarman's answer, which covers your requirements 1-3 and has my vote:
To remove everything except a certain range of characters from a string with XSLT 1.0 (your 4th requirement), do the following.
<!-- declare at top level -->
<xsl:variable
name="validRange"
select="'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
/>
<!-- later, within a template… -->
<xsl:attribute name="id">
<xsl:value-of select="
concat(
'id_',
translate(
#MenuText,
translate(#MenuText, $validRange, ''),
''
)
)
" />
</xsl:attribute>
The inner translate() removes any valid character from #MenuText, leaving only the invalid ones. These are fed to the outer translate(), which now can remove all invalid chars from the #MenuText, whatever they might be in this instance. Only the valid chars remain.
You can make a function out of it:
<xsl:template name="HtmlIdFromString">
<xsl:param name="input" select="''" />
<xsl:value-of select="
concat('id_', translate( $input, translate($input, $validRange, ''), ''))
" />
</xsl:template>
and call it like this:
<xsl:attribute name="id">
<xsl:call-template name="HtmlIdFromString">
<xsl:with-param name="input" select="#MenuText" />
</xsl:call-template>
</xsl:attribute>
Use
<xsl:template match="#*">
<xsl:copy>
<xsl:apply-templates select="#*"/>
</xsl:copy>
</xsl:template>
to copy all existing attributes.
The replace() function is only supported in xslt 2.0 but i found this workaround for xslt 1.0.