How to embed elements between 2 siblings in a new elemt replacing the siblings via XSLT?
Having the following XML:
<body> <p class="infobox_start" /> <h1>Header Inside</h1> <p>A paragraph inside an infobox</p> <ul> <li>Item One Inside</li> <li>Item Two Inside</li> </ul> <p class="infobox_end" /> <p>A paragraph outside of an infobox</p> <h1>Header Outside</h1> <ul> <li>Item One Outside</li> <li>Item Two Outside</li> </ul> <p class="infobox_start" /> <h1>Header B Inside</h1> <p>A paragraph inside an infobox</p> <ul> <li>Item A Inside</li> <li>Item B Inside</li> </ul> <p class="infobox_end" /> </body>
I would like to get:
<body> <div class="infobox"> <h1>Header Inside</h1> <p>A paragraph inside an infobox</p> <ul> <li>Item One Inside</li> <li>Item Two Inside</li> </ul> </div> <p>A paragraph outside of an infobox</p> <h1>Header Outside</h1> <ul> <li>Item One Outside</li> <li>Item Two Outside</li> </ul> <div class="infobox"> <h1>Header B Inside</h1> <p>A paragraph inside an infobox</p> <ul> <li>Item A Inside</li> <li>Item B Inside</li> </ul> </div> </body>
that means I want to wrap what is in between class="infobox_start"
and class="infobox_end"
paragraphs into div
elements, replacing these 2 placeholder paragraphs.
The XSLT with which I tried to achieve this (XSLT 2.0) is:
<xsl:template match="/body"> <body> <xsl:apply-templates /> </body> </xsl:template> <xsl:template match="/body/p[@class='infobox_start']"> <xsl:variable name="infostart" select="." /> <xsl:variable name="infoend" select="./following-sibling::p[@class='infobox_end'][1]" /> <div> <xsl:apply-templates select="$infostart/following::*[. << $infoend]" /> </div> </xsl:template> <xsl:template match="h1"> <h1><xsl:apply-templates /></h1> </xsl:template> <xsl:template match="p"> <p><xsl:apply-templates /></p> </xsl:template> <xsl:template match="ul"> <ul><xsl:apply-templates /></ul> </xsl:template> <xsl:template match="li"> <li><xsl:apply-templates /></li> </xsl:template>
Unfortunately, the result ist:
<?xml version="1.0" encoding="UTF-8"?> <body> <div> <h1>Header Inside</h1> <p>A paragraph inside an infobox</p> <ul> <li>Item One Inside</li> <li>Item Two Inside</li> </ul> <li>Item One Inside</li> <li>Item Two Inside</li> </div> <h1>Header Inside</h1> <p>A paragraph inside an infobox</p> <ul> <li>Item One Inside</li> <li>Item Two Inside</li> </ul> <p/> <p>A paragraph outside of an infobox</p> <h1>Header Outside</h1> <ul> <li>Item One Outside</li> <li>Item Two Outside</li> </ul> <div> <h1>Header B Inside</h1> <p>A paragraph inside an infobox</p> <ul> <li>Item A Inside</li> <li>Item B Inside</li> </ul> <li>Item A Inside</li> <li>Item B Inside</li> </div> <h1>Header B Inside</h1> <p>A paragraph inside an infobox</p> <ul> <li>Item A Inside</li> <li>Item B Inside</li> </ul> <p/> </body>
While I managed to replace the placeholder paragraphs and embed the content into divs, the problem is that with this ‘solution’ templates are applied 2-times to the list items inside div-elements and two times to the whole content between the placeholders. What solution would deliver me the desired result shown above without the whole repition?
I think, as soon as you want to process several such elements and wrap contents, it is a task for for-each-group group-starting-with/group-ending-with
, here as an XSLT 3 sample:
<?xml version="1.0" encoding="utf-8"?> <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0" xmlns:xs="http://www.w3.org/2001/XMLSchema" exclude-result-prefixes="#all" expand-text="yes"> <xsl:mode on-no-match="shallow-copy"/> <xsl:template match="body"> <xsl:copy> <xsl:for-each-group select="*" group-starting-with="p[@class = 'infobox_start']"> <xsl:choose> <xsl:when test="self::p[@class = 'infobox_start']"> <xsl:for-each-group select="current-group()" group-ending-with="p[@class = 'infobox_end']"> <xsl:choose> <xsl:when test="current-group()[last()][self::p[@class = 'infobox_end']]"> <div class="infobox"> <xsl:apply-templates select="current-group()"/> </div> </xsl:when> <xsl:otherwise> <xsl:apply-templates select="current-group()"/> </xsl:otherwise> </xsl:choose> </xsl:for-each-group> </xsl:when> <xsl:otherwise> <xsl:apply-templates select="current-group()"/> </xsl:otherwise> </xsl:choose> </xsl:for-each-group> </xsl:copy> </xsl: template> <xsl:template match="p[@class = ('infobox_start', 'infobox_end')]"/> </xsl:stylesheet>
For an XSLT 2 processor like older versions of Saxon 9 you would need to remove the xsl:mode
declaration and spell out the identity transformation template instead.