user2850751
user2850751

Reputation: 53

Is there a way to match a template spanning multiple XML nodes in XSLT?

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?

Upvotes: 0

Views: 215

Answers (4)

michael.hor257k
michael.hor257k

Reputation: 117073

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 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]"/>
    </IF>
    <!-- continue sibling recursion with end of current IF block -->
    <xsl:variable name="prev" select="count(preceding-sibling::LINE[OPERATOR = 'IFBEGIN']) - count(preceding-sibling::LINE[OPERATOR = 'IFEND'])" />
    <xsl:apply-templates select="following-sibling::LINE[OPERATOR = 'IFEND'][count(preceding-sibling::LINE[OPERATOR = 'IFBEGIN']) - count(preceding-sibling::LINE[OPERATOR = 'IFEND']) = $prev + 1][1]"/>    
</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>

Upvotes: 1

MrD at KookerellaLtd
MrD at KookerellaLtd

Reputation: 2797

A 'functional' style XSLT 1.0 solution (with nodesets).

I must admit I don't think XSLT is well suited to this sort of thing, nodesets (like iterators in imperative languages) don't sit nicely (at least with me) with this sort of recursive type problem, where functional Lists do, so the strategy is to map the data to a functional list, and then after that we no longer need to construct complex axis queries with predicates. It doesnt require knowledge of "sibling recursion" (which is unknown to me), its mainstream functional style programming.

So this is a very unXSLT style solution, I'll include the full code and then explain loosely what its doing below.

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

    <xsl:template match="/">
        <CODE>
            <xsl:variable name="codeAsListRTF">
                <xsl:apply-templates select="CODE/LINE[1]" mode="toList"/>
            </xsl:variable>
            <xsl:variable name="return">
                <xsl:apply-templates select="msxsl:node-set($codeAsListRTF)/LINE[1]" mode="run"/>
            </xsl:variable>
            <xsl:copy-of select="msxsl:node-set($return)/RETURN/RESULT/*"/>
        </CODE>
    </xsl:template>

    <xsl:template match="LINE" mode="toList">
        <LINE>
            <xsl:copy-of select="*"/>
            <NEXT>
                <xsl:apply-templates select="following-sibling::LINE[1]" mode="toList"/>
            </NEXT>
        </LINE>
    </xsl:template>

    <xsl:template match="LINE" mode="run">
        <xsl:variable name="resultAndPcRTF">
            <xsl:apply-templates select="." mode="step"/>
        </xsl:variable>
        <xsl:variable name="resultAndPc" select="msxsl:node-set($resultAndPcRTF)/RETURN"/>
        <xsl:choose>
            <xsl:when test="$resultAndPc/NEXT/LINE">
                <RETURN>
                    <xsl:variable name="tailResultRTF">
                        <xsl:apply-templates select="$resultAndPc/NEXT/LINE" mode="run"/>
                    </xsl:variable>
                    <RESULT>
                        <xsl:copy-of select="$resultAndPc/RESULT/*"/>
                        <xsl:copy-of select="msxsl:node-set($tailResultRTF)/RETURN/RESULT/*"/>
                    </RESULT>
                    <xsl:copy-of select="msxsl:node-set($tailResultRTF)/RETURN/NEXT"/>
                </RETURN>
            </xsl:when>
            <xsl:otherwise>
                <RETURN>
                    <RESULT>
                        <xsl:copy-of select="$resultAndPc/RESULT/*"/>
                    </RESULT>
                    <xsl:copy-of select="NEXT"/>
                </RETURN>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>

    <xsl:template match="LINE[OPERATOR = 'ASSIGN']" mode="step">
        <RETURN>
            <RESULT>
                <ASSIGN PARAM1="{PARAM1}" PARAM2="{PARAM2}" />
            </RESULT>
            <xsl:copy-of select="NEXT"/>
        </RETURN>
    </xsl:template>

    <xsl:template match="LINE[OPERATOR = 'ADD']" mode="step">
        <RETURN>
            <RESULT>
                <ADD PARAM1="{PARAM1}" PARAM2="{PARAM2}" />
            </RESULT>
            <xsl:copy-of select="NEXT"/>
        </RETURN>
    </xsl:template>

    <xsl:template match="LINE[OPERATOR = 'WRITE']" mode="step">
        <RETURN>
            <RESULT>
                <WRITE PARAM1="{PARAM1}" PARAM2="{PARAM2}" />
            </RESULT>
            <xsl:copy-of select="NEXT"/>
        </RETURN>
    </xsl:template>

    <xsl:template match="LINE[OPERATOR = 'IFBEGIN']" mode="step">
        <xsl:variable name="resultsRTF">
            <xsl:apply-templates select="NEXT/LINE" mode="run"/>
        </xsl:variable>
        <xsl:variable name="results" select="msxsl:node-set($resultsRTF)/*"/>
        <RETURN>
            <RESULT>
                <IF PARAM1="{PARAM1}" PARAM2="{PARAM2}">
                    <xsl:copy-of select="$results/RESULT/*"/>
                </IF>
            </RESULT>
            <xsl:copy-of select="$results/NEXT"/>
        </RETURN>
    </xsl:template>

    <xsl:template match="LINE[OPERATOR = 'IFEND']" mode="step">
        <RETURN>
            <RESULT/>
            <NEXT/>
        </RETURN>
    </xsl:template>
</xsl:stylesheet> 

how it works

the solution 1st maps the input code data in to a functional list (ie. a recursive structure, with data and a 'next' element).

e.g. it takes data like this (note this has a nested if - which is tricky)

<?xml version="1.0" encoding="utf-8"?>
<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>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>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>

and maps it to a functional style nested list, rather than an xml set of elements.

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

now we can process the data as a data structure that 'knows' what follows rather than using XSLT style axis to navigate them, so we can process things in a functional manner, letting the data structure and the stack manage our position in the list.

There is then basically a 'step' function (implemented as match templates), that maps an input line, to an output line + the next line to process.

There is then a 'run' function, that steps through a line recursively and aggregates the results.

I suspect if I wrote this in a functional language and then ported it to XSLT, it may be possible to tidy it up a bit.

I'd also worry about it processing large inputs, the stack depth will be the length of the list (I think), and I'm not too sure how efficient some of the copying is (it may be o(n^2)).

Upvotes: 0

Heiko Thei&#223;en
Heiko Thei&#223;en

Reputation: 16892

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"/>
      <!-- Recursion starts here -->
      <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']">
    <!-- Recursion ends here -->
  </xsl:template>
  <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>
    <!-- Recursion continues here -->
    <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?)

Upvotes: 1

michael.hor257k
michael.hor257k

Reputation: 117073

Here is a different approach that performs the transformation in two passes:

  • In the first pass, each LINE element is assigned an id and a parent-id attribute;
  • In the second pass, these ids are used to build a nested hierarchy by linking each parent to its children recursively:

XSLT 1.0 (+ node-set() extension function)

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

<xsl:key name="child" match="LINE" use="@parent-id" />

<xsl:template match="/CODE">
    <!-- assign id and parent-id to each LINE -->
    <xsl:variable name="first-pass">
        <xsl:call-template name="first-pass">
            <xsl:with-param name="nodes" select="LINE"/>
            <xsl:with-param name="ancestors" select="."/>
        </xsl:call-template>
    </xsl:variable>
    <!-- create nested output -->
    <CODE>
        <xsl:variable name="root-id" select="generate-id()" />
        <xsl:for-each select="exsl:node-set($first-pass)">
            <xsl:apply-templates select="key('child', $root-id)"/>
        </xsl:for-each>
    </CODE>
</xsl:template>

<xsl:template name="first-pass">
    <xsl:param name="nodes"/>
    <xsl:param name="ancestors"/>
    <xsl:if test="$nodes">
        <xsl:variable name="current-node" select="$nodes[1]" />
        <!-- add the original node's id and its parent id -->
        <LINE id="{generate-id($current-node)}" parent-id="{generate-id($ancestors[last()])}">
            <xsl:copy-of select="$current-node/*"/>
        </LINE>
        <!-- prepare next ancestors -->
        <xsl:variable name="base" select="$ancestors[position() != last()]"/>
        <xsl:variable name="last" select="$ancestors[last()][not($current-node/OPERATOR = 'IFEND')]"/>
        <xsl:variable name="curr" select="$current-node[OPERATOR = 'IFBEGIN']"/>
        <!-- recursive call -->
        <xsl:call-template name="first-pass">
            <xsl:with-param name="nodes" select="$nodes[position() > 1]"/>
            <xsl:with-param name="ancestors" select="$base | $last | $curr"/>
        </xsl:call-template>
    </xsl:if>
</xsl:template>

<xsl:template match="LINE">
    <xsl:element name="{OPERATOR}">
        <xsl:apply-templates select="*[starts-with(name(), 'PARAM')][text()]"/>
        <xsl:apply-templates select="key('child', @id)"/>
    </xsl:element>
</xsl:template>

<xsl:template match="LINE[OPERATOR = 'IFBEGIN']">
    <IF>
        <xsl:apply-templates select="*[starts-with(name(), 'PARAM')][text()]"/>
        <xsl:apply-templates select="key('child', @id)"/>
    </IF>
</xsl:template>

<xsl:template match="LINE[OPERATOR = 'IFEND']"/>

<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>

Upvotes: 0

Related Questions