Here is a much simpler (no explicit conditional instructions, no user-defined functions, no modes) and shorter, working transformation:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="node()|@*">
<xsl:param name="pVars" as="element()*"/>
<xsl:copy>
<xsl:apply-templates select="node()|@*">
<xsl:with-param name="pVars" select="$pVars"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
<xsl:template match="for">
<xsl:param name="pVars" as="element()*"/>
<xsl:variable name="vCurrentFor" select="."/>
<xsl:for-each select="@from to @to">
<xsl:variable name="vnewVars">
<xsl:sequence select="$pVars"/>
<var name="{$vCurrentFor/@var}" value="{current()}"/>
</xsl:variable>
<xsl:apply-templates select="$vCurrentFor/node()">
<xsl:with-param name="pVars" select="$vnewVars/*"/>
</xsl:apply-templates>
</xsl:for-each>
</xsl:template>
<xsl:template match="text()[contains(., '$(')]">
<xsl:param name="pVars" as="element()*"/>
<xsl:analyze-string select="."
regex="\$\((.+?)\)">
<xsl:non-matching-substring>
<xsl:value-of select="."/>
</xsl:non-matching-substring>
<xsl:matching-substring>
<xsl:variable name="vName" select="regex-group(1)"/>
<xsl:variable name="vReplacement" select=
"$pVars[@name eq $vName][last()]/@value"/>
<xsl:sequence select="string($vReplacement)"/>
</xsl:matching-substring>
</xsl:analyze-string>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided XML document:
<Doc>
<stuff>
<for var="i" from="1" to="2">
<item>$(i)</item>
<for var="j" from="2" to="4">
<innerItem>$(j)</innerItem>
</for>
</for>
</stuff>
</Doc>
the wanted, correct result is produced:
<Doc>
<stuff>
<item>1</item>
<innerItem>2</innerItem>
<innerItem>3</innerItem>
<innerItem>4</innerItem>
<item>2</item>
<innerItem>2</innerItem>
<innerItem>3</innerItem>
<innerItem>4</innerItem>
</stuff>
</Doc>
It is possible to perform much more complicated processing:
Using together variables from differnt levels:
<Doc>
<stuff>
<for var="x" from="1" to="2">
<item>$(x)</item>
<for var="y" from="2" to="4">
<innerItem>$(x).$(y)</innerItem>
</for>
</for>
</stuff>
</Doc>
The result on this document is:
<Doc>
<stuff>
<item>1</item>
<innerItem>1.2</innerItem>
<innerItem>1.3</innerItem>
<innerItem>1.4</innerItem>
<item>2</item>
<innerItem>2.2</innerItem>
<innerItem>2.3</innerItem>
<innerItem>2.4</innerItem>
</stuff>
</Doc>
Or with this XML document:
<Doc>
<stuff>
<for var="x" from="1" to="2">
<item>
<value>$(x)</value>
<for var="y" from="2" to="4">
<innerItem>
<value>$(x).$(y)</value>
<for var="z" from="3" to="5">
<inner-most-Item>$(x).$(y).$(z)</inner-most-Item>
</for>
</innerItem>
</for>
</item>
</for>
</stuff>
</Doc>
the result is:
<Doc>
<stuff>
<item>
<value>1</value>
<innerItem>
<value>1.2</value>
<inner-most-Item>1.2.3</inner-most-Item>
<inner-most-Item>1.2.4</inner-most-Item>
<inner-most-Item>1.2.5</inner-most-Item>
</innerItem>
<innerItem>
<value>1.3</value>
<inner-most-Item>1.3.3</inner-most-Item>
<inner-most-Item>1.3.4</inner-most-Item>
<inner-most-Item>1.3.5</inner-most-Item>
</innerItem>
<innerItem>
<value>1.4</value>
<inner-most-Item>1.4.3</inner-most-Item>
<inner-most-Item>1.4.4</inner-most-Item>
<inner-most-Item>1.4.5</inner-most-Item>
</innerItem>
</item>
<item>
<value>2</value>
<innerItem>
<value>2.2</value>
<inner-most-Item>2.2.3</inner-most-Item>
<inner-most-Item>2.2.4</inner-most-Item>
<inner-most-Item>2.2.5</inner-most-Item>
</innerItem>
<innerItem>
<value>2.3</value>
<inner-most-Item>2.3.3</inner-most-Item>
<inner-most-Item>2.3.4</inner-most-Item>
<inner-most-Item>2.3.5</inner-most-Item>
</innerItem>
<innerItem>
<value>2.4</value>
<inner-most-Item>2.4.3</inner-most-Item>
<inner-most-Item>2.4.4</inner-most-Item>
<inner-most-Item>2.4.5</inner-most-Item>
</innerItem>
</item>
</stuff>
</Doc>
I will stop just here, but given a good design of this language, the possibilities are limitless.
UPDATE: The OP has asked:
"Is there a way to also allow the expansion inside attributes? for
example: <inner-most-Item id="$(i)">
"
Yes, this is quite easy -- just adding a new template matching attributes and refactoring code:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:my="my:my">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="node()|@*">
<xsl:param name="pVars" as="element()*"/>
<xsl:copy>
<xsl:apply-templates select="node()|@*">
<xsl:with-param name="pVars" select="$pVars"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
<xsl:template match="for">
<xsl:param name="pVars" as="element()*"/>
<xsl:variable name="vCurrentFor" select="."/>
<xsl:for-each select="@from to @to">
<xsl:variable name="vnewVars">
<xsl:sequence select="$pVars"/>
<var name="{$vCurrentFor/@var}" value="{current()}"/>
</xsl:variable>
<xsl:apply-templates select="$vCurrentFor/node()">
<xsl:with-param name="pVars" select="$vnewVars/*"/>
</xsl:apply-templates>
</xsl:for-each>
</xsl:template>
<xsl:template match="text()[contains(., '$(')]">
<xsl:param name="pVars" as="element()*"/>
<xsl:value-of select="my:evalText($pVars, .)"/>
</xsl:template>
<xsl:template match="@*[contains(., '$(')]">
<xsl:param name="pVars" as="element()*"/>
<xsl:attribute name="{name()}">
<xsl:value-of select="my:evalText($pVars, .)"/>
</xsl:attribute>
</xsl:template>
<xsl:function name="my:evalText">
<xsl:param name="pVars" as="element()*"/>
<xsl:param name="pText"/>
<xsl:analyze-string select="$pText"
regex="\$\((.+?)\)">
<xsl:non-matching-substring>
<xsl:value-of select="."/>
</xsl:non-matching-substring>
<xsl:matching-substring>
<xsl:variable name="vName" select="regex-group(1)"/>
<xsl:variable name="vReplacement" select=
"$pVars[@name eq $vName][last()]/@value"/>
<xsl:value-of select="string($vReplacement)"/>
</xsl:matching-substring>
</xsl:analyze-string>
</xsl:function>
</xsl:stylesheet>
When now this transformation is applied on the following XML document:
<Doc>
<stuff>
<for var="i" from="1" to="2">
<item name="X$(i)">$(i)</item>
<for var="j" from="2" to="4">
<innerItem name="X$(i).$(j)">$(j)</innerItem>
</for>
</for>
</stuff>
</Doc>
the wanted, correct result is produced:
<Doc>
<stuff>
<item name="X1">1</item>
<innerItem name="X1.2">2</innerItem>
<innerItem name="X1.3">3</innerItem>
<innerItem name="X1.4">4</innerItem>
<item name="X2">2</item>
<innerItem name="X2.2">2</innerItem>
<innerItem name="X2.3">3</innerItem>
<innerItem name="X2.4">4</innerItem>
</stuff>
</Doc>