hielsnoppe
hielsnoppe

Reputation: 2869

Reapplying templates to output of other templates in XSLT

The following should be either simple or impossible to do, but right now I can not find out how, and so I ask. In my XSLT I have templates that produce elements, that then should again be transformed. More precise whenever a template outputs an element that in the original input would be subject to transformation it should again be transformed. Thereby infinite loops are possible but are to be avoided by careful design of the templates. Consider as an example:

input.xml

<?xml version="1.0" encoding="utf-8" ?>
<example>
    <a />
    <b />
</example>

transform.xsl

<?xml version="1.0" encoding="UTF-8"?>
<xsl:transform version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:template match="@*|node()"><xsl:copy>
        <xsl:apply-templates select="@*|node()" />
    </xsl:copy></xsl:template>

    <xsl:template match="a">a</xsl:template>

    <xsl:template match="b">
        <B>b <a /></B>
    </xsl:template>
</xsl:transform>

current-output.xml

<?xml version="1.0"?>
<example>
    a
    <B>b <a/></B>
</example>

desired-output.xml

<?xml version="1.0"?>
<example>
    a
    <B>b a</B>
</example>

What is the best solution to achieve this with just one transformation, if any?

Upvotes: 0

Views: 1627

Answers (3)

Dimitre Novatchev
Dimitre Novatchev

Reputation: 243529

This is a two-pass transformation that produces the wanted result:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:ext="http://exslt.org/common" exclude-result-prefixes="ext">

 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

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

 <xsl:template match="/">
  <xsl:variable name="vrtfPass1">
    <xsl:apply-templates/>
  </xsl:variable>

  <xsl:variable name="vPass1" select="ext:node-set($vrtfPass1)/*"/>

  <xsl:apply-templates select="$vPass1" mode="pass2"/>
 </xsl:template>

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

 <xsl:template match="replace">
  <xsl:text>&#xA;Hello world&#xA;</xsl:text>
 </xsl:template>

 <xsl:template match="@*|node()" mode="pass2">
  <xsl:call-template name="identity"/>
 </xsl:template>
</xsl:stylesheet>

When applied on the provided XML document:

<example>
    <content>Lorem ipsum</content>
    <content><replace /></content>
</example>

the wanted, correct result is produced:

<example>
   <wrapper>
Hello world
Lorem ipsum</wrapper>
   <wrapper>
Hello world

Hello world
</wrapper>
</example>

Do note:

In XSLT 1.0 the result of applying templates is of the infamous type RTF (Result Tree Fragment) and by definition it cannot be further accessed and processed, except using xsl:copy-of and the standard string functions.

This is why almost every XSLT 1.0 processor provides a vendor-specific extension function xxx:node-set() that takes an RTF and converts it to a "regular" tree whoce descendants can be accessed using any XPath expression. Here the xxx prefix must be bound to a vendor-specifix namespace-uri.

The EXSLT ext:node-set() is implemented by most XSLT processors -- thus its use guarantees significant degree of portability among different XSLT processors.

For an additional multi-pass transformation example, see this:

https://stackoverflow.com/a/3200026/36305

and this:

http://www.dpawson.co.uk/xsl/sect1/N169.html#d860e392

Upvotes: 2

hielsnoppe
hielsnoppe

Reputation: 2869

Based on the input from the other answers here is what I came up with to solve my problem. Consider as a more appropriate example input:

<?xml version="1.0" encoding="utf-8" ?>
<example>
    <a />
    <b />
    <c />
</example>

The elements <a />, <b /> and <c /> are used like <replace /> in my original example, but as shown in the transformation, their templates each contain further references to them:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:transform version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ext="http://exslt.org/common"
exclude-result-prefixes="ext">

    <xsl:template match="@*|node()">
        <xsl:variable name="this">
            <xsl:apply-templates select="." mode="normal" />
        </xsl:variable>
        <xsl:apply-templates select="ext:node-set($this)" mode="normal" />
    </xsl:template>

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

    <xsl:template match="a" mode="normal">a</xsl:template>

    <xsl:template match="b" mode="normal">
        <B>b <a /></B>
    </xsl:template>

    <xsl:template match="c" mode="normal">
        <C>c <b /></C>
    </xsl:template>
</xsl:transform>

When applied to the example input this transformation produces my wanted result of:

<?xml version="1.0"?>
<example>
    a
    <B>b a</B>
    <C>c <B>b a</B></C>
</example>

Apparently all recursive references have been resolved. Although it is tested with some more complex examples, I am not sure whether or not it will work in all cases.

The idea behind this is the following: The first two templates are for copying. The one with mode="normal" is a plain identity transformation and the one without does the recursion. Modes are used in order to prevent an infinite loop within the recursion template.

Please let me know, what you think about this and whether there are obvious flaws in it!

Upvotes: 0

Flynn1179
Flynn1179

Reputation: 12075

It doesn't work in all cases, but there's a very simple way of achieving what you're asking. All you need to do is give your replace template a name, and call it with xsl:call-template. This only needs a couple of minor changes to your existing stylesheet:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:transform version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:template match="@*|node()"><xsl:copy>
        <xsl:apply-templates select="@*|node()" />
    </xsl:copy></xsl:template>

    <xsl:template match="content">
        <wrapper>
            <xsl:call-template name="replace"/>
            <xsl:apply-templates select="@*|node()" />
        </wrapper>
    </xsl:template>

    <xsl:template match="replace" name="replace">
        Hello world
    </xsl:template>
</xsl:transform>

Upvotes: 1

Related Questions