xslt-1.0

parse XML without using global variables


I want to parse this kind of XML using XSLT 1.0 only:

<root>
    <Block id="1" inputPorts="2" outputPorts="3" />
    <OutputPort id="2" parent="1" /> <!-- first OutputPort after Block so current = 0, should give coordinate(0,3) = 1/6 -->
    <InputPort id="3" parent="1" /> <!-- first InputPort after Block so current = 0, should give coordinate(0,2) = 1/4 -->
    <OutputPort id="4" parent="1" /> <!-- second OutputPort after Block so current = 1, should give coordinate(1,3) = 3/6 -->
    <InputPort id="5" parent="1" /> <!-- second InputPort after Block so current = 1, should give coordinate(1,2) = 3/4 -->
    <OutputPort id="6" parent="1" /> <!-- third OutputPort after Block so current = 2, should give coordinate(2,3) of 5/6 -->

    <Block id="7" inputPorts="0" outputPorts="1" />
    <OutputPort id="8" parent="7" /> <!-- first OutputPort after Block so current = 0, should give coordinate(0,1) = 1/2 -->
</root>

There are multple Block tags. InputPort and OutputPort tags follow in any order after the Block tag, as per number available in attributes of Block. However, they are before the next Block tag.

The output should be like this:

<root>
    <Block id="1" inputPorts="2" outputPorts="3" />
    <OutputPort id="2" parent="1" coordinate="0.1666" />
    <InputPort id="3" parent="1" coordinate="0.25" />
    <OutputPort id="4" parent="1" coordinate="0.5" />
    <InputPort id="5" parent="1" coordinate="0.75" />
    <OutputPort id="6" parent="1" coordinate="0.8333" />

    <Block id="7" inputPorts="0" outputPorts="1" />
    <OutputPort id="8" parent="7" coordinate="0.5" />
</root>

The position of each port depends on the port number and the total number of ports. The basic formula is:

coordinate(current, total) = (2 * current + 1) / (2 * total)

How do I get the value of current and total in my template?

Each Block, InputPort, and OutputPort tags have their own templates.

What I have tried involves use of global variables. However, I have found out that global variables cannot be reassigned globally in XSLT 1.0. We can reassign new values inside templates, but that is of no use in other templates or even in the same template called for a different tag. Here is my solution with global variables:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:variable name="inputPorts" value="0" />
  <xsl:variable name="outputPorts" value="0" />
  <xsl:variable name="currentInputPort" value="0" />
  <xsl:variable name="currentOutputPort" value="0" />
  <xsl:output method="xml" indent="no" />
  <xsl:template match="@*|node()">
    <xsl:copy>
       <xsl:apply-templates select="@*|node()" />
    </xsl:copy>
  </xsl:template>
  <xsl:template match="comment()" />
  <xsl:template match="Block">
    <xsl:copy>
      <xsl:variable name="inputPorts">
        <xsl:value-of select="@inputPorts" />
      </xsl:variable>
      <xsl:variable name="outputPorts">
        <xsl:value-of select="@outputPorts" />
      </xsl:variable>
      <xsl:variable name="currentInputPort" value="0" />
      <xsl:variable name="currentOutputPort" value="0" />
      <xsl:attribute name="id">
        <xsl:value-of select="@id" />
      </xsl:attribute>
      <xsl:attribute name="inputPorts">
        <xsl:value-of select="@inputPorts" />
      </xsl:attribute>
      <xsl:attribute name="outputPorts">
        <xsl:value-of select="@outputPorts" />
      </xsl:attribute>
    </xsl:copy>
  </xsl:template>
  <xsl:template match="InputPort">
    <xsl:copy>
      <xsl:attribute name="id">
        <xsl:value-of select="@id" />
      </xsl:attribute>
      <xsl:attribute name="parent">
        <xsl:value-of select="@parent" />
      </xsl:attribute>
      <xsl:attribute name="coordinate">
        <xsl:value-of select="(2 * $currentInputPort + 1) div (2 * $inputPorts)" />
      </xsl:attribute>
      <xsl:variable name="currentInputPort" select="$currentInputPort + 1" />
    </xsl:copy>
  </xsl:template>
  <xsl:template match="OutputPort">
    <xsl:copy>
      <xsl:attribute name="id">
        <xsl:value-of select="@id" />
      </xsl:attribute>
      <xsl:attribute name="parent">
        <xsl:value-of select="@parent" />
      </xsl:attribute>
      <xsl:attribute name="coordinate">
        <xsl:value-of select="(2 * $currentOutputPort + 1) div (2 * $outputPorts)" />
      </xsl:attribute>
      <xsl:variable name="currentOutputPort" select="$currentOutputPort + 1" />
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>

Solution

  • If it is acceptable to output the ports of a block grouped by type, you could perform the transformation in a single pass:

    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:key name="k-in" match="InputPort" use="@parent" />
    <xsl:key name="k-out" match="OutputPort" use="@parent" />
    
    <xsl:template match="/root">
        <xsl:copy>
            <xsl:for-each select="Block">
                <xsl:variable name="inputPorts" select="@inputPorts" />
                <xsl:variable name="outputPorts" select="@outputPorts" />
                <xsl:copy-of select="."/>
                <!-- input ports -->
                <xsl:for-each select="key('k-in', @id)">
                    <xsl:copy>
                        <xsl:copy-of select="@*"/>
                        <xsl:variable name="numerator" select="2 * (position() - 1) + 1" />
                        <xsl:attribute name="coordinate">
                            <xsl:value-of select="$numerator div (2 * $inputPorts)"/>
                        </xsl:attribute>
                    </xsl:copy>
                </xsl:for-each>
                <!-- output ports -->
                <xsl:for-each select="key('k-out', @id)">
                    <xsl:copy>
                        <xsl:copy-of select="@*"/>
                        <xsl:variable name="numerator" select="2 * (position() - 1) + 1" />
                        <xsl:attribute name="coordinate">
                            <xsl:value-of select="$numerator div (2 * $outputPorts)"/>
                        </xsl:attribute>
                    </xsl:copy>
                </xsl:for-each>
            </xsl:for-each>
        </xsl:copy>
    </xsl:template>
    
    </xsl:stylesheet>
    

    Result

    <root>
      <Block id="1" inputPorts="2" outputPorts="3"/>
      <InputPort id="3" parent="1" coordinate="0.25"/>
      <InputPort id="5" parent="1" coordinate="0.75"/>
      <OutputPort id="2" parent="1" coordinate="0.166666666666667"/>
      <OutputPort id="4" parent="1" coordinate="0.5"/>
      <OutputPort id="6" parent="1" coordinate="0.833333333333333"/>
      <Block id="7" inputPorts="0" outputPorts="1"/>
      <OutputPort id="8" parent="7" coordinate="0.5"/>
    </root>