Previous Page Next Page

Recipe 14.13. Extending XSLT with JavaScript

Problem

You want to execute JavaScript to implement functionality missing from XSLT.

Solution

The following examples use Xalan-Java 2's ability to invoke scripting languages such as JavaScript. A typical use of a JavaScript-based extension invokes a function that is not native to XSLT or XPath. One common example is trigonometric functions:

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xalan="http://xml.apache.org/xslt"
xmlns:trig="http://www.ora.com/XSLTCookbook/extend/trig">
  
<xsl:output method="text"/>
   
<xalan:component prefix="trig" functions="sin">
  <xalan:script lang="javascript">
    function sin (arg){ return Math.sin(arg);} 
  </xalan:script>
</xalan:component>
   
<xsl:template match="/">
  The sin of 45 degrees is <xsl:text/>
  <xsl:value-of select="trig:sin(3.14159265 div 4)"/>
</xsl:template>
     
</xsl:stylesheet>

With JavaScript, you can actually implement functions that have side effects and objects that maintain state:[1]

[1] Shame on me for suggesting such a thing! Seriously, though, when you leave the confines of XSLT, you need not play by its rules, but you must accept the consequences.

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xalan="http://xml.apache.org/xslt"
xmlns:count="http://www.ora.com/XSLTCookbook/extend/counter">
  
<xsl:output method="text"/>
   
<xalan:component prefix="count" 
                 functions="counter nextCount resetCount makeCounter">
  <xalan:script lang="javascript">
   
    
    function counter(initValue)
    {
      this.value = initValue ;
    } 
       
    function nextCount(ctr) 
    {
      return ctr.value++ ;
    }
   
    function resetCount(ctr, value) 
    {
      ctr.value = value  ;
      return "" ;
    }
   
    function makeCounter(initValue)
    {
      return new counter(initValue) ;
    }
    
  </xalan:script>
</xalan:component>
   
<xsl:template match="/">
  <xsl:variable name="aCounter" select="count:makeCounter(0)"/>
  Count: <xsl:value-of select="count:nextCount($aCounter)"/>
  Count: <xsl:value-of select="count:nextCount($aCounter)"/>
  Count: <xsl:value-of select="count:nextCount($aCounter)"/>
  Count: <xsl:value-of select="count:nextCount($aCounter)"/>
  <xsl:value-of select="count:resetCount($aCounter,0)"/>
  Count: <xsl:value-of select="count:nextCount($aCounter)"/>
</xsl:template>
     
</xsl:stylesheet>

In most implementations, this code results in:

  Count: 0
  Count: 1
  Count: 2
  Count: 3
  Count: 0

A processor that expects no side effects can potentially change the order of evaluation and undermine the expected results.

Here you can access JavaScript's regular expression library:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xalan="http://xml.apache.org/xslt"
xmlns:regex="http://www.ora.com/XSLTCookbook/extend/regex">
  
<xsl:output method="text"/>
   
<xalan:component prefix="regex" 
     functions="match leftContext rightContext getParenMatch makeRegExp">
  <xalan:script lang="javascript">
   
    function Matcher(pattern)
    {
      this.re = new RegExp(pattern) ;
      this.re.compile(pattern) ;
      this.result="" ;
      this.left="" ;
      this.right="" ;
    } 
   
    function match(matcher, input)
    {
      matcher.result = matcher.re.exec(input) ;
      matcher.left = RegExp.leftContext ;
      matcher.right = RegExp.rightContext ;
      return matcher.result[0] ;
    }
           
    function leftContext(matcher) 
    {
      return matcher.left ;
    }
   
    function rightContext(matcher) 
    {
      return matcher.right ;
    }
   
    function getParenMatch(matcher, which)
    {
      return matcher.result[which] ;
    }
    
    function makeRegExp(pattern)
    {
      return new Matcher(pattern) ;
    }
    
  </xalan:script>
</xalan:component>
   
<xsl:template match="/">
  <xsl:variable name="dateParser" 
       select="regex:makeRegExp('(\d\d?)[/-](\d\d?)[/-](\d{4}|\d{2})')"/>
  Match: <xsl:value-of 
              select="regex:match($dateParser, 
                     'I was born on 05/03/1964 in New York City.')"/>
  Left: <xsl:value-of select="regex:leftContext($dateParser)"/>
  Right: <xsl:value-of select="regex:rightContext($dateParser)"/>
  Month: <xsl:value-of select="regex:getParenMatch($dateParser, 1)"/>
  Day: <xsl:value-of select="regex:getParenMatch($dateParser,2)"/>
  Year: <xsl:value-of select="regex:getParenMatch($dateParser,3)"/>
</xsl:template>     
</xsl:stylesheet>

This example results in:

  Match: 05/03/1964
  Left: I was born on
  Right:  in New York City.
  Month: 05
  Day: 03
  Year: 1964

In addition, Xalan lets you create JavaScript-based extension elements. Here is an extension element that repeats the execution of its content n times. It is useful for duplicating strings, structure, or as a simple looping construct:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xalan="http://xml.apache.org/xslt"
xmlns:rep="http://www.ora.com/XSLTCookbook/extend/repeat" 
extension-element-prefixes="rep">
  
<xsl:output method="xml"/>
   
<xalan:component prefix="rep" elements="repeat">
  <xalan:script lang="javascript">
<![CDATA[
    function repeat(ctx, elem)
    {
      //Get the attribute value n as an integer
      n = parseInt(elem.getAttribute("n")) ;
      //get the transformer which is required to execute nodes
      xformer = ctx.getTransformer( ) ;
      //Execute content of repeat element n times
      for(var ii=0; ii < n; ++ii)
      {
        node = elem.getFirstChild( ) ;
        while(node)
        {
          node.execute(xformer) ;
          node = node.getNextSibling( ) ;
        }
      }
      //The return value is inserted into the output
      //so return null to prevent this
      return null ;
    } 
]]>
  </xalan:script>
</xalan:component>
   
<xsl:template match="/">
  <tests>
    <!--Use to duplicate text-->
    <test1><rep:repeat n="10">a</rep:repeat></test1>
    <!--Use to duplicate structure-->
    <test2>
      <rep:repeat n="10">
        <Malady>
          <FirstPart>Shim's</FirstPart>
          <SecondPart>Syndrome</SecondPart>
        </Malady>
      </rep:repeat>
    </test2>
    <!--Use to repeat the execution of xslt code -->
    <!--(which is really what we've been doing in test1 and test2)-->
    <test3>
      <rep:repeat n="10">
        <xsl:for-each select="*">
          <xsl:copy/>
        </xsl:for-each>
      </rep:repeat>
    </test3>
  </tests>
</xsl:template>
   
</xsl:stylesheet>

Discussion

Creating extensions in JavaScript (or another embedded scripting language) is seductive because although you need to switch languages mentally, there is no need to switch to a different development environment or invoke a separate compiler. However, possibly the greatest benefit is that languages like JavaScript or VBScript are very easy to learn.[2]

[2] In fact, before writing this book, I could count the number of lines of JavaScript I had written on two hands and a few toes.

The challenge to using scripting-based extensions is that the documentation on how to tie XSLT and the scripts together tends to be thin. A few pointers are in order. Most of this information is available in the Xalan extension documents (http://xml.apache.org/xalan-j/extensions.html), but it is easy to miss when you are in a hurry to get something working.

First, script-based extensions are available only in Xalan-Java, not Xalan C++.

Second, make sure you add bsf.jar and js.jar (for JavaScript) to your class path either on the command line when invoking Java from a Unix shell:

java -cp /xalan/bin/xalan.jar:/xalan/bin/xercesImpl.jar:/xalan/bin/bsf.jar: /xalan/
bin/js.jar org.apache.xalan.xslt.Process -in input.xml -xsl trans.xslt

or in the CLASSPATH environment variable:

export CLASSPATH=/xalan/bin/xalan.jar:/xalan/bin/xercesImpl.jar:/
xalan/bin/bsf.jar:/xalan/bin/js.jar

For Windows, replace colon path separators with semicolons and use set rather than export.

Third, note that js.jar is not part of the Xalan distribution. You must get it separately from Mozilla.org (http://www.mozilla.org/rhino/).

Once you configure your environment correctly, you need to specify your stylesheet to conform to Xalan's requirements for script-based extensions. See the introduction of this chapter for the gory details.

Implementing extension functions are much easier than implementing extension elements, and the examples in the "Solution" section (in conjunction with Xalan's documentation) should be sufficient. The rest of this section focuses on extension elements.

When an extension element's associated function is invoked, it is automatically passed two objects. The first is a context of type org.apache.xalan.extensions.XSLProcessorContext. This object is a handle for getting several other useful objects, such as the context node, the Stylesheet object, and the transformer. It also implements a function outputToResultTree(Stylesheet stylesheetTree, java.lang.Object obj) that can output data to the result tree. That fact that all these objects are Java based but accessible from JavaScript is a function of the Bean Scripting Framework (http://jakarta.apache.org/bsf/), which is contained in bsf.jar.

The second object is an instance of org.apache.xalan.templates.ElemExtensionCall. This object represents the extension element itself. From this element, you can extract attributes and child elements that your script needs to interpret to implement the extension's functionality. This is done using standard DOM function calls such as getAttribute( ), getFirstChild(), getLastChild( ), etc.

There are few limitations on what you can do with a scripting-based extension element. You simply must be capable and willing to dig into the Xalan-Java source code and documentation to find out how to make it do what you want. However, you should use scripting-based extensions only for simple tasks because they are significantly slower than native Java extensions.

See Also

The definitive source for information on Xalan extensibility is http://xml.apache.org/xalan-j/extensionslib.html.


Previous Page Next Page