-1

I am looking for a clean way to do the following using XSLT.

Convert this source:

<para>blah blah</para>
<list>num1</list>
<list>num2</list>
<list>num3</list>
<para>blah blah</para>
<list>num1</list>
<list>num2</list>
<para>blah blah blah blah blah</para>

To this output:

<p>blah blah</p>
<ol>
    <li>num1</li>
    <li>num2</li>
    <li>num3</li>
</ol>
<p>blah blah</p>
<ol>
    <li>num1</li>
    <li>num2</li>
</ol>
<p>blah blah blah blah blah</p>

Keep in mind I do not know exactly how many <list>'s there will be.

So far I have this:

<xsl:template match="para">
    <p><xsl:value-of select="." /></p>
</xsl:template>

<xsl:template match="list">
    <ol><li><xsl:value-of select="." /></li></ol>
</xsl:template>

But my output looks like this:

<p>blah blah</p>    
<ol><li>num1</li></ol>
<ol><li>num2</li></ol>
<ol><li>num3</li></ol>
<p>blah blah</p>
<ol><li>num1</li></ol>
<ol><li>num2</li></ol>
<p>blah blah blah blah blah</p>

I know why I am getting duplicate <ol> elements, but I do not know how to stop it. Quite a brain teaser.

Any help would be greatly appreciated.

1

3 Answers 3

2

XSLT 2.0 has tools especially for this kind of Operations:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:template match="xml">
        <xsl:for-each-group select="*" group-adjacent="boolean(self::list)">
            <xsl:choose>
                <xsl:when test="current-grouping-key()">
                    <ol>
                        <xsl:apply-templates select="current-group()"/>
                    </ol>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:apply-templates select="current-group()"/>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:for-each-group>
    </xsl:template>
    <xsl:template match="para">
        <p>
            <xsl:apply-templates/>
        </p>
    </xsl:template>
    <xsl:template match="list">
        <li>
            <xsl:apply-templates/>
        </li>
    </xsl:template>
</xsl:stylesheet>

With this XML:

<xml>
    <para>blah blah</para>
    <list>num1</list>
    <list>num2</list>
    <list>num3</list>
    <para>blah blah</para>
    <list>num1</list>
    <list>num2</list>
    <para>blah blah blah blah blah</para>
</xml>

You'll get the desired Output:

<p>blah blah</p>
<ol>
    <li>num1</li>
    <li>num2</li>
    <li>num3</li>
</ol>
<p>blah blah</p>
<ol>
    <li>num1</li>
    <li>num2</li>
</ol>
<p>blah blah blah blah blah</p>

You should read up on for-each-group at http://www.w3.org/TR/xslt20/#xsl-for-each-group

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

2 Comments

Thanks! This looks like a great way to do it.
+1 good answer. But I think it's no good to use such "brick" template rules.
2

This XSLT 1.0 stylesheet:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:template match="node()">
        <xsl:apply-templates select="node()[1]|following-sibling::node()[1]"/>
    </xsl:template>
    <xsl:template match="para">
        <p>
            <xsl:value-of select="."/>
        </p>
        <xsl:apply-templates select="following-sibling::node()[1]"/>
    </xsl:template>
    <xsl:template match="list[preceding-sibling::node()[1][not(self::list)]]">
        <ol>
            <xsl:call-template name="makeList"/>
        </ol>
        <xsl:apply-templates select="following-sibling::node()
                                              [not(self::list)][1]"/>
    </xsl:template>
    <xsl:template match="list" name="makeList">
        <li>
            <xsl:value-of select="."/>
        </li>
        <xsl:apply-templates select="following-sibling::node()[1]
                                              [self::list]"/>
    </xsl:template>
</xsl:stylesheet>

Output:

<p>blah blah</p>
<ol>
        <li>num1</li>
        <li>num2</li>
        <li>num3</li>
</ol>
<p>blah blah</p>
<ol>
        <li>num1</li>
        <li>num2</li>
</ol>
<p>blah blah blah blah blah</p>

Note: Fine grained traversal.

Edit: Compact code.

4 Comments

Thanks for the help this is a possible approach. But isn't this approach hard to maintain? Because I will have to add the following-sibling select on every top level element I match on.
@joe: This stylesheet has four rules: a default fine grained traversal, a rule for p, a rule for ol and a rule for li. So, it's a one to one transformation. I don't see the hard to maintain problem. You've asked to Dimitre: what if I have an element H1 before the list. Well, you have to declare waht to do with H1 then. Now, it's bypassed. So, two solutions: copy everything you don't want to transform or add a rule for H1.
What I am saying is if I add <xsl:template match="Heading1"><h1> <xsl:value-of select="."/></h1></xsl:template>. I will have to put <xsl:apply-templates select="following-sibling::node()[1]"/> in the match as well.
@joe: Yes, applying templates to first child and first following sibling is the way this works.
0

This transformation:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:key name="kFollowing" match="list"
  use="generate-id(preceding-sibling::para[1])"/>

 <xsl:template match="node()|@*" name="identity">
  <xsl:copy>
   <xsl:apply-templates select="node()|@*"/>
  </xsl:copy>
 </xsl:template>

 <xsl:template match="para">
  <p>
    <xsl:apply-templates/>
  </p>
   <xsl:variable name="vFol"
    select="key('kFollowing',generate-id())"/>
   <xsl:if test="$vFol">
      <ol>
       <xsl:apply-templates mode="copy"
           select="key('kFollowing',generate-id())"/>
      </ol>
  </xsl:if>
 </xsl:template>

 <xsl:template match="list" mode="copy">
  <li><xsl:value-of select="."/></li>
 </xsl:template>
 <xsl:template match="list"/>
</xsl:stylesheet>

when applied on the following XML document (wrapping the provided input in a single top element):

<t>
    <para>blah blah</para>
    <list>num1</list>
    <list>num2</list>
    <list>num3</list>
    <para>blah blah</para>
    <list>num1</list>
    <list>num2</list>
    <para>blah blah blah blah blah</para>
</t>

produces the wanted, correct result:

<t>
    <p>blah blah</p>
    <ol>
        <li>num1</li>
        <li>num2</li>
        <li>num3</li>
    </ol>
    <p>blah blah</p>
    <ol>
        <li>num1</li>
        <li>num2</li>
    </ol>
    <p>blah blah blah blah blah</p>
</t>

UPDATE: The OP has indicated in a comment that now he wants a solution where any non-list element can delimit a group of adjacent list siblings.

Here is the solution to the changed question :

<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="list"
  use="generate-id(preceding-sibling::*[not(self::list)][1])"/>

 <xsl:template match="node()|@*" name="identity">
  <xsl:copy>
   <xsl:apply-templates select="node()|@*"/>
  </xsl:copy>
 </xsl:template>

 <xsl:template match="*[not(self::list) and following-sibling::*[1][self::list]]">
  <xsl:call-template name="identity"/>
   <xsl:variable name="vFol"
    select="key('kFollowing',generate-id())"/>
   <xsl:if test="$vFol">
      <ol>
       <xsl:apply-templates mode="copy"
           select="key('kFollowing',generate-id())"/>
      </ol>
  </xsl:if>
 </xsl:template>

 <xsl:template match="list" mode="copy">
  <li><xsl:value-of select="."/></li>
 </xsl:template>
 <xsl:template match="list"/>
</xsl:stylesheet>

When this transformation is applied on the following XML document (Note that the separating elements have now random names):

<t>
    <bara>blah blah</bara>
    <list>num1</list>
    <list>num2</list>
    <list>num3</list>
    <vara>blah blah</vara>
    <list>num1</list>
    <list>num2</list>
    <dara>blah blah blah blah blah</dara>
</t>

the wanted, correct result is produced:

<t>
    <bara>blah blah</bara>
    <ol>
        <li>num1</li>
        <li>num2</li>
        <li>num3</li>
    </ol>
    <vara>blah blah</vara>
    <ol>
        <li>num1</li>
        <li>num2</li>
    </ol>
    <dara>blah blah blah blah blah</dara>
</t>

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.