javascriptnode.jsxsltsaxon-js

Is there a way to implement SaxonJS higher order functions in the Node.js runtime environment (not JS/HTML)


We have an ongoing project where we are porting over an older C# system that used custom functions to augment the XSLT processing, and we were planning on writing it in Node.js/saxon-js.

It seems from the documentation that event though the higher order functions feature is available to JavaScript SaxonJS, that might not be the case for the Node.js subset of it.

Is there an alternative to that, still in the realm of Node.js / server-side JavaScript runtime?


Solution

  • First of all, in XSLT 3 with XPath 3.1 there is a rich built-in function library https://www.w3.org/TR/xpath-functions-31/, furthermore xsl:function since XSLT 2 allows you to implement more functions in XSLT/XPath itself (https://www.w3.org/TR/xslt-30/#stylesheet-functions).

    As for SaxonJS (tested with the current 2.5 release) and Node.js to call into JavaScript, the following is an example that works for me:

    const SaxonJS = require("saxon-js");
    
    globalThis.test1 = function() {
      return new Date().toGMTString();
    }
    
    const xslt1 = `<xsl:stylesheet  
      xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
      version="3.0"
      xmlns:xs="http://www.w3.org/2001/XMLSchema"
      xmlns:js="http://saxonica.com/ns/globalJS"
      exclude-result-prefixes="#all"
      expand-text="yes">
    
      <xsl:mode on-no-match="shallow-copy"/>
    
      <xsl:template match="/" name="xsl:initial-template">
        <test>{js:test1()}</test>
        <xsl:comment>Run with {system-property('xsl:product-name')} {system-property('xsl:product-version')} {system-property('Q{http://saxon.sf.net/}platform')}</xsl:comment>
      </xsl:template>
      
    </xsl:stylesheet>`;
    
    console.log(SaxonJS.XPath.evaluate(`
      transform(
        map {
         'stylesheet-text' : $xslt,
         'delivery-format' : 'serialized'
        }
      )?output`,
      null,
      { params : { xslt: xslt1 } }
    ));
    

    Output e.g.

    <?xml version="1.0" encoding="UTF-8"?><test>Sat, 19 Aug 2023 14:35:00 GMT</test><!--Run with SaxonJS 2.5 Node.js-->
    

    Documentation https://www.saxonica.com/saxon-js/documentation2/index.html#!development/global and https://www.saxonica.com/saxon-js/documentation2/index.html#!xdm/conversions, although there the globalThis use is not mentioned; I believe that is necessary for Node as there, contrary to client-side JavaScript, a global function or var statement does not create a global function or variable.

    As for processing nodes, I would think it should be save to access the value with e.g. data() before passing to JavaScript, as in

    const SaxonJS = require("saxon-js");
    
    globalThis.test1 = function(date) {
      return new Date(date);
    }
    
    const xml1 = `<root>
      <item>
        <name>item 1</name>
        <date>Sat Aug 19 2023 17:20:07 GMT+0200</date>
      </item>
    </root>`;
    
    const xslt1 = `<xsl:stylesheet  
      xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
      version="3.0"
      xmlns:xs="http://www.w3.org/2001/XMLSchema"
      xmlns:js="http://saxonica.com/ns/globalJS"
      exclude-result-prefixes="#all"
      expand-text="yes">
    
      <xsl:mode on-no-match="shallow-copy"/>
    
      <xsl:template match="/" name="xsl:initial-template">
        <xsl:copy>
          <xsl:apply-templates/>
          <xsl:comment>Run with {system-property('xsl:product-name')} {system-property('xsl:product-version')} {system-property('Q{http://saxon.sf.net/}platform')}</xsl:comment>
        </xsl:copy>
      </xsl:template>
    
      <xsl:template match="date">
        <xsl:copy>{js:test1(data())}</xsl:copy>
      </xsl:template>
      
    </xsl:stylesheet>`;
    
    console.log(SaxonJS.XPath.evaluate(`
      transform(
        map {
         'stylesheet-text' : $xslt,
         'source-node' : parse-xml($xml),
         'delivery-format' : 'serialized'
        }
      )?output`,
      null,
      { params : { xslt: xslt1, xml: xml1 } }
    ));
    

    I can't tell for sure how the DOM library used by SaxonJS under Node.js and with the (clojure?) compilation exposes the usual browser know/W3C/WHATWG defined DOM properties and methods; you might need to wait until someone from Saxonica answers that separately or you can consider to ask follow-up questions with more specific details.

    As simple textContent property of an element node seems to work here:

    const SaxonJS = require("saxon-js");
    
    globalThis.test1 = function(dateNode) {
      return new Date(dateNode.textContent);
    }
    
    const xml1 = `<root>
      <item>
        <name>item 1</name>
        <date>Sat Aug 19 2023 17:20:07 GMT+0200</date>
      </item>
    </root>`;
    
    const xslt1 = `<xsl:stylesheet  
      xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
      version="3.0"
      xmlns:xs="http://www.w3.org/2001/XMLSchema"
      xmlns:js="http://saxonica.com/ns/globalJS"
      exclude-result-prefixes="#all"
      expand-text="yes">
    
      <xsl:mode on-no-match="shallow-copy"/>
    
      <xsl:template match="/" name="xsl:initial-template">
        <xsl:copy>
          <xsl:apply-templates/>
          <xsl:comment>Run with {system-property('xsl:product-name')} {system-property('xsl:product-version')} {system-property('Q{http://saxon.sf.net/}platform')}</xsl:comment>
        </xsl:copy>
      </xsl:template>
    
      <xsl:template match="date">
        <xsl:copy>{js:test1(.)}</xsl:copy>
      </xsl:template>
      
    </xsl:stylesheet>`;
    
    console.log(SaxonJS.XPath.evaluate(`
      transform(
        map {
         'stylesheet-text' : $xslt,
         'source-node' : parse-xml($xml),
         'delivery-format' : 'serialized'
        }
      )?output`,
      null,
      { params : { xslt: xslt1, xml: xml1 } }
    ));
    

    One final note: for readability and easy understanding above I have always used the XSLT as a JavaScript string and run XSLT by calling the XPath 3.1 function fn:transform with SaxonJS.XPath.evaluate; once you have developed your XSLT code for SaxonJS, however, it is highly recommended to "precompile" to SEF, using e.g. xslt3 -t -nogo -xsl:xslt1.xsl -export:xslt1.sef.json -relocate:on and to then use SaxonJS.transform, passing in the the created sef.json file. That way you get much better performance.