Reputation: 89
I have the following XML structure:
<node0>
<beforeContainer @id="test">
<beforeContainer2/>
</beforeContainer>
<container id='1'>
<node1>
<node2>
<text>Test1</text>
</node2>
</node1>
</container>
<node3>
<node4>
<text>Test2</text>
<node5>
<container id='2'>
<node6>
<button>Click</button>
</node6>
</container>
</node5>
</node4>
</node3>
<text>Test3</text>
<node7>
<node8>
<node9>
<container id='3'>
<node10>
<button>Click2</button>
</node10>
</container>
</node9>
</node8>
</node7>
</node0>
I have to extract all the nodes names between consequent <container>
nodes. The <container>
nodes are in different level of depth in the tree.
The output should look like this:
<container id='1'>
<node1/>
<node2/>
<text/>
<node3/>
<node4/>
<text/>
<node5/>
</container>
<container id='2'>
<node6/>
<button/>
<text/>
<node7/>
<node8/>
<node9/>
</container>
<container id='3'>
<node10/><button/>
</container>
I tried different x-Path expressions but unsuccessfully since they always have some specific scope(descendant-or-self::node(), following-sibling, etc..)
Upvotes: 0
Views: 597
Reputation: 167436
Simple grouping problem in XSLT 2 or 3:
<xsl:template match="/*">
<xsl:for-each-group select="descendant::*" group-starting-with="container">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:apply-templates select="current-group() except ." mode="copy"/>
</xsl:copy>
</xsl:for-each-group>
</xsl:template>
<xsl:template match="*" mode="copy">
<xsl:copy/>
</xsl:template>
Complete sample https://xsltfiddle.liberty-development.net/bdxtqQ is:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
version="3.0">
<xsl:mode on-no-match="shallow-copy"/>
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/*">
<xsl:for-each-group select="descendant::*" group-starting-with="container">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:apply-templates select="current-group() except ." mode="copy"/>
</xsl:copy>
</xsl:for-each-group>
</xsl:template>
<xsl:template match="*" mode="copy">
<xsl:copy/>
</xsl:template>
</xsl:stylesheet>
As for your edit of having elements preceding the first container
that you don't want to output, change the template doing the grouping to
<xsl:template match="/*">
<xsl:for-each-group select="descendant::*" group-starting-with="container">
<xsl:if test="self::container">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:apply-templates select="current-group() except ." mode="copy"/>
</xsl:copy>
</xsl:if>
</xsl:for-each-group>
</xsl:template>
https://xsltfiddle.liberty-development.net/bdxtqQ/1
The way the for-each-group group-starting-with
works (see https://www.w3.org/TR/xslt-30/#element-for-each-group) is that if the first item in the group population is not matching the group-starting-with
pattern it forms a group nevertheless:
If the group-starting-with attribute is present, then its value must be a pattern.
The items in the population are examined in population order. If an item matches the pattern, or is the first item in the population, then a new group is created and the item becomes its first member. Otherwise, the item is appended to the same group as its preceding item within the population.
This is helpful in many cases but then often requires to use a nested xsl:if
or xsl:choose
inside the xsl:for-each-group
that tests whether we have a "real" group matching the group-starting-with
pattern or have just collected any items before the first such group.
Upvotes: 2