Search code examples
xmlxsltxslt-2.0

XSLT - Identify consecutive nodes which has same patterns of attribute values


I have xml like this,

<section>
        <p id="ss_main">aa</p>
        <p id="ss_chap">bb</p>
        <p id="main">cc</p>
        <p id="main">dd</p>
        <p id="main">ee</p>
        <p id="ss_main">ff</p>
        <p id="main">gg</p>
        <p id="main">hh</p>
        <p id="main">ii</p>
        <p id="main">jj</p>
        <p id="ss_chap">xx</p>
        <p id="ss_main">yy</p>
        <p id="ss_chap">zz</p>
    </section>

what my requirement is place new nodes named <ss_start> and <ss_end> by covering existing nodes witch are start with ss.

so the output should be,

<section>
        <ss_start/>
        <p id="ss_main">aa</p>
        <p id="ss_chap">bb</p>
        <ss_end/>
        <p id="main">cc</p>
        <p id="main">dd</p>
        <p id="main">ee</p>
        <ss_start/>
        <p id="ss_main">ff</p>
        <ss_end/>
        <p id="main">gg</p>
        <p id="main">hh</p>
        <p id="main">ii</p>
        <p id="main">jj</p>
        <ss_start/>
        <p id="ss_chap">xx</p>
        <p id="ss_main">yy</p>
        <p id="ss_chap">zz</p>
        <ss_end/>
    </section>

I can write xsl like follows to cover specific node by <ss_start> and <ss_end>

<xsl:template match="p[@id='ss_main']">
        <ss_start/>
        <p id="ss_main"><xsl:apply-templates/></p>
        <ss_end/>
    </xsl:template>

but I'm struggling to find consecutive nodes that id attr starting from ss and cover them by <ss_start> and <ss_end>.

Can anyone suggest me a method how can I do this?


Solution

  • XSLT 1.0 sibling recursion

    In XSLT 1.0 you can do this as follows, which uses a technique called sibling recursion (though sibling traversal is probably a better term).

    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
        version="1.0">
    
        <xsl:output indent="yes" />
    
        <xsl:template match="node() | @*">
            <xsl:copy>
                <xsl:apply-templates select="node() | @*" />
            </xsl:copy>
        </xsl:template>
    
        <xsl:template match="section">
            <xsl:copy>
                <xsl:apply-templates select="*[1]" />
            </xsl:copy>
        </xsl:template>
    
        <xsl:template match="section/*[starts-with(@id, 'ss')]" priority="5">
            <xsl:if test="self::*[not(preceding-sibling::*[1][starts-with(@id, 'ss')])]">
                <ss_start />
            </xsl:if>
            <xsl:copy>
                <xsl:apply-templates select="node() | @*" />
            </xsl:copy>
            <xsl:if test="self::*[not(following-sibling::*[1][starts-with(@id, 'ss')])]">
                <ss_end />
            </xsl:if>
            <xsl:apply-templates select="following-sibling::*[1]" />
        </xsl:template>
    
        <xsl:template match="section/*">
            <xsl:copy>
                <xsl:apply-templates select="node() | @*" />
            </xsl:copy>
            <xsl:apply-templates select="following-sibling::*[1]" />
        </xsl:template>
    
    </xsl:stylesheet>
    

    Which, when run against your input, will create this output:

    <?xml version="1.0" encoding="UTF-8"?>
    <section>
       <ss_start/>
       <p id="ss_main">aa</p>
       <p id="ss_chap">bb</p>
       <ss_end/>
       <p id="main">cc</p>
       <p id="main">dd</p>
       <p id="main">ee</p>
       <ss_start/>
       <p id="ss_main">ff</p>
       <ss_end/>
       <p id="main">gg</p>
       <p id="main">hh</p>
       <p id="main">ii</p>
       <p id="main">jj</p>
       <ss_start/>
       <p id="ss_chap">xx</p>
       <p id="ss_main">yy</p>
       <p id="ss_chap">zz</p>
       <ss_end/>
    </section>
    

    I see now that you tagged your question with xslt-2.0, which means you can use grouping. I'll try to update with an example in XSLT 2.0.

    XSLT 2.0 group-adjacent

    In XSLT 2.0, you can use a boolean true/false as the the group-adjacent grouping key as follows, which is quite a bit shorter than the XSLT 1.0 code above:

    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
        version="2.0">
    
        <xsl:output indent="yes" />
    
        <xsl:template match="node() | @*">
            <xsl:copy>
                <xsl:apply-templates select="node() | @*" />
            </xsl:copy>
        </xsl:template>
    
        <xsl:template match="section">
            <xsl:copy>
                <xsl:for-each-group select="*" group-adjacent="starts-with(@id, 'ss')">
                    <xsl:if test="current-grouping-key()"><ss_start /></xsl:if>
                    <xsl:apply-templates select="current-group()" />
                    <xsl:if test="current-grouping-key()"><ss_end /></xsl:if>
                </xsl:for-each-group>
            </xsl:copy>
        </xsl:template>
    
    </xsl:stylesheet>