Tricky XSLT recursive tree transformation
This is an open discussion with 8 replies, filed under General.
Search
One way to achieve this is: Count the elements up to the next header line.
<xsl:template match="//tbody"> <xsl:for-each select="tr[td/@class='header']"> <xsl:variable name="elements-number" select="count(following-sibling::*[not(self::tr[td/@class='header'])]) - count(following-sibling::tr/following-sibling::*[not(self::tr[td/@class='header'])])"/> <xsl:apply-templates select="following-sibling::*[position() <= $elements-number]" mode="element"> <xsl:with-param name="rank" select="."/> </xsl:apply-templates> </xsl:for-each> </xsl:template> <xsl:template match="tr" mode="element"> <xsl:param name="rank"/> <entry> <color> <xsl:value-of select="td[1]"/> </color> <shape> <xsl:value-of select="td[2]"/> </shape> <taste> <xsl:value-of select="td[3]"/> </taste> <rank> <xsl:value-of select="rank"/> </rank> </entry> </xsl:template>
I have not included the type or country, but that shouldn't be too hard.
Sorry, it's buggy. It only outputs 3 elements. Wait a minute...
I was counting wrong. Here you are:
<xsl:template match="//tbody"> <xsl:for-each select="tr[td/@class='header']"> <xsl:variable name="elements-number" select=" count(following-sibling::*[not(self::tr[td/@class='header'])]) - count(following-sibling::tr[td/@class='header']/following-sibling::*[not(self::tr[td/@class='header'])])"/> <xsl:apply-templates select="following-sibling::*[position() <= $elements-number]" mode="element"> <xsl:with-param name="rank" select="normalize-space(td/text())"/> </xsl:apply-templates> </xsl:for-each> </xsl:template> <xsl:template match="tr" mode="element"> <xsl:param name="rank"/> <entry> <color> <xsl:value-of select="td[1]"/> </color> <shape> <xsl:value-of select="td[2]"/> </shape> <taste> <xsl:value-of select="td[3]"/> </taste> <rank> <xsl:value-of select="$rank"/> </rank> </entry> </xsl:template>
[EDIT]: Added normalize-space()
...
Just a short explanation:
- For each
tr
element with a class 'header', count all followingtr
elements which don't have the class 'header' - Subtract the count of elements following the next
tr
element with a class 'header' (and not having a class 'header') - Result: number of elements up to the next "header"
I am sure there are simpler ways, but this technique saved my day once. :-)
If you want a more "expert-like" solution, you may use IDs:
<xsl:template match="//tbody"> <data> <xsl:apply-templates select="tr[td/@class='header']" mode="header"/> </data> </xsl:template> <xsl:template match="tr" mode="header"> <xsl:variable name="next-header" select="generate-id(following-sibling::tr[td/@class='header'])"/> <xsl:apply-templates select="following-sibling::tr[not(generate-id(.) = $next-header or preceding-sibling::tr[generate-id(.) = $next-header])]" mode="element"> <xsl:with-param name="rank" select="normalize-space(td/text())"/> </xsl:apply-templates> </xsl:template> <xsl:template match="tr" mode="element"> <xsl:param name="rank"/> <entry> <color> <xsl:value-of select="td[1]"/> </color> <shape> <xsl:value-of select="td[2]"/> </shape> <taste> <xsl:value-of select="td[3]"/> </taste> <rank> <xsl:value-of select="$rank"/> </rank> </entry> </xsl:template>
[EDIT]: Added initial apply-templates
.
The above only selects elements which are not the "next header" or following the "next header". Same result.
Thanks for the solution... Saved me a day...
You're welcome, @qnn!
Which solution will you use? (I think that the second one is more elegant.)
Create an account or sign in to comment.
Hi!
I have a loosely structured XHTML data and I need to convert it to properly structured XML to import to a Symphony section.
Here's the example:
I am trying to achieve following structure:
Firstly I need to extract the fruit type from the tbody/tr/td/img[1]/@src, secondly the country from tbody/tr/td/img[2]/@alt attribute and finally the grade from tbody/tr/td itself.
Next I need to populate all the entries under each category while including those values (like shown above).
But... As you can see, the the data I was given is very loosely structured. A category is simply a td and after that come all the items in that category. To make the things worse, in my datasets, the number of items under each category varies between 1 and 100...
I've tried a few approaches but just can't seem to get it. Any help is greatly appreciated. I know that XSLT 2.0 introduces xsl:for-each-group, but I am limited to XSLT 1.0.