Result of SaxonJS.XPath.evaluate( fn:transform() ) doet not return root document node

76 views Asked by At

For a NodeJs tool I have to perform a simple transformation of XML using XSLT.

I want to use SaxonJs, but I do not want to get in the whole xslt3/sef thing (the xslt changes and the tool needs to run on computers without xslt3 installed).

There is an alternative and that is using the fn:transform function in an XPath expression.

While it seems to work exactly as I want, I can not perform any XPath on the result when I use certain expressions, because according to SaxonJs, the root node is not a document node.

So this works: /*[1]/*[1]

But this does not: //*[@someAttr]/name

Because then I get this error:

Exception has occurred: XError: Root node for '/' must be a document node

I do not get an error when I use the second XPath expression on my source document.

Also, when I serialize both the source and result, and diff them, they are identical in every way(!) (Except for a small change in attributes values, but that is what the XSLT is supposed to do)

This is my code:

const applyXsltToXml = function (xmlData, transformXslt) {

  const filteredXML = SaxonJS.XPath.evaluate(
    `fn:transform(map { ` +
      `'source-node': fn:parse-xml($xml), ` +
      `'stylesheet-node': fn:parse-xml($xslt), ` +
      `'delivery-format': 'raw' ` +
    `})?output`,
    null,
    { 'params': { 'xml': xmlData, 'xslt': transformXslt } }
  );

  if (_debug) {
    _write(`Xml transformed using Xslt; root node "${filteredXML?.localName}"`);
  }

  return filteredXML;
}

It seems to me that the problem is that the result XDM is somehow not correct. The differences I see when I compare the XDM of the source document and the result of the result XML:

  • the sourceDoc has a documentElement property
  • this sourceDoc.documentElement also has a $d property but in the XDM of transformedDoc it is a direct child ($d contains the namespaces)
  • localName/nodeName/tagName are also direct properties of transformedDoc, while in sourceDoc they can be seen in property firstChild

This is the XSLT I am using:

let xslt = `<?xml version="1.0"?>
  <xsl:stylesheet 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>
  
      <!-- Filter out @cmyk -->
      <xsl:template match="@cmyk"/>
  
  </xsl:stylesheet>
  `;

It seems like there is just one small minor thing going wrong, but I can't see it. I have been looking at the arguments for fn:transform but could not find anything. Could it have something to do with ?output?

Or do I need to convert the result into a document somehow?


Update

Thanks to @martin-honnen I now know it is the .xslt style-sheet that is the problem.

I tried a slightly different xslt v1 copy, but this also does not work.

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:output omit-xml-declaration="yes" indent="no"/>
  
        <xsl:template match="node() | @*">
          <xsl:copy>
            <xsl:apply-templates select="node() | @*"/>
          </xsl:copy>
        </xsl:template>
  
      <!-- Filter out -->
      <xsl:template match="@cmyk"/>
  
  </xsl:stylesheet>

What could explain this?

1

There are 1 answers

4
Martin Honnen On

I will give you one example of using SaxonJS 2 (tested with 2.6 under Node.js) to run a stylesheet similar to yours (adapted to use XSLT 3 features) against an XML sample to return the transformation result as an XDM document on which subsequently an XPath evaluation is performed:

const SaxonJS = require("saxon-js");

const applyXsltToXml = function (xmlData, transformXslt) {

  const filteredXML = SaxonJS.XPath.evaluate(
    `transform(map {
      'source-node': fn:parse-xml($xml),
      'stylesheet-node': fn:parse-xml($xslt),
      'delivery-format': 'raw'
    })?output`,
    null,
    { 'params': { 'xml': xmlData, 'xslt': transformXslt } , resultForm : 'xdm'}
  )[0];

  return filteredXML;
}


const xmlExample1 = `<root>
  <foo>bar</foo>
  <bar cmyk="...">test</bar>
</root>`;

const xsltExample1 = `<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  
      <xsl:mode on-no-match="shallow-copy"/>
  
      <!-- Filter out @cmyk -->
      <xsl:template match="@cmyk"/>
  
  </xsl:stylesheet>`;
  
const resultExample1 = applyXsltToXml(xmlExample1, xsltExample1);

console.log(resultExample1);

const result2 = SaxonJS.XPath.evaluate(`count(//@cmyk)`, resultExample1);

console.log(result2);

The final XPath evalution returns 0.

As for your original attempts with using

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

there is a subtle difference between that template as the base template and the ones the XSLT 3 xsl:mode on-no-match="shallow-copy" defines as the template you use doesn't copy document nodes/root nodes but only element nodes/comment nodes/processing instruction nodes while the named xsl:mode declaration does an explicit shallow copy of the root node/document node. That explains why your fn:transform call returned an element node and for that indeed an attempt to use a path starting with / has to fail. I can't explain why you got /*[1]/*[1] to work, however.