Project

Profile

Help

Result of XSLT transformation used as input for further XPath evaluation gives error "Root node for '/' must be a document node" if the identity transformation is not declared as a mode but rather as a template

Added by Martin Honnen 2 months ago

Hi there,

on StackOverflow https://stackoverflow.com/q/78166593/252228 someone found an issue with SaxonJS making it hard to use the result you get from fn:transform as the input for further XPath evaluation as you then might get an error "Root node for '/' must be a document node".

I think the user there is satisfied to use the XSLT code change I made there (instead of spelling out the identity transformation template I used the xsl:mode on-no-match="shallow-copy" declaration) but somehow, on further investigation, it seems odd or quirky that SaxonJS (tested with 2.6) allows to use the result of fn:transformfor XPath evaluation for a stylesheet doing the identity transformation based on anxsl:mode` declaration but doesn't allow that once the identity transformation template is used.

I can't quite make sense of the error so I guess I let you know and you can investigate further.

A sample that gives the error is:

const SaxonJS = require("saxon-js")


let xslt = `<?xml version="1.0"?>
  <xsl:stylesheet version="3.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>
  `;
  
const applyXsltToXml = function (xmlData, transformXslt) {

const filteredXML = SaxonJS.XPath.evaluate(
    `transform(map {
      'source-node': parse-xml($xml),
      'stylesheet-node': 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 resultExample1 = applyXsltToXml(xmlExample1, xslt);

console.log(SaxonJS.serialize(resultExample1));

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

console.log(result2);

It gives the error (for the second XPath evaluation)

H: Root node for '/' must be a document node
 in saxon-js/SaxonJS2N.js — line 4604

J,nb)}catch(ob){throw new H("Error in xsl:result-document/@href ?xpath:"+ob.message,"SXJS0099",l);}ea=ea.expand();if(1<ea.length)throw new H("xsl:result-document/@href ?xpath yields more than one node","SXJS0099",l);ea=ea[0]}else ea=J.masterDocument.getElementById(Fa.substring(1));if(!ea)return Aa.Qa("Result document href="+Fa+": no such node",1,J.fixed.Ga),Tb;if("?."===Fa&&ea!==J.masterDocument&&ea.ownerDocument!==V.xc(J.masterDocument))return Aa.Qa("Result document href="+Fa+": current node is not in master document",
1,J.fixed.Ga),Tb;if(R)for(;ea.firstChild;)ea.removeChild(ea.firstChild);Q=function(){Qa(Ka,ea);!Pd.find("SaxonJS").getConfigurationProperty("autoResetIndexes")&&"_saxonIndexes"in J.masterDocument||(J.masterDocument._saxonIndexes={},J.masterDocument._saxonIndexesBC={})}}else throw new H("xsl:result-document/@href value in browser must be '?.' or '#frag'");}else throw new H("Unsupported result-document destination "+N,"SXJS0002");if("html-page"!==N&&(""===Fa||Pa.toString()===ua)){const la=J.fixed.Aj;
if(la){if(la.Ti)throw new H("Cannot use xsl:result-document to write to a destination already used for the principal output","XTDE1490");if(la.Wg)throw new H("Cannot write more than one xsl:result-document to the principal output destination","XTDE1490");la.Wg=!0}}if("html-page"!==N){J.pushCurrentOutputURI(Pa.toString());const la=B(l,"content"),ea=ee.getComplexContentOutputter(Wa);ee.push(la,J,ea);ea.close()}Q();J.popCurrentOutputURI();return Tb}catch(t){D(t,l)}}},root:function(l){return J=>{J.failIfNoCurrentFocus("XPDY0002",
"/",l);J=J.getCurrentFocus();if(!V.Q(J))throw new H("Context item for '/' must be a node","XPTY0020",l);const R=V.ic(J);if(9!==R.nodeType&&11!==R.nodeType)throw new H("Root node for '/' must be a document node","XPDY0050",l);return Ub(V.ic(J))}},sequence:b,slash:function(l){const J=k(l),R=p(l);return t=>{const Q=t.newContext(!1);Q.focus=Xb(J(t));return Q.focus.mapOneToMany(()=>R(Q))}},some:function(l){const J=parseInt(l.slot,10),R=k(l),t=p(l);return Q=>{const T=R(Q);return bc(T.some(function(ka){Q.localVars[J]=
[ka];return e(t(Q))}))}},sort:function(l){const J=Nd.Ak(l);return R=>{try{return J(R)}catch(t){if(t instanceof H&&"FOCH0002"===t.code)throw new H(t.message,"XTDE1035",l);if(t instanceof H&&"XPTY0004"===t.code)throw new H(t.message,"XTDE1030",l);throw t;}}},sourceDoc:S,str:function(l){const J=l.val;return()=>Yb(J)},subscript:function(l){const J=k(l),R=p(l);return t=>{var Q=J(t);t=R(t).next();if("ADI"===t.code)t=t.value;else if(t.equals(t.round(0)))t=Sa.XS.integer.I(t).value;else return Tb;return(Q=
Q.nf(t))?Ub(Q):Tb}},supplied:function(l){const J=parseInt(l.slot,10);return R=>Pb(R.localVars[J])},tail:function(l){const J=parseInt(l.start,10)-1;let R=k(l);return t=>{let Q=J;return R(t).filter(function(){return 0>=Q--})}},tailCallLoop:b,to:function(l){const J=k(l),R=p(l);return t=>{const Q=J(t).next();t=R(t).next();return null===Q||null===t||0<Q.compareTo(t)?Tb:n(Q,t)}},treat:function(l){const J=oc(l.as),R=J.ga(),t=k(l);return Q=>t(Q).mapOneToOne(function(T){if(R(T))return T;const ka=h(l.diag);
throw new H("Required item type of "+ka.gd+" is "+J.toString()+"; supplied value is "+Aa.showValue(T),ka.code,l);})},"true":function(){return()=>bc(!0)},"try":function(l){const J=k(l),R=y(l,"catch");R.forEach(t=>{const Q=t.errors.split(" ").map(T=>{if("*"===T)return()=>!0;if(/^\*:/.test(T)){const ua=T.substring(2);return Ba=>Ba.local===ua}if(/}\*$/.test(T)){const ua=T.substring(2,T.length-2);return Ba=>Ba.q===ua}const ka=ha.QName.fromEQName(T);return ua=>ua.equals(ka)});t.test=1===Q.length?Q[0]:T=>

in saxon-js/SaxonJS2N.js — line 4604
 in saxon-js/SaxonJS2N.js — line 4604
 in saxon-js/SaxonJS2N.js — line 4563
 in saxon-js/SaxonJS2N.js — line 4570
at Array.map in ECMAScript
 in saxon-js/SaxonJS2N.js — line 4570
at Object.G (as evaluate) in saxon-js/SaxonJS2N.js — line 4538
at Object.evaluate in saxon-js/SaxonJS2N.js — line 4867

A simple switch to an xsl:mode declared identity transformation makes the code work as intended and output 0 for the second XPath evaluation:

const SaxonJS = require("saxon-js")


let xslt = `<?xml version="1.0"?>
  <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 applyXsltToXml = function (xmlData, transformXslt) {

const filteredXML = SaxonJS.XPath.evaluate(
    `transform(map {
      'source-node': parse-xml($xml),
      'stylesheet-node': 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 resultExample1 = applyXsltToXml(xmlExample1, xslt);

console.log(SaxonJS.serialize(resultExample1));

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

console.log(result2);

As I said, I can't really tell why spelling out the identity transformation makes a difference to the result and then gives that error about the Rott node for / must be a document node, it seems like some quirk or bug but I am happy to hear what you think.


Replies (2)

Result of XSLT transformation used as input for further XPath evaluation gives error "Root node for '/' must be a document node" if the identity transformation is not declared as a mode but rather as a template - Added by Norm Tovey-Walsh 2 months ago

on StackOverflow https://stackoverflow.com/q/78166593/252228 someone found an issue with SaxonJS making it hard to use the result you get from fn:transform as the input for further XPath evaluation as you then might get an error "Root node for '/' must be a document node".

Thanks, Martin. I’ll take a look.

Be seeing you,
norm

--
Norm Tovey-Walsh
Saxonica

RE: Result of XSLT transformation used as input for further XPath evaluation gives error "Root node for '/' must be a document node" if the identity transformation is not declared as a mode but rather as a template - Added by Martin Honnen 2 months ago

I have looked a bit deeper and I now think that SaxonJS is doing the right thing, the default built-in XSLT 3processing without a mode declaration is text-only-copy and https://www.w3.org/TR/xslt-30/#built-in-rule defines that to do

<xsl:template match="document-node()|element()" mode="M">
  <xsl:apply-templates mode="#current"/>
</xsl:template>

meaning a document node is not copied, only its children are processed.

If the stylesheet then spells out the identity transformation (as done) as

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

that will copy any children but node() doesn't match any document node so the whole stylesheet creates indeed an element node as the result for which a subsequent / XPath evaluation is supposed to fail.

So my bad, thinking that template does an identity transformation in the sense of xsl:mode on-no-match="shallow-copy" and in the sense of copying the document/root node is wrong.

    (1-2/2)

    Please register to reply