2

I'm new to XSLT, forgive my ignorance, if I have input XML like this:

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

I want to use a transform to have the output XML expanded like this:

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

All I've got is this: what to do next?

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

    <xsl:template match="@* | node()">
        <xsl:copy>         
            <xsl:apply-templates select="@* | node()"/>
        </xsl:copy>
    </xsl:template>

  <xsl:template match="for">
    <xsl:variable name="from" select="@from" />
    <xsl:variable name="to" select="@to" />
    <xsl:copy>
      <xsl:apply-templates select="@* | node()"/>
    </xsl:copy>    
  </xsl:template>
</xsl:stylesheet>

2 Answers 2

2

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>
Sign up to request clarification or add additional context in comments.

5 Comments

Fantastic! Thanks so much, I tried it with the .NET Saxonica and it works perfectly
Is there a way to also allow the expansion inside attributes? for example: <inner-most-Item id="$(i)"> I assume the template matching text() handles it for normal elements, is there an attribute() function?
I think I got the first part: <xsl:template match="@*[contains(., '$(')]"> its the inside part i'm not sure what to do
@Eric: Yes, this is very easy to do -- simply create another template matching @*[contains(., '$(')] and provide the same code within its bodu, but wrapped within an xsl:attribute name="{name()}".
@Eric: See the update at the end of this answer -- the requested new code with an example run.
0

This works on the provided input XML. I had to use modes. The string replace function is based on the answer for this question: XSLT string replace

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:fun="http://www.nothing.org">
    <xsl:output method="xml" indent="yes"/>
    <xsl:template match="/">
        <xsl:apply-templates mode="parse"/>
    </xsl:template>
    <xsl:template name="forImpl">
        <xsl:param name="counter" select="@from"/>
        <xsl:param name="to" select="@to"/>
        <xsl:param name="varName" select="@var"/>
        <xsl:param name="nodes"/>
        <xsl:apply-templates select="$nodes" mode="parse">
            <xsl:with-param name="varName" select="concat('$(', $varName, ')')"/>
            <xsl:with-param name="counter" select="$counter"/>
        </xsl:apply-templates>
        <xsl:if test="$counter != $to">
            <xsl:call-template name="forImpl">
                <xsl:with-param name="counter" select="$counter + 1"/>
                <xsl:with-param name="to" select="$to"/>
                <xsl:with-param name="varName" select="$varName"/>
                <xsl:with-param name="nodes" select="$nodes"/>
            </xsl:call-template>
        </xsl:if>
    </xsl:template>
    <xsl:template match="@* | node()" mode="parse">
        <xsl:param name="varName"/>
        <xsl:param name="counter"/>
        <xsl:copy>
            <xsl:apply-templates select="@* | node()" mode="parse">
                <xsl:with-param name="varName" select="$varName"/>
                <xsl:with-param name="counter" select="$counter"/>
            </xsl:apply-templates>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="for" mode="parse">
        <xsl:param name="varName"/>
        <xsl:param name="counter"/>
        <xsl:call-template name="forImpl">
            <xsl:with-param name="counter" select="@from"/>
            <xsl:with-param name="to" select="@to"/>
            <xsl:with-param name="varName" select="@var"/>
            <xsl:with-param name="nodes">
                <xsl:copy-of select="./*"/>
            </xsl:with-param>
        </xsl:call-template>
    </xsl:template>
    <xsl:template match="text()" mode="parse">
        <xsl:param name="varName"/>
        <xsl:param name="counter"/>
        <xsl:value-of select="if (contains(., $varName)) then fun:string-replace-all(., $varName, $counter) else ."/>
    </xsl:template>
    <xsl:function name="fun:string-replace-all">
        <xsl:param name="text"/>
        <xsl:param name="replace"/>
        <xsl:param name="by"/>
        <xsl:choose>
            <xsl:when test="contains($text, $replace)">
                <xsl:value-of select="substring-before($text,$replace)"/>
                <xsl:value-of select="$by"/>
                <xsl:value-of select="fun:string-replace-all(substring-after($text,$replace), $replace, $by)"/>
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="$text"/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:function>
</xsl:stylesheet>

2 Comments

I am trying to execute this transformation with Saxon 9.1.05 -- the result is endless recursion.
"Loading net.sf.saxon.event.MessageEmitter Recoverable error XTRE0540: Ambiguous rule match for /Doc/text()[1] Matches both "text()" on line 46 of file:/C:/Program%20Files/Java/jre6/bin/marrowtr.xsl and "node()" on line 24 of file:/C:/Program%20Files/Java/jre6/bin/marrowtr.xsl Error at xsl:apply-templates on line 28 of marrowtr.xsl: SXLM0001: Too many nested apply-templates calls. The stylesheet may be looping. at xsl:apply-templates (file:/C:/Program%20Files/Java/jre6/bin/marrowtr.xsl#4) processing /Doc Transformation failed: Run-time errors were reported "

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.