Search code examples
xmlxsltgroupingword-wrap

Using for-each-group with group-adjacent for grouping and wrapping in XSLT 2


Obviously my understandig of how grouping in XSLT 2 works is not good enough. I´m sure that the approach with xsl:for-each-group with group-adjacent as attribute is right. However, I dont get the wanted result.

I want to group several adjacent elements in my XML source. The elements are identified through attribute values of a descendant element. I need to wrap them with a new element (e.g. p div="lernz"). Sometimes these elements appear as neighbours, sometimes not.

I did not get the right result with my stylesheet. Sometimes the element before the first item of what I think should be a group is matched. But in my results so far every single item of what I think is a group is wrapped. Where is the mistake? Any help is greatly appreciated.

XML Input

 <wx:sect>
  <w:p>
    <w:pPr w:val = 'Lern'/>
  </w:p>
  <w:p>
    <w:pPr w:val = 'Lern'/>
  </w:p>
  <w:p>
    <w:pPr w:val = 'Merk'/>
  </w:p>
  <w:p>
    <w:pPr w:val = 'Lern'/>
  </w:p>
  <w:p>
    <w:pPr w:val = 'Merk'/>
  </w:p>
</w:sect>

Desired Output

<wx:sect>
   <p div="wrapper-lernz">
       <w:p>
          <w:pPr w:val = 'Lern'/>
        </w:p>
        <w:p>
          <w:pPr w:val = 'Lern'/>
        </w:p>
    </p>
        <w:p>
          <w:pPr w:val = 'Merk'/>
        </w:p>
    <p div="wrapper-lernz">
      <w:p>
        <w:pPr w:val = 'Lern'/>
      </w:p>
    </p>
      <w:p>
        <w:pPr w:val = 'Merk'/>
      </w:p>
</w:sect>

My Stylesheet (part of it)

<xsl:template match="//w:p[w:pPr[@w:val]]">
    <xsl:for-each-group select="current()" group-adjacent="//@w:val = 'Lern">
         <p div="wrapper-lernz">
             <xsl:value-of select="current-group()"/>
          </p>
        </xsl:for-each-group>
    </xsl:template>

Solution

  • Your template matches w:p, but if you want to group adjacent elements, you should be matching the parent w:sect tag. By doing xsl:for-each-group select="current()" you are trying to group a single element.

    To handle only putting the "lern" elements in a container tag, you should group-adjacent= on the the "w:val" value, and then have an xsl:choose to decide whether to add a parent tag.

    Try this template:

    <xsl:template match="//w:sect[w:p[w:pPr/@w:val]]">
      <xsl:copy>
        <xsl:for-each-group select="w:p" group-adjacent="w:pPr/@w:val">
          <xsl:choose>
            <xsl:when test="current-grouping-key() = 'Lern'">
              <p div="wrapper-lernz">
                 <xsl:copy-of select="current-group()"/>
              </p>
            </xsl:when>
            <xsl:otherwise>
              <xsl:copy-of select="current-group()"/>
            </xsl:otherwise>
          </xsl:choose>
        </xsl:for-each-group>
      </xsl:copy>
    </xsl:template>
    

    (Note it also does xsl:copy-of instead of xsl:value-of as xsl:value-of just returns the text value of a node, not the node itself)