Reputation: 3920
I know that this has been answered before, but I am curious about how to achieve this without keys.
I have this XML and I have to get the nodes between {%tab%} and {%endtab%}.
<element>
<hello>{%tab%}</hello>
<hello>yes</hello>
<hello>{%endtab%}</hello>
<hello>no</hello>
<hello>no</hello>
<hello>{%tab%}</hello>
<hello>yes</hello>
<hello>{%endtab%}</hello>
</element>
This is what I got:
<xsl:template match="hello[preceding-sibling::hello[not(normalize-space(text())!='{%tab%}')]]
[following-sibling::hello[not(normalize-space(text())!='{%endtab%}')]]
[
(preceding-sibling::hello[not(normalize-space(text())!='{%tab%}')])[1]/(following-sibling::hello[not(normalize-space(text())!='{%endtab%}')])[1]
= (following-sibling::hello[not(normalize-space(text())!='{%endtab%}')])[1]
]">
<strong><xsl:value-of select="." /></strong>
</xsl:template>
This selects all nodes that are followed by {%endtab%} and are preceded by {%tab%} nodes.
hello[preceding-sibling::hello[not(normalize-space(text())!='{%tab%}')]]
[following-sibling::hello[not(normalize-space(text())!='{%endtab%}')]]
The problem with this is that the "no" nodes are also selected because they are also between those nodes. So, I have to ensure that the {%endtab%} that follows certain node (its first occurrence) is the same that the one that follows the preceding {%tab%}, which I do with this XPath:
[
(preceding-sibling::hello[not(normalize-space(text())!='{%tab%}')])[1]
/(following-sibling::hello[not(normalize-space(text())!='{%endtab%}')])[1]
= (following-sibling::hello[not(normalize-space(text())!='{%endtab%}')])[1]
]
But, this is not filtering the "no" nodes as expected.
Upvotes: 2
Views: 128
Reputation: 116992
Two notes of general interest:
I know that this has been answered before, but I am curious about how to achieve this without keys.
A link to the solution using a key would be in order:
https://stackoverflow.com/a/28179346/3016153
Since an XSLT 2.0 solution has been suggested earlier, I would suggest another one which I believe is much simpler:
XSLT 2.0
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="utf-8" indent="yes"/>
<xsl:template match="/element">
<xsl:copy>
<xsl:for-each-group select="hello" group-starting-with="hello[.='{%tab%}']">
<xsl:for-each-group select="current-group()" group-ending-with="hello[.='{%endtab%}']">
<xsl:if test="current-group()[self::hello[.='{%tab%}']]">
<xsl:for-each select="current-group()[not(position()=1 or position()=last())]">
<strong><xsl:value-of select="." /></strong>
</xsl:for-each>
</xsl:if>
</xsl:for-each-group>
</xsl:for-each-group>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Upvotes: 2
Reputation: 243449
This XPath expression:
/*/*[not(. = '{%tab%}' or . = '{%endtab%}')
and preceding-sibling::*[. = '{%tab%}' or . = '{%endtab%}'][1] = '{%tab%}'
and following-sibling::*[. = '{%tab%}' or . = '{%endtab%}'][1] = '{%endtab%}'
]
selects any element that is a child of the top element and is between a sibling element with string-value {%tab%}
and a sibling element with string-value {%endtab%}
Here is a running proof with a simple XSLT transformation:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="/">
<xsl:copy-of select=
"/*/*[not(. = '{%tab%}' or . = '{%endtab%}')
and preceding-sibling::*[. = '{%tab%}' or . = '{%endtab%}'][1] = '{%tab%}'
and following-sibling::*[. = '{%tab%}' or . = '{%endtab%}'][1] = '{%endtab%}'
]"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided source XML document:
<element>
<hello>{%tab%}</hello>
<hello>yes</hello>
<hello>{%endtab%}</hello>
<hello>no</hello>
<hello>no</hello>
<hello>{%tab%}</hello>
<hello>yes</hello>
<hello>{%endtab%}</hello>
</element>
the wanted, correct result is produced:
<hello>yes</hello>
<hello>yes</hello>
Upvotes: 2
Reputation: 3788
Edit: at first I didn't notice the xslt-1.0 tag; this answer is probably useless for the original poster
Using XSLT 2.0 and the same clever trick of @bjimba's answer you could group together the elements inside a "tab", which would allow to do something both with the whole group and with each single hello
element:
XSLT 2.0
...
<xsl:for-each-group select="hello" group-by="my:tabId(.)">
<!-- do something with a whole run -->
<strong>
<!-- do something with the single elements -->
<xsl:apply-templates select="current-group()"/>
</strong>
</xsl:for-each-group>
...
<xsl:function name="my:tabId" as="xs:integer?">
<xsl:param name="e" as="element()"/>
<xsl:variable name="tabStartCount"
select="count($e/preceding-sibling::hello[. = '{%tab%}'])"/>
<xsl:variable name="tabEndCount"
select="count($e/preceding-sibling::hello[. = '{%endtab%}'])"/>
<xsl:sequence select="
if ($e != '{%endtab%}' and ($tabStartCount > $tabEndCount)) then
$tabStartCount
else
()
"/>
</xsl:function>
Upvotes: 0
Reputation: 928
Ignore the following siblings and count the preceding tab
s and the preceding endtab
s. If they aren't equal, you are are either an endtab
or a yes
. Exclude the case where you're an endtab
.
Allow me to dispense with normalize-space()
to make the example clearer.
<xsl:template match="hello[
(. != '{%endtab%}')
and
(
count(preceding-sibling::hello[. = '{%tab%}'])
!= count(preceding-sibling::hello[. = '{%endtab%}'])
)
]">
Upvotes: 3