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

50 Views Asked by At

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.

2

There are 2 best solutions below

1
Martin Honnen On BEST ANSWER

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.

0
Michael Kay On

There are a number of different ways of tackling this, and which one works best probably depends on how much commonality there is between the different input document types.

One way would be to write a stylesheet module to handle each kind of input document, each using a different mode, and then use the top-level stylesheet as a driver to decide which kind of document you are dealing with, and then do an apply-templates in the appropriate mode. That's the approach I would use if the processing for each kind of document is quite different.

At the other extreme, if the documents are very similar, I think I would simply define template rules to handle their union. Just write rules for each kind of element that might be encountered, regardless what document type it appears in.

You could do something more dynamic using xsl:evaluate or fn:transform, which is the approach you have attempted, but I don't really see the advantage in this, unless perhaps new document types are being introduced all the time.

Finally, for some applications the appropriate design is to have one XSLT stylesheet per input document type (perhaps sharing common modules for reuse), and to decide which XSLT stylesheet to apply at the level of the calling application.