Previous Page Next Page

Recipe 5.3. Selecting Nodes by Context

Problem

You want to select nodes that are bracketed by preceding and following nodes.

Solution

XSLT 1.0

There are several ways to solve this problem in XSLT 1.0, but the easiest to understand computes the position of the nodes that should be selected at each step. If you had the following unstructured document and you desired to select the paragraphs that are bracketed by headings, you can use this technique:

<doc>
  <heading>Structure, I don't need any stinkin structure</heading>
  <para>1.1</para>
  <para>1.2</para>
  <para>1.3</para>
  <para>1.4</para>
  <heading>Confessions of a flat earther</heading>
  <para>2.1</para>
  <para>2.2</para>
  <para>2.3</para>
  <heading>Flat hierarchies save trees!</heading>
  <para>3.1</para>
  <para>3.2</para>
</doc>

 <xsl:template match="/doc">
    <xsl:copy>
      <!-- First select the bracketing elements -->
      <xsl:apply-templates select="heading"/>
    </xsl:copy>
 </xsl:template>
 
<!-- Match on the bracketing elements --> 
 <xsl:template match="heading">
  <!-- Compute how many of the desired elements (para) follow this heading -->
  <xsl:variable name="numFollowingPara" select="count(following-sibling::para)"/>
  
  <!-- Compute how many of the desired elements (para) follow the next heading 
       and subtract from the preceding count to get the position of the last
       para in this group-->
  <xsl:variable name="lastParaInHeading" 
      select="$numFollowingPara -
             count(following-sibling::heading[1]/following-sibling::para)"/>

    <!-- You now can select the desired elements by their position relative to 
         the current heading -->
  
    <xsl:apply-templates 
      select="following-sibling::para[position( ) &lt;= $lastParaInHeading]"/>

  </xsl:template>

XSLT 2.0

This problem is tailor-made for the for-each-group instruction. Specifically, you would use the group-starting-with attribute:

<xsl:template match="/doc">
  <xsl:copy>
      <xsl:for-each-group select="*" group-starting-with="heading">
        <!--Select the para elements in the group bracketed by heading -->
        <xsl:apply-templates select="current-group( )[self::para]"/> 
      </xsl:for-each-group>             
  </xsl:copy>
</xsl:template>

Discussion

Selecting nodes based on their position relative to other nodes is a common requirement in document-oriented XML transformations where structure is implied rather than literally encoded into the hierarchy of the document. Clearly, if each group consisting of a heading and paragraphs was contained in a separate parent element (for example, a section element), then the problem would be trivial. This is a classic trade-off between ease of use for the document creators vs. ease of use for the document transformers. With the introduction of for-each-group in XSLT 2.0, the trade-off equals out since you can much more easily deal with unstructured documents.

See Also

Recipe 8.8 shows applications of this technique for transforming implicitly structured documents into explicitly structured ones. It also shows other ways the problem can be approached in XSLT 1.0 and 2.0.


Previous Page Next Page