Search code examples
node.jsxmlxsltsaxonsaxon-js

Reuse XSLT for different XML inputs using parameters in nodeJS with saxon-js


I want to transform an unknown number of different but very similar structured input XML documents with XSLT into a single output XML format. My platform is node.js and therefore I'm looking into saxon-js to reach that goal.

My question is now: Is that even possible with XSLT? Can I parameterize XSLT in a way to get different XPath expressions depending on the input XML, which I cannot change in any way? I was looking for a way to add XPath expressions as parameters but I couldn't find any type to create them.

Input XML examples:

<format1>
  <some_id>4d736817-ebc1-42d3-be3f-4dda7178d1aa</some_id>
  <the_uri>https://example.com</the_uri>
</format1>
<format2>
  <also_the_id>9dac9085-99b3-4d52-aa56-fa59fc41e066</also_the_id>
  <url_field>https://example2.com</url_field>
</format2>

Desired output XML:

<myXml>
  <provider>provider1</provider>
  <id>4d736817-ebc1-42d3-be3f-4dda7178d1aa</id>
  <url>https://example.com</url>
</myXml>

XSLT (xsltString):

<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
  <xsl:param name="providerId" />
  <xsl:param name="xpathId" />
  <xsl:param name="xpathUrl" />
  <xsl:template match="/">
    <myXml>
      <provider>
        <xsl:value-of select="$providerId" />
      </provider>
      <id>
        <xsl:value-of select="$xpathId"/>
      </id>
      <url>
        <xsl:value-of select="$xpathUrl"/>
      </url>
    </myXml>
  </xsl:template>
</xsl:stylesheet>

node.js Code (inputXml being "format1" xml from above):

const inputXmlDoc = await SaxonJS.getResource({
  text: inputXml,
  type: "xml",
});

const outputXml = SaxonJS.XPath.evaluate(
  `transform(map {
    'stylesheet-text': $xslt,
    'stylesheet-params': map {
      QName('', 'providerId'): $providerId,
      QName('', 'xpathId'): '$xpathId',
      QName('', 'xpathUrl'): '$xpathUrl',
    },
    'source-node': .,
    'delivery-format': 'serialized'
  })?output`,
  inputXmlDoc,
  {
    params: {
      xslt: xsltString,
      providerId: 'some-provider-id',
      xpathId: '/format1/some_id'
      xpathUrl: '/format1/the_uri'
    },
  },
);

Output xml (erroneous):

<myXml>
  <provider>some-provider-id</provider>
  <id>/format1/some_id</id>
  <url>/format1/the_uri</url>
</myXml>

This code is not functional as XPath expressions are not strings. It works for providerId which is just a string. So I just get the XPath expression instead of the resolved value. My goal would be to only change the params map to account for different input XMLs, reusing the same XSLT document and get the same output XML format.


Solution

  • I think you can use static parameters and shadow attributes _select="{$xpathId}".

    
    <xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
      <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
      <xsl:param name="providerId" />
      <xsl:param name="xpathId" static="yes" select="'/format1/some_id'"/>
      <xsl:param name="xpathUrl" static="yes" select="'/format1/theUri'"/>
      <xsl:template match="/">
        <myXml>
          <provider>
            <xsl:value-of select="$providerId" />
          </provider>
          <id>
            <xsl:value-of _select="{$xpathId}"/>
          </id>
          <url>
            <xsl:value-of _select="{$xpathUrl}"/>
          </url>
        </myXml>
      </xsl:template>
    </xsl:stylesheet>
    

    and use an additional map for fn:transform

    
    'static-params': map {
          QName('', 'xpathId'): $xpathId,
          QName('', 'xpathUrl'): $xpathUrl
        },
    

    Of course given the two different XML formats you could also just consider a stylesheet like

    <xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" expand-text="yes">
      <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
    
      <xsl:param name="providerId" />
    
      <xsl:mode on-no-match="shallow-skip"/>
    
      <xsl:template match="/*">
        <myXml>
          <provider>{$providerId}</provider>
          <xsl:apply-templates/>
        </myXml>
      </xsl:template>
    
      <xsl:template match="/format1/some_id | /format2/also_the_id">
        <id>{.}</id>
      </xsl:template>
    
      <xsl:template match="/format1/the_uri | /format2/url_field">
        <url>{.}</url>
      </xsl:template>
    
    </xsl:stylesheet>
    

    Just added that as a note to show that, as long as you deal with known vocabularies of XML elements, it should suffice to provide the adequate match patterns.