Reputation: 317
I have the following markup, a table with a mix of TRs with one or two child TDs
<table>
<tr>
<td>
<p>XXX</p>
</td>
</tr>
<tr>
<td>
<p>YYYYYY</p>
</td>
<td>
<p>YYYYYY</p>
</td>
</tr>
<tr>
<td>
<p>YYYYYY</p>
</td>
<td>
<p>YYYYYY</p>
</td>
</tr>
<tr>
<td>
<p>YYYYYY</p>
</td>
<td>
<p>YYYYYY</p>
</td>
</tr>
<tr>
<td>
<p>XXX</p>
</td>
</tr>
<tr>
<td>
<p>YYYYYY</p>
</td>
<td>
<p>YYYYYY</p>
</td>
</tr>
<tr>
<td>
<p>YYYYYY</p>
</td>
<td>
<p>YYYYYY</p>
</td>
</tr>
<tr>
<td>
<p>XXX</p>
</td>
</tr>
<tr>
<td>
<p>YYYYYY</p>
</td>
<td>
<p>YYYYYY</p>
</td>
</tr>
<tr>
<td>
<p>YYYYYY</p>
</td>
<td>
<p>YYYYYY</p>
</td>
</tr>
</table>
I am trying to use XSLT 1.0 to transform this into
<AAA>
XXX
<BBB>YYYYY</BBB>
<BBB>YYYYY</BBB>
...
</AAA>
<AAA>
XXX
<BBB>YYYYY</BBB>
<BBB>YYYYY</BBB>
...
</AAA>
...
Abstracting from the fact that this is probably to the best way to approach a nested loop in XSLT, what XPath expression put into ??? below would select all TR siblings of a current TR (XXX) which have two TD children, but only until the next TR (XXX). I am differentiating XXX and YYYYY-containing nodes on the basis of how many TD children they have. 1 = XXX, 2 = YYYYY.
<xsl:for-each select="//table/tr">
<xsl:if test="count(td) = '1'">
XXX
</xsl:if>
<xsl:for-each select="???">
all YYYYY up until the next XXX
</xsl:for-each>
</xsl:for-each>
I have "following-sibling::tr[child::td[following-sibling::td]" but that matches all the following with YYYYY to the end - how do I make it select only those up until the next TR with XXX?
Thanks!
Upvotes: 1
Views: 1689
Reputation: 163322
I've always thought it would be useful to have an XPath construct that does exactly what you ask for: all the items in a sequence up to (and including/excluding) the first that matches some condition. I've considered syntax for this such as expr until condition
. There are attempts in Saxon and EXSLT to provide higher-order extension functions to meet the requirement, such as saxon:leading(). Sadly, though, there's nothing in the spec; though in XPath 3.0 it can be done fairly easily yourself using higher-order functions.
You can sometimes solve the problem using grouping (xsl:for-each-group), but often the simplest and most elegant approach is to use recursion: that is, write a function that processes the first item in the sequence and that calls itself to process the next item provided some condition is true.
Another approach is to search the sequence for the first node that matches the condition, and then use subsequence() to select the nodes up to that position:
<xsl:variable name="positions" select="
for $i in 1 to count($seq)
return if (my:condition($seq[$i])) then $i else ()"/>
<xsl:apply-templates select="subsequence($seq, 1, $positions[1])"/>
Upvotes: 1
Reputation: 243449
I. XSLT 1.0 solution:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:key name="kFollowing" match="tr[td[2]]"
use="generate-id(preceding-sibling::tr
[not(td[2])]
[1]
)"/>
<xsl:template match="/*">
<xsl:apply-templates select="tr[not(td[2])]"/>
</xsl:template>
<xsl:template match="tr[not(td[2])]">
<AAA>
<xsl:value-of select="concat('
', td/p, '
')"/>
<xsl:apply-templates select="key('kFollowing', generate-id())"/>
</AAA>
</xsl:template>
<xsl:template match="p">
<BBB><xsl:value-of select="."/></BBB>
</xsl:template>
</xsl:stylesheet>
when this XSLT 1.0 transformation is applied on the provided XML document:
<table>
<tr>
<td>
<p>XXX</p>
</td>
</tr>
<tr>
<td>
<p>YYYYYY</p>
</td>
<td>
<p>YYYYYY</p>
</td>
</tr>
<tr>
<td>
<p>YYYYYY</p>
</td>
<td>
<p>YYYYYY</p>
</td>
</tr>
<tr>
<td>
<p>YYYYYY</p>
</td>
<td>
<p>YYYYYY</p>
</td>
</tr>
<tr>
<td>
<p>XXX</p>
</td>
</tr>
<tr>
<td>
<p>YYYYYY</p>
</td>
<td>
<p>YYYYYY</p>
</td>
</tr>
<tr>
<td>
<p>YYYYYY</p>
</td>
<td>
<p>YYYYYY</p>
</td>
</tr>
<tr>
<td>
<p>XXX</p>
</td>
</tr>
<tr>
<td>
<p>YYYYYY</p>
</td>
<td>
<p>YYYYYY</p>
</td>
</tr>
<tr>
<td>
<p>YYYYYY</p>
</td>
<td>
<p>YYYYYY</p>
</td>
</tr>
</table>
produces the wanted, correct result:
<AAA>
XXX
<BBB>YYYYYY</BBB>
<BBB>YYYYYY</BBB>
<BBB>YYYYYY</BBB>
<BBB>YYYYYY</BBB>
<BBB>YYYYYY</BBB>
<BBB>YYYYYY</BBB>
</AAA>
<AAA>
XXX
<BBB>YYYYYY</BBB>
<BBB>YYYYYY</BBB>
<BBB>YYYYYY</BBB>
<BBB>YYYYYY</BBB>
</AAA>
<AAA>
XXX
<BBB>YYYYYY</BBB>
<BBB>YYYYYY</BBB>
<BBB>YYYYYY</BBB>
<BBB>YYYYYY</BBB>
</AAA>
Explanation:
Appropriate use of a key to define all tr
that have a second td
child as a function of the generate-id()
of the immediately-preceding single-child tr
element.
II. XSLT 2.0 solution:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/*">
<xsl:for-each-group group-starting-with="tr[not(td[2])]"
select="tr">
<xsl:apply-templates select="current-group()[1]"/>
</xsl:for-each-group>
</xsl:template>
<xsl:template match="tr[not(td[2])]">
<AAA>
<xsl:value-of select="concat('
', td/p, '
')"/>
<xsl:apply-templates select="current-group()[position() gt 1]"/>
</AAA>
</xsl:template>
<xsl:template match="p">
<BBB><xsl:value-of select="."/></BBB>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the same XML document (above), the same correct result is produced:
<AAA>
XXX
<BBB>YYYYYY</BBB>
<BBB>YYYYYY</BBB>
<BBB>YYYYYY</BBB>
<BBB>YYYYYY</BBB>
<BBB>YYYYYY</BBB>
<BBB>YYYYYY</BBB>
</AAA>
<AAA>
XXX
<BBB>YYYYYY</BBB>
<BBB>YYYYYY</BBB>
<BBB>YYYYYY</BBB>
<BBB>YYYYYY</BBB>
</AAA>
<AAA>
XXX
<BBB>YYYYYY</BBB>
<BBB>YYYYYY</BBB>
<BBB>YYYYYY</BBB>
<BBB>YYYYYY</BBB>
</AAA>
Explanation:
Appropriate use of xsl:for-each-group
with group-starting-with
attribute and the current-group()
function.
Upvotes: 1