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:
i: The current index/counter value.count: The target upper bound.step: The increment value per iteration (typically defaulting to 1).
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 <= $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
- The template evaluates the condition
<xsl:if test="$i <= $count">. - When the condition holds, the payload node
(
<item>) is emitted. - The parameter
$i + $stepis calculated and forwarded to the next frame. - Once
$iexceeds$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:
contains($string, $delimiter): Checks if the delimiter exists within the string.substring-before($string, $delimiter): Extracts the token preceding the first delimiter match.substring-after($string, $delimiter): Extracts the remaining string following the first delimiter match.
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) > 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:
- Trailing Delimiters: A string like
'a,b,c,'produces an empty string in the base case unless guarded bystring-length($text) > 0. - Leading Delimiters: A string like
',a,b'results in an empty<token/>element initially. Usenormalize-space()or length checks inside the<xsl:when>block if empty tokens should be ignored. - Multi-Character Delimiters: The
contains(),substring-before(), andsubstring-after()functions support multi-character delimiters (e.g.,:::or\r\n) without requiring modification to the recursion logic.
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.
- Tail-Call Optimization (TCO): Keep the
<xsl:call-template>tag as the final operational instruction within the branch so conforming processors can reuse stack frames. - Large Datasets: For extremely large iterations
(thousands of items), linear recursion can exceed maximum call-stack
limits. In such scenarios, divide-and-conquer recursion (splitting lists
in halves) or pre-processing data into dummy node-sets for processing
via
<xsl:for-each>provides greater stack safety.