Home > Software design >  Is there a way to match a template spanning multiple XML nodes in XSLT?
Is there a way to match a template spanning multiple XML nodes in XSLT?

Time:02-03

I have code in the form of XML that I want to transform into a simpler XML using XSLT 1.0. For example:

<CODE>
    <LINE>
        <OPERATOR>ASSIGN</OPERATOR>
        <PARAM1>I_NUMBER</PARAM1>
        <PARAM2>3</PARAM2>
    </LINE>
    <LINE>
        <OPERATOR>IFBEGIN</OPERATOR>
        <PARAM1>IS_TRUE</PARAM1>
        <PARAM2></PARAM2>
    </LINE>
    <LINE>
        <OPERATOR>ASSIGN</OPERATOR>
        <PARAM1>I_INT</PARAM1>
        <PARAM2>3</PARAM2>
    </LINE>
    <LINE>
        <OPERATOR>ADD</OPERATOR>
        <PARAM1>I_NUMBER</PARAM1>
        <PARAM2>I_INT</PARAM2>
    </LINE>
    <LINE>
        <OPERATOR>IFEND</OPERATOR>
        <PARAM1></PARAM1>
        <PARAM2></PARAM2>
    </LINE>
    <LINE>
        <OPERATOR>WRITE</OPERATOR>
        <PARAM1>I_NUMBER</PARAM1>
        <PARAM2></PARAM2>
    </LINE>
</CODE>

I want to transform it in such a way that each node of XML corresponds to a line of code, like so:

<CODE>
  <ASSIGN PARAM1=I_NUMBER PARAM2=3 />
  <IF PARAM1=IS_TRUE>
    <ASSIGN PARAM1=I_INT PARAM2=3 />
    <ADD PARAM1=I_NUMBER PARAM2=I_INT />
  </IF>
  <WRITE PARAM1=I_NUMBER />
<CODE>

I'm able to take the OPERATOR and make it into the element, but I'm having trouble with representing the IF blocks. My XSLT so far:

<xsl:template match="/">
    <CODE>
        <xsl:apply-templates/>
    </CODE>
</xsl:template>

<xsl:template match="LINE[.//OPERATOR[starts-with(.,'IFBEGIN')]]">
    <IF>
      <xsl:apply-templates select="following-sibling::LINE[1][not(OPERATOR[starts-with(.,'IFEND')])]"/>
    </IF>
</xsl:template>

<xsl:template match="LINE" >
  <xsl:element name="{OPERATOR}">
    <xsl:if test="PARAM1"><xsl:attribute name="Param1"><xsl:value-of select="PARAM1"/></xsl:attribute></xsl:if>
    <xsl:if test="PARAM2"><xsl:attribute name="Param2"><xsl:value-of select="PARAM2"/></xsl:attribute></xsl:if>
  </xsl:element>
</xsl:template>

This is making an IF block, but it is duplicating the elements within below.

Is what I'm trying to do possible?

CodePudding user response:

The following stylesheet in XSLT 1.0 does the job.

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:output method="xml" indent="yes"/>
  <xsl:template match="CODE">
    <xsl:copy>
      <xsl:apply-templates select="LINE[1]"/>
    </xsl:copy>
  </xsl:template>
  <xsl:template match="LINE" mode="attribute">
    <xsl:attribute name="PARAM1">
      <xsl:value-of select="PARAM1"/>
    </xsl:attribute>
    <xsl:if test="string(PARAM2)"> <!-- only if PARAM2 is non-empty -->
      <xsl:attribute name="PARAM2">
        <xsl:value-of select="PARAM2"/>
      </xsl:attribute>
    </xsl:if>
  </xsl:template>
  <xsl:template match="LINE[OPERATOR='IFBEGIN']">
    <IF>
      <xsl:apply-templates select="." mode="attribute"/>
      <xsl:apply-templates select="following-sibling::LINE[1]"/>
    </IF>
    <!-- Now continue after the matching IFEND -->
    <xsl:apply-templates select="following-sibling::LINE[OPERATOR='IFBEGIN' or OPERATOR='IFEND'][1]"
      mode="balance">
      <xsl:with-param name="balance" select="0"/>
    </xsl:apply-templates>
  </xsl:template>
  <xsl:template match="LINE[OPERATOR='IFEND']"/>
  <xsl:template match="LINE[OPERATOR='IFBEGIN']" mode="balance">
    <xsl:param name="balance"/>
    <xsl:apply-templates select="following-sibling::LINE[OPERATOR='IFBEGIN' or OPERATOR='IFEND'][1]"
      mode="balance">
      <xsl:with-param name="balance" select="$balance   1"/>
    </xsl:apply-templates>
  </xsl:template>
  <xsl:template match="LINE[OPERATOR='IFEND']" mode="balance">
    <xsl:param name="balance"/>
    <xsl:choose>
      <xsl:when test="$balance = 0">
        <xsl:apply-templates select="following-sibling::LINE[1]"/>
      </xsl:when>
      <xsl:otherwise>
        <xsl:apply-templates select="following-sibling::LINE[OPERATOR='IFBEGIN' or OPERATOR='IFEND'][1]"
          mode="balance">
          <xsl:with-param name="balance" select="$balance - 1"/>
        </xsl:apply-templates>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
  <xsl:template match="LINE">
    <xsl:element name="{OPERATOR}">
      <xsl:apply-templates select="." mode="attribute"/>
    </xsl:element>
    <xsl:apply-templates select="following-sibling::LINE[1]"/>
  </xsl:template>
</xsl:stylesheet>

The most noteworthy point is that the LINE elements are not selected as one nodeset (what you call "spanning multiple nodes"), because some of them must be surrounded by an <IF> element, but you cannot create the opening <IF> during one template invocation and the closing </IF> during another.

Instead, only one LINE element is selected at a time and the next one is selected within the template for the current one.

The next most noteworthy point is the mechanism in the IFBEGIN template to continue after the matching IFEND. This is necessary for <IF> elements to be nested.

(An alternative approach would be to use <xsl:output method="text"> and render the XML yourself. Then you could output an <IF> in one template invocation and an </IF> in another. But I would consider that against the spirit of XSLT. Hadn't you posted an earlier question where you tried that?)

CodePudding user response:

This is possible, but not trivial, in XSLT 1.0.

One way to approach this is a technique known as "sibling recursion". In this case, it could be implemented as follows:

XSLT 1.0

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

<xsl:template match="/CODE">
    <CODE>
        <!-- start sibling recursion -->
        <xsl:apply-templates select="LINE[1]"/>
    </CODE>
</xsl:template>

<xsl:template match="LINE" >
    <xsl:element name="{OPERATOR}">
        <xsl:apply-templates select="*[starts-with(name(), 'PARAM')][text()]"/>
    </xsl:element>
    <!-- continue sibling recursion; stop if reached end of IF block -->
    <xsl:apply-templates select="following-sibling::LINE[1][not(OPERATOR = 'IFEND')]"/>
</xsl:template>

<xsl:template match="LINE[OPERATOR = 'IFBEGIN']">
    <IF>
        <xsl:apply-templates select="*[starts-with(name(), 'PARAM')][text()]"/>
        <!-- start sibling recursion inside IF block -->
        <xsl:apply-templates select="following-sibling::LINE[1][not(OPERATOR = 'IFEND')]"/>
    </IF>
    <!-- continue sibling recursion with end of IF block -->
    <xsl:apply-templates select="following-sibling::LINE[OPERATOR = 'IFEND'][1]"/>    
</xsl:template>

<xsl:template match="LINE[OPERATOR = 'IFEND']">
    <!-- resume sibling recursion outside of IF block -->
    <xsl:apply-templates select="following-sibling::LINE[1]"/>
</xsl:template>

<xsl:template match="*[starts-with(name(), 'PARAM')]">
    <xsl:attribute name="Param{substring-after(name(), 'PARAM')}">
        <xsl:value-of select="."/>
    </xsl:attribute>
</xsl:template>

</xsl:stylesheet>

However, this assumes that IF blocks cannot be nested. If this assumption is not true, then it gets a little more complicated:

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

<xsl:template match="/CODE">
    <CODE>
        <!-- start sibling recursion -->
        <xsl:apply-templates select="LINE[1]"/>
    </CODE>
</xsl:template>

<xsl:template match="LINE" >
    <xsl:element name="{OPERATOR}">
        <xsl:apply-templates select="*[starts-with(name(), 'PARAM')][text()]"/>
    </xsl:element>
    <!-- continue sibling recursion; stop if reached end of IF block -->
    <xsl:apply-templates select="following-sibling::LINE[1][not(OPERATOR = 'IFEND')]"/>
</xsl:template>

<xsl:template match="LINE[OPERATOR = 'IFBEGIN']">
    <IF>
        <xsl:apply-templates select="*[starts-with(name(), 'PARAM')][text()]"/>
        <!-- start sibling recursion inside IF block -->
        <xsl:apply-templates select="following-sibling::LINE[1][not(OPERATOR = 'IFEND')]"/>
    </IF>
    <!-- continue sibling recursion with end of current IF block -->
    <xsl:apply-templates select="following-sibling::LINE[OPERATOR = 'IFEND'][count(following-sibling::LINE[OPERATOR = 'IFEND']) = count(current()/preceding-sibling::LINE[OPERATOR = 'IFBEGIN'])]"/>    
</xsl:template>

<xsl:template match="LINE[OPERATOR = 'IFEND']">
    <!-- resume sibling recursion outside of IF block -->
    <xsl:apply-templates select="following-sibling::LINE[1][not(OPERATOR = 'IFEND')]"/>
</xsl:template>

<xsl:template match="*[starts-with(name(), 'PARAM')]">
    <xsl:attribute name="Param{substring-after(name(), 'PARAM')}">
        <xsl:value-of select="."/>
    </xsl:attribute>
</xsl:template>

</xsl:stylesheet>
  • Related