Search code examples
xsltxslt-1.0xslt-grouping

Using XSLT to wrap elements, but it is not including the last matching element


I'm trying to transform old XML files into a new and improved structure. Part of this needs me to wrap some loose elements into a parent container, as well as modify their children

Old structure

<monograph>
  <title>asdf</title>

  <dosage.sec id="dosage.sec.1">
    <dosage.sec>asgfd</dosage.sec>
    <dosage.sec>asgfd</dosage.sec>
  </dosage.sec>
  <dosage.sec id="dosage.sec.2">
    <dosage.sec>asgfd</dosage.sec>
    <dosage.sec>asgfd</dosage.sec>
  </dosage.sec>
  <dosage.sec id="dosage.sec.3">
    <dosage.sec>asgfd</dosage.sec>
    <dosage.sec>asgfd</dosage.sec>
  </dosage.sec>

  <products>
    <prod>sadf</prod>
    <prod>sadf</prod>
  </products>
</monograph>

New structure

<monograph>
  <title>asdf</title>

  <dosage>
    <dosage.sec id="dosage.sec.1">
      <dosage.qual>asgfd</dosage.qual>
      <dosage.qual>asgfd</dosage.qual>
    </dosage.sec>
    <dosage.sec id="dosage.sec.2">
      <dosage.qual>asgfd</dosage.qual>
      <dosage.qual>asgfd</dosage.qual>
    </dosage.sec>
    <dosage.sec id="dosage.sec.3">
      <dosage.qual>asgfd</dosage.qual>
      <dosage.qual>asgfd</dosage.qual>
    </dosage.sec>
  </dosage>

  <products>
    <prod>sadf</prod>
    <prod>sadf</prod>
  </products>
</monograph>

I found this answer and modified it a bit to suit my needs:

<!-- wrap dosage.sec elements in a dosage container -->
<xsl:template match="node()|@*" name="dosage.sec">
    <xsl:copy>
        <xsl:apply-templates select="node()|@*" />
    </xsl:copy>
</xsl:template>

<xsl:template match="monograph/dosage.sec[not(preceding-sibling::*[1][self::dosage.sec])]">
    <dosage>
        <xsl:call-template name="dosage.sec" />
        <xsl:apply-templates mode="copy" select="following-sibling::*[1][self::dosage.sec]" />
    </dosage>
</xsl:template>

<xsl:template match="monograph/dosage.sec" mode="copy">
    <xsl:call-template name="dosage.sec"/>
</xsl:template>


<!-- rename children dosage.sec -->
<xsl:template match="dosage.sec/dosage.sec">
    <dosage.qual>
        <xsl:apply-templates />
    </dosage.qual>
</xsl:template>

But my output is:

<monograph>
  <title>asdf</title>

  <dosage>
    <dosage.sec id="dosage.sec.1">
      <dosage.qual>asgfd</dosage.qual>
      <dosage.qual>asgfd</dosage.qual>
    </dosage.sec>
    <dosage.sec id="dosage.sec.2">
      <dosage.qual>asgfd</dosage.qual>
      <dosage.qual>asgfd</dosage.qual>
    </dosage.sec>
    <dosage.sec id="dosage.sec.3">
      <dosage.qual>asgfd</dosage.qual>
      <dosage.qual>asgfd</dosage.qual>
    </dosage.sec>
  </dosage>

  <dosage.sec id="dosage.sec.3">
    <dosage.qual>asgfd</dosage.qual>
    <dosage.qual>asgfd</dosage.qual>
  </dosage.sec>

  <products>
    <prod>sadf</prod>
    <prod>sadf</prod>
  </products>
</monograph>

I'm using PHP5's built-in XSLTProcessor object - all XML and XSL are version 1.0


Solution

  • I had a few other problems on the way - for one, I discovered that the dosage.sec elements were not in contiguous block and I was erroneously creating two dosage containers.

    These templates solved my problems:

    <!-- wrap dosage.sec elements in a dosage container -->
    <xsl:template match="monograph/dosage.sec" name="dosage.sec">
        <xsl:param name="drugname" />
    
        <xsl:copy>
            <xsl:apply-templates>
                <xsl:with-param name="drugname" select="$drugname" />
            </xsl:apply-templates>
        </xsl:copy>
    </xsl:template>
    
    <xsl:template match="monograph/dosage.sec[not(preceding::dosage.sec)]">
        <xsl:param name="drugname" />
    
        <dosage>
            <xsl:call-template name="dosage.sec">
                <xsl:with-param name="drugname" select="$drugname" />
            </xsl:call-template>
    
            <xsl:apply-templates mode="copy" select="following::dosage.sec">
                <xsl:with-param name="drugname" select="$drugname" />
            </xsl:apply-templates>
        </dosage>
    </xsl:template>
    
    <xsl:template match="monograph/dosage.sec" mode="copy">
        <xsl:param name="drugname" />
    
        <xsl:call-template name="dosage.sec">
            <xsl:with-param name="drugname" select="$drugname" />
        </xsl:call-template>
    </xsl:template>
    
    <xsl:template match="monograph/dosage.sec[(preceding::dosage.sec)]" />