Federico
Federico

Reputation: 3920

How to group nodes between nodes?

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

Answers (4)

michael.hor257k
michael.hor257k

Reputation: 116992

Two notes of general interest:

1.

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

2.

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

Dimitre Novatchev
Dimitre Novatchev

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

lfurini
lfurini

Reputation: 3788

Edit: at first I didn't notice the 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

bjimba
bjimba

Reputation: 928

Ignore the following siblings and count the preceding tabs and the preceding endtabs. 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

Related Questions