Search code examples
xmlxsltxpathxslt-2.0xslt-grouping

How to create html list from flat xml file using XSLT


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.


Solution

  • 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