Search code examples
xmlxsltxslt-2.0xsl-grouping

Group-adjacent nodes using xslt


Questions concerning the group-adjacent function have already been asked, I know. For some reason though, I can't get this to work.

I inherited project at work that uses xslt and my boss doesn't appreciate how steep the learning curve is for learning xslt and how dissimilar it is from procedural programming.

I'm nonetheless determined to learn it as well as I can, but I still have to work with it while I'm learning. Any help in the solving the below problem would be enormously appreciated.

I have an xml document that's formatted as such:

<document xml:lang="EN">
  <CRPg>text</CRPg>
  <CRPg>text</CRPg>
  <CRPg>text</CRPg>
  <Section/> <!--section marker-->
  <TOCChap>chapter title</TOCChap>
  <TOCAu>chapter author</TOCAu>
  <TOCChap>chapter title</TOCChap>
  <TOCAu>chapter author</TOCAu>
  <TOCChap>chapter title</TOCChap>
  <TOCAu>chapter author</TOCAu>
  <TOCChap>chapter title</TOCChap>
  <TOCAu>chapter author</TOCAu>
  <Section/> <!--section marker-->
  <Txt>body text</Txt>
  <Txt>body text</Txt>
  <Txt>body text</Txt>
  <Txt>body text</Txt>
</document>

It has basically no hierarchy at the moment, and the markers are included to give it structure and are included throughout the file. I'm trying to group all TOC elements and nest them within a containing element:

<document xml:lang="EN">
  <CRPg>text</CRPg>
  <CRPg>text</CRPg>
  <CRPg>text</CRPg>
  <Section/> <!--section marker-->
  <Wrapper> <!-- create new element to wrap around TOC and TOCAuth -->
   <TOCChap>chapter title</TOCChap>
   <TOCAu>chapter author</TOCAu>
   <TOCChap>chapter title</TOCChap>
   <TOCAu>chapter author</TOCAu>
   <TOCChap>chapter title</TOCChap>
   <TOCAu>chapter author</TOCAu>
   <TOCChap>chapter title</TOCChap>
   <TOCAu>chapter author</TOCAu>
  </Wrapper>
  <Section/> <!--section marker-->
  <Txt>body text</Txt>
  <Txt>body text</Txt>
  <Txt>body text</Txt>
  <Txt>body text</Txt>
</document>

This is proving more difficult than I had anticipated. I've tried many different versions of the below code, and I've pasted in the one that seems the least wrong. I realize it's still pretty bad:

<xsl:template match="*">
<xsl:for-each-group select="TOCChap | TOCAuth" group-adjacent="self::TOCChap | self::TOCAuth">
  <xsl:choose>
    <xsl:when test="current-grouping-key()">
      <wrapper>
        <xsl:apply-templates select="current-group()"/>

      </wrapper>
    </xsl:when>
    <xsl:otherwise>
      <xsl:apply-templates select="current-group()"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:for-each-group>

Thank you in advance.


Solution

  • There are three reasons why your attempt doesn't work:

    1. You are selecting the wrong set of nodes;

    2. You are using the union operator | instead of the logical operator or;

    3. There is no element named TOCAuth in your document.

    Once you fix these three problems by changing the instruction to:

    <xsl:for-each-group select="*" group-adjacent="self::TOCChap or self::TOCAu">
    

    you will see exactly the result you're looking for - see a complete demo here: http://xsltransform.net/jyH9rME

    --
    BTW, your case is almost exactly identical to the example given in the XSLT 2.0 specification for using group-adjacent - see the last example here: http://www.w3.org/TR/xslt20/#grouping-examples


    P.S.

    I cannot help wondering why don't you take advantage of the <Section/> markers to place all your groups inside a wrapper - for example, applying:

    XSLT 2.0

    <xsl:stylesheet version="2.0" 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" version="1.0" encoding="utf-8" indent="yes"/>
    
    <!-- identity transform -->
    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>
    
    <xsl:template match="/document">
        <xsl:copy>
            <xsl:apply-templates select="@*" />
            <xsl:for-each-group select="*" group-starting-with="Section">
                <wrapper>
                    <xsl:apply-templates select="current-group()" />
                </wrapper>  
            </xsl:for-each-group>
        </xsl:copy>
    </xsl:template>
    
    <xsl:template match="Section"/>
    
    </xsl:stylesheet>
    

    to your input would result in:

    :<?xml version="1.0" encoding="utf-8"?>
    <document xml:lang="EN">
       <wrapper>
          <CRPg>text</CRPg>
          <CRPg>text</CRPg>
          <CRPg>text</CRPg>
       </wrapper>
       <wrapper>
          <TOCChap>chapter title</TOCChap>
          <TOCAu>chapter author</TOCAu>
          <TOCChap>chapter title</TOCChap>
          <TOCAu>chapter author</TOCAu>
          <TOCChap>chapter title</TOCChap>
          <TOCAu>chapter author</TOCAu>
          <TOCChap>chapter title</TOCChap>
          <TOCAu>chapter author</TOCAu>
       </wrapper>
       <wrapper>
          <Txt>body text</Txt>
          <Txt>body text</Txt>
          <Txt>body text</Txt>
          <Txt>body text</Txt>
       </wrapper>
    </document>