Search code examples
xmlxsltxslt-2.0

Grouping text node and adjacent elements of particular type


Please suggest, how to group the text node and some elements like 'i' or 'b' or 'list' within 'p' element. Ensuring div should not child to p.

XML: (with line breaks or whitespaces for display purpose, to run use below 2nd XML)

<article>
<body>
    <para>
        <display><fig>Fig1</fig></display>
        the text node1
    </para>
    <para>
        <display><fig>Fig1</fig></display>
    </para>
    <para>
        <display><fig>Fig1</fig></display>
        the text node1 <i>h</i> ther <b>b</b> the text4
        <display><tab>Table1</tab></display>
        the text node2
        <list><li>list1</li></list>
    </para>
    <para>The text node3</para>

</body>
</article>

XML: (without line breaks)

<article><body><para><display><fig>Fig1</fig></display>the text node1</para><para><display><fig>Fig1</fig></display></para><para><display><fig>Fig1</fig></display>the text node1 <i>h</i> ther <b>b</b> the text4<display><tab>Table1</tab></display>the text node2<list><li>list1</li></list></para><para>The text node3</para></body></article>

XSLT:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">

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

<xsl:template match="para">
    <xsl:choose>
        <xsl:when test="not(text())"><xsl:apply-templates/></xsl:when>
        <xsl:when test="display and text() or *">
            <xsl:for-each select="node()">
                <xsl:choose>
                    <xsl:when test="name()='display'"><div><xsl:apply-templates/></div></xsl:when>
                    <xsl:when test="name()='i' or name()='b'">
                        <xsl:copy><xsl:apply-templates select="@*|node()"/></xsl:copy>
                    </xsl:when>
                    <xsl:when test="not(*)"><p><xsl:value-of select="."/></p></xsl:when><!--Here grouping required with adjacent elements 'i' or 'b' etc -->
                    <xsl:otherwise><p><xsl:apply-templates/></p></xsl:otherwise>
                </xsl:choose>
            </xsl:for-each>
        </xsl:when>
        <xsl:otherwise>
            <p><xsl:apply-templates/></p>
        </xsl:otherwise>
    </xsl:choose>
</xsl:template>
</xsl:stylesheet>

Required Result:

<article>
<body>
    <div><fig>Fig1</fig></div><!--ensure div should not child to 'p'-->
    <p>the text node1</p>       <!--Text area including 'i' and 'b' to be within 'p' -->
    <div><fig>Fig1</fig></div>
    <div><fig>Fig1</fig></div>
    <p>the text node1 <i>h</i> ther <b>b</b> the text4</p><!--Text area including 'i' and 'b' to be within 'p' -->
    <div><tab>Table1</tab></div>
    <p>the text node2<list><li>list1</li></list></p><!--text area includes 'list' element -->
    <p>The text node3</p>
</body>
</article>

Solution

  • As you are using XSLT 2.0, you can make use of xsl:for-each-group here, to group adjacent child nodes depending on whether they are display element or not.

    <xsl:for-each-group select="node()" group-adjacent="boolean(self::display)"> 
    

    So, the nodes other than display will have a grouping key of false and so be grouped together, allowing you to wrap them in a p tag

    Try this XSLT

    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
    <xsl:output method="xml" indent="yes" />
      <xsl:strip-space elements="*" />
    
    <xsl:template match="@*|node()">
        <xsl:copy><xsl:apply-templates select="@*|node()"/></xsl:copy>
    </xsl:template>
    
    <xsl:template match="para">
      <xsl:for-each-group select="node()" group-adjacent="boolean(self::display)">
        <xsl:choose>
            <xsl:when test="current-grouping-key()">
               <xsl:apply-templates select="current-group()" />
            </xsl:when>
            <xsl:otherwise>
                <p>
                    <xsl:apply-templates select="current-group()" />
                </p>
            </xsl:otherwise>
          </xsl:choose>
        </xsl:for-each-group>
    </xsl:template>
    
    <xsl:template match="display">
       <div>
        <xsl:apply-templates />
       </div>
    </xsl:template>
    </xsl:stylesheet>