2

I am a noob on XSLT. I have a XML where t nodes are followed by other nodes, and then another t node might appear again followed by nodes again, and so on

<t />
<n1 />
<n2 />
..

<t/>
<n3 />
<n4 />
...

What I need to turn this XML into is a HTML where t nodes wraps all nodes following it up to the next t node

<div class='t'>
   <div class='n1'/>
   <div class='n2'/>
    ...
</div>

<div class='t'>
   <div class='n3'/>
   <div class='n4'/>
    ...
</div>

I am having a hard time implementing this. Any ideas \ hints?

Thanks!

1
  • Can you identify which XSLT version you support? Assuming 2.0 you can also use grouping functions. Commented Mar 16, 2011 at 0:51

2 Answers 2

6

This is grouping adjacents. There are many solutions:

Whit this wellformed input:

<root>
    <t />
    <n1 />
    <n2 />
    <t/>
    <n3 />
    <n4 />
</root>

XSLT 1.0: traversing with following axis

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:strip-space elements="*"/>
    <xsl:template match="root">
        <xsl:copy>
            <xsl:apply-templates select="node()[1]" mode="group"/>
            <xsl:apply-templates select="t"/>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="t">
        <div class="t">
            <xsl:apply-templates select="following-sibling::node()[1]"
                                 mode="group"/>
        </div>
    </xsl:template>
    <xsl:template match="t" mode="group"/>
    <xsl:template match="node()" mode="group">
        <xsl:apply-templates select="."/>
        <xsl:apply-templates select="following-sibling::node()[1]"
                             mode="group"/>
    </xsl:template>
    <xsl:template match="*[starts-with(name(),'n')]">
        <div class="{name()}"/>
    </xsl:template>
</xsl:stylesheet>

XSLT 1.0: Keys

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:strip-space elements="*"/>
    <xsl:key name="kNodeByMark"
             match="node()[../t][not(self::t)]"
             use="generate-id((..|preceding-sibling::t[1])[last()])"/>
    <xsl:template match="root">
        <xsl:copy>
            <xsl:apply-templates select="key('kNodeByMark',generate-id())"/>
            <xsl:for-each select="t">
                <div class="t">
                    <xsl:apply-templates
                         select="key('kNodeByMark',generate-id())"/>
                </div>
            </xsl:for-each>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="*[starts-with(name(),'n')]">
        <div class="{name()}"/>
    </xsl:template>
</xsl:stylesheet>

XSLT 2.0: for-each-group instruction

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:strip-space elements="*"/>
    <xsl:template match="root">
        <xsl:copy>
            <xsl:apply-templates select="node()[../t[1] >> .]"/>
            <xsl:for-each-group select="node()" group-starting-with="t">
                <div class="t">
                    <xsl:apply-templates 
                         select="current-group()[position()>1]"/>
                </div>
            </xsl:for-each-group>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="*[starts-with(name(),'n')]">
        <div class="{name()}"/>
    </xsl:template>
</xsl:stylesheet>

Output:

<root>
    <div class="t">
        <div class="n1" />
        <div class="n2" />
    </div>
    <div class="t">
        <div class="n3" />
        <div class="n4" />
    </div>
</root>

EDIT: Traversing following axis refactored to look like the others solutions. Stripping identity rules.

Sign up to request clarification or add additional context in comments.

Comments

0

See my note on your question, regarding "which XSLT version?". If grouping is supported in your target version, see other answers here, as that is easier to understand and will almost certainly perform better on any XSLT processor. If you aren't certain, I recommend going with a 1.0 solution like this one.

You can do it with the "XML fragment" exactly like you posted with most XSLT processors, but I added a "root" element to your XML, to reduce certain unknowns in answering your question.

In this solution below, I've tried to keep a direct correlation between the shape of the XSLT and the shape of the output you desire. In my opinion that makes it easier to maintain/understand, at least for smaller stylesheets.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="/root">
    <xsl:for-each select="t">
      <div class='t'>
        <xsl:for-each select="following-sibling::*[count(preceding-sibling::t)=(count(current()/preceding-sibling::t) + 1) and not(self::t)]">
          <div class='{name()}' />
        </xsl:for-each>
      </div>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

The right-hand side of "following-sibling::*[count(preceding-sibling::t)=(count(current()/preceding-sibling::t) + 1) and not(self::t)]" could be simplified, I'm sure, using something like "current()::position()" (which isn't valid, fyi), but I'm rusty and couldn't remember some of the alias syntax.

This basically says: 1) Evaluate every T. 2) Select elements with the same quantity of T preceding them, as the index of the T we are currently evaluating.

Note that you've probably tried iterating through procedurally, and found you can't store the last value found in XSLT. Or you've found that you can, but only with nested templates. This same type of pivot you are performing has many XSLT neophytes hitting roadblocks, so don't feel bad.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.