How to Simulate Loops and Tokenize Strings in XSLT 1.0?

XSLT 1.0 lacks native procedural loops (for, while) and built-in string splitting functions like XPath 2.0's fn:tokenize(). Because XSLT is a pure functional language with immutable variables, iterative tasks such as numeric counters and string tokenization must be implemented using recursive named templates. By passing updated parameters through successive template calls and defining precise base cases to prevent infinite recursion, developers can achieve robust iteration and string processing in legacy XSLT 1.0 environments.

Implementing Loop Counters with Named Recursion

To simulate a standard for loop that iterates from a start index to an end index, a named template calls itself with an incremented parameter until a termination condition is met.

The Recursive Counter Pattern

A recursive counter template requires three primary parameters:

During each invocation, the template tests whether i is less than or equal to count. If true, it executes the target body logic and recursively calls itself using <xsl:call-template>, passing i + step as the updated parameter.

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

  <!-- Entry Point -->
  <xsl:template match="/">
    <items>
      <xsl:call-template name="for-loop">
        <xsl:with-param name="i" select="1"/>
        <xsl:with-param name="count" select="5"/>
        <xsl:with-param name="step" select="1"/>
      </xsl:call-template>
    </items>
  </xsl:template>

  <!-- Recursive Loop Template -->
  <xsl:template name="for-loop">
    <xsl:param name="i" select="1"/>
    <xsl:param name="count"/>
    <xsl:param name="step" select="1"/>

    <xsl:if test="$i &lt;= $count">
      <!-- Body of the loop -->
      <item index="{$i}">
        <xsl:value-of select="concat('Iteration number ', $i)"/>
      </item>

      <!-- Recursive Step -->
      <xsl:call-template name="for-loop">
        <xsl:with-param name="i" select="$i + $step"/>
        <xsl:with-param name="count" select="$count"/>
        <xsl:with-param name="step" select="$step"/>
      </xsl:call-template>
    </xsl:if>
  </xsl:template>
</xsl:stylesheet>

Execution Flow and Termination

  1. The template evaluates the condition <xsl:if test="$i &lt;= $count">.
  2. When the condition holds, the payload node (<item>) is emitted.
  3. The parameter $i + $step is calculated and forwarded to the next frame.
  4. Once $i exceeds $count, the template exits without calling itself, unwinding the call stack.

String Tokenization via Recursive Substrings

String tokenization divides a delimited string (such as a comma-separated values list) into discrete substrings. In XSLT 1.0, this relies on three core XPath 1.0 string functions:

The Recursive Tokenizer Pattern

The template checks whether the input string contains the specified delimiter. If found, it outputs the token obtained via substring-before() and invokes itself recursively on the remainder retrieved via substring-after(). If the delimiter is not found, the base case handles the final remaining token.

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

  <!-- Entry Point -->
  <xsl:template match="/">
    <tokens>
      <xsl:call-template name="tokenize-string">
        <xsl:with-param name="text" select="'apple,banana,cherry,date'"/>
        <xsl:with-param name="delimiter" select="','"/>
      </xsl:call-template>
    </tokens>
  </xsl:template>

  <!-- Recursive Tokenizer Template -->
  <xsl:template name="tokenize-string">
    <xsl:param name="text"/>
    <xsl:param name="delimiter" select="','"/>

    <xsl:choose>
      <!-- Recursive Case: Delimiter is present -->
      <xsl:when test="contains($text, $delimiter)">
        <token>
          <xsl:value-of select="substring-before($text, $delimiter)"/>
        </token>
        
        <xsl:call-template name="tokenize-string">
          <xsl:with-param name="text" select="substring-after($text, $delimiter)"/>
          <xsl:with-param name="delimiter" select="$delimiter"/>
        </xsl:call-template>
      </xsl:when>

      <!-- Base Case: Delimiter absent, process final token -->
      <xsl:otherwise>
        <xsl:if test="string-length($text) &gt; 0">
          <token>
            <xsl:value-of select="$text"/>
          </token>
        </xsl:if>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
</xsl:stylesheet>

Edge Cases and Normalization

When designing production-ready tokenizers in XSLT 1.0, accounting for anomalies in the raw text stream ensures stable execution:

Best Practices and Stack Depth Considerations

Because XSLT 1.0 processors execute on varied runtimes (such as libxslt, MSXML, or Saxon 6.5), recurring template calls consume call stack frames unless the processor supports tail-call optimization.