tags:

views:

935

answers:

2

Let's say I have an empty XML file like so:

<root></root>

And I want to add an element to root during an XSL transformation like so:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;

  <xsl:output method="xml" encoding="UTF-8" omit-xml-declaration="yes" />

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

  <xsl:template match="/">
    <xsl:copy>
      <xsl:element name="root">
        <xsl:element name="label">Some text</xsl:element>
      </xsl:element>
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>

Transforming the XML file gives me:

<root>
  <label>Some text</label>
</root>

However, I'd like to modify the XSL file so that I can transform the runtime-generated XML. Basically, I'd like the XSL to think that the original XML file contained the dynamically created XML and then do an HTML transform on that.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;

  <xsl:output method="html" encoding="UTF-8" omit-xml-declaration="yes" />

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

  <xsl:template match="/">
    <xsl:copy>
      <xsl:element name="root">
        <xsl:element name="label">Some text</xsl:element>
      </xsl:element>
    </xsl:copy>
  </xsl:template>

  <!-- This part is new -->
  <xsl:template match="//label">
    <b>
      <xsl:value-of select="." />
    </b>
  </xsl:template>

</xsl:stylesheet>

In this case, my desired output would be: <b>label</b>.

Is it possible to do this in one XSL file? Am I looking at this problem incorrectly?

+3  A: 

There are three essential elements to running a multi-pass transform.

  • <xsl:import>
  • <xsl:apply-imports>
  • node-set() extension function

The example passes an XML document through two transforms serially. That is, it passes the content through a transform that removes namespace nodes and then takes the output from the first transform and passes it through a second transform that changes the title of the document. In this case the document is an XHTML document. The second transform is written such that it cannot accept an XHTML document with a namespace defined.

Original XHTML document

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"&gt;
<html xmlns="http://www.w3.org/1999/xhtml"&gt;
  <head>
    <title>This is the old title</title>
  </head>
  <body>
    <p>This is some text.</p>
  </body>
</html>

After pass 1 (remove namespace nodes)

<html>
  <head>
    <title>This is the old title</title>
  </head>
  <body>
    <p>This is some text.</p>
  </body>
</html>

Result (after pass 2)

Note that the title text has changed.

<html>
  <head> 
    <title>This is the new title</title>
  </head>
  <body>
    <p>This is some text.</p>
  </body>
</html>

XSLT for pass 1

This transform applies templates in this file to the content to remove namespace nodes, but copies the rest of the content. Then, during pass 2, applies the templates that are defined in the imported XSLT by using the <xsl:apply-imports> tag. The templates for pass 2 are imported using the <xsl:import> tag.

The results of the first pass are stored in a variable named "treefrag". The tree fragment is converted to a node-set using the extension function "node-set()". In this example, the Microsoft XML parser 4.0 is used, so the urn:schemas-microsoft-com:xslt namespace is declared.

<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:msxsl="urn:schemas-microsoft-com:xslt"
>

  <xsl:import href="pass2.xslt" />
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes" />

  <xsl:template match="/">
    <xsl:param name="pass">1</xsl:param>

    <xsl:choose>
      <xsl:when test="$pass=1">
        <xsl:variable name="treefrag">
          <xsl:apply-templates>
            <xsl:with-param name="pass" select="$pass" />
          </xsl:apply-templates>
        </xsl:variable>
        <xsl:variable name="doc" select="
          msxsl:node-set($treefrag)
        " />
        <xsl:apply-templates select="$doc">
          <xsl:with-param name="pass">2</xsl:with-param>
        </xsl:apply-templates>
      </xsl:when>
      <xsl:when test="$pass=2">
        <xsl:apply-imports />
      </xsl:when>
    </xsl:choose>

  </xsl:template>   

  <!-- identity template without namespace nodes -->
  <xsl:template match="*">
    <xsl:param name="pass">2</xsl:param>

    <xsl:choose>
      <xsl:when test="$pass=1">
        <xsl:element name="{name()}">
          <xsl:apply-templates select="@*|node()">
            <xsl:with-param name="pass" select="$pass" />
          </xsl:apply-templates>
        </xsl:element>
      </xsl:when>
      <xsl:when test="$pass=2">
        <xsl:apply-imports />
      </xsl:when>
    </xsl:choose>
  </xsl:template>

  <xsl:template match="@*|text()|comment()|processing-instruction()">
    <xsl:param name="pass">2</xsl:param>

    <xsl:choose>
      <xsl:when test="$pass=1">
        <xsl:copy>
          <xsl:apply-templates select="@*|node()">
            <xsl:with-param name="pass" select="$pass" />
          </xsl:apply-templates>
        </xsl:copy>
      </xsl:when>
      <xsl:when test="$pass=2">
        <xsl:apply-imports />
      </xsl:when>
    </xsl:choose>
  </xsl:template>
</xsl:stylesheet>

XSLT for pass 2

This transform simply changes the contents of the TITLE tag can copies all the rest of the content.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;

  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

  <xsl:template match="title">
    <title>This is the new title</title>
  </xsl:template>

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

</xsl:stylesheet>

Source article: "XSLT: Multi-pass transforms"

Doug D
Thanks for the answer, Doug. This put me on the right track and I was able to come up with a solution to my real problem, detailed in my own answer that I posted.
Adam Plumb
A: 

Doug's answer is technically correct, and he put me on the right path of looking for the nodeset extension function (thanks!). The only difference is that I'm using the Xalan-java processor instead of the microsoft one. Here is a working (extremely simple) example of what I was trying to achive:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
            xmlns:xalan="http://xml.apache.org/xalan" exclude-result-prefixes="xalan">
<xsl:output method="html" encoding="UTF-8" omit-xml-declaration="yes"/>

<xsl:template match="label">
    <b><xsl:value-of select="." /></b>
</xsl:template>

<xsl:template name="someTemplate">
    <root>
        <label>hey there</label>
    </root>
</xsl:template>

<xsl:template match="/">
    <xsl:variable name="gen">
        <xsl:call-template name="someTemplate" />
    </xsl:variable>

    <xsl:apply-templates select="xalan:nodeset($gen)//label" />
</xsl:template>

</xsl:stylesheet>

The basic idea is that you create a variable (here named "gen") that calls a template ("someTemplate") which creates some xml. That xml tree is then processed by the xalan:nodeset function. Here, I run apply-templates and process all label fields, which matches with my label template and creates the bolded label.

The reason why I'm doing all this is because I'm working on a web-app where all the content on each page is defined in XML and then processed using XSL. This works great until you want to fill parts of the page with the results of an AJAX call, because then you either need to build the new html inside a javascript function from the returned XML (messy and bad) or pass the AJAX XML response through an XSL transform on the server side and just insert the xhtml result into the page.

In my situation, because I'm describing each web page in XML, I've created simple, generic elements that have "types". My elements are "block", which is any block of content. A block can have sub-nodes such as "button" (Which is a link or button), "list" (which is multiple blocks), "image", or "input". The details of the schema are irrelevant; I just wanted to show that my XML schema is very simple and generic.

On the other hand, the AJAX XML response that I'm retrieving to refresh/build up the page is being created by a third-party that does not use my schema. On the contrary, each service they provide returns very specific XML for each unique request. For this reason, I need a way to convert their very specific XML into my very generic XML so that I can transform it using a single set of XSL templates.

Here is a sample of one of the xsl templates I'm now using that transforms the AJAX response and converts their specific XML to my generic XML.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
            xmlns:xalan="http://xml.apache.org/xalan" exclude-result-prefixes="xalan">
<xsl:output method="html" encoding="UTF-8" omit-xml-declaration="yes"/>

<!-- base.xsl includes a generic template that matches all blocks and routes
        to the needed template based on the block type -->
<xsl:include href="base.xsl" />

<xsl:template name="templateForAJAXResponse">
    <block>
        <type>list</type>
        <list>
            <type>itemBrowser</type>
            <xsl:for-each select="/response/items/item">
                <block>
                    <type>button</type>
                    <button>
                        <type>imageButton</type>
                        <href>something.action</href>
                        <src><xsl:value-of select="src" /></src>
                        <title><xsl:value-of select="name" /></title>
                    </button>
                </block>
            </xsl:for-each>
        </list>
    </block>
</xsl:template>

<xsl:template match="/">
    <xsl:variable name="gen">
        <xsl:call-template name="templateForAJAXResponse" />
    </xsl:variable>

    <div>
        <xsl:apply-templates select="xalan:nodeset($gen)/block" />
    </div>
</xsl:template>

</xsl:stylesheet>
Adam Plumb