Search code examples
xmlxsltnokogiri

How do I evaluate an XSLT expression in isolation using Nokogiri?


We're using an XSLT transform, given to us by a third party, to validate an XML document; it transforms the document into a collection of error nodes, which will be empty if the XML document is valid. We're using Nokogiri to perform the transform.

One rule in the XSLT document is triggering, even though I don't think it should be. The expression for the part that's failing reduces to:

date:year(normalize-space(date:add(normalize-space('2023-06-01'),normalize-space('-P3M'))))
=
date:year(normalize-space(date:add(normalize-space('2024-05-31'),normalize-space('-P3M'))))    

which I think should be comparing the year part of 1st March 2023 with the year part of 29th Feb 2024 and finding them to be different, but it's finding them to be the same.

I want to test this expression in isolation, to confirm whether it really is the problem. I've tried evaluating just one part of it like so:

doc = Nokogiri::XML('')
doc.xpath(
  "date:year(normalize-space(date:add(normalize-space('2023-06-01'),normalize-space('-P3M'))))"
)

but this raises xmlXPathCompOpEval: function add bound to undefined prefix date.

If I try adding the EXSLT namespace like so:

doc.xpath(
  "date:year(normalize-space(date:add(normalize-space('2023-06-01'),normalize-space('-P3M'))))", 
  { "date" => "http://exslt.org/dates-and-times") }
)

...then it raises xmlXPathCompOpEval: function add not found.

What do I need to do to use Nokogiri to evaluate the above expression?


Solution

  • If you can use an actual XSLT document then running this stylesheet:

    <xsl:stylesheet version="1.0" 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:date="http://exslt.org/dates-and-times"
    extension-element-prefixes="date">
    <xsl:output method="xml" version="1.0" encoding="utf-8" indent="yes"/>
    
    <xsl:template match="/">
        <test>
            <xsl:value-of select="date:year(normalize-space(date:add(normalize-space('2023-06-01'),normalize-space('-P3M'))))
    =
    date:year(normalize-space(date:add(normalize-space('2024-05-31'),normalize-space('-P3M')))) "/>
        </test>
    </xsl:template>
    
    </xsl:stylesheet>
    

    against any XML document should give you the result you are looking for - which of course should be:

    <?xml version="1.0" encoding="utf-8"?>
    <test>false</test>
    

    You could use Nokogiri to do it like so:

      xml = Nokogiri::XML("")
    
      xslt = Nokogiri::XSLT(<<~XSLT)
        <xsl:stylesheet version="1.0" 
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
        xmlns:date="http://exslt.org/dates-and-times"
        extension-element-prefixes="date">
        <xsl:output method="xml" version="1.0" encoding="utf-8" indent="yes"/>
        
        <xsl:template match="/">
            <test>
                <xsl:value-of select="date:year(normalize-space(date:add(normalize-space('2023-06-01'),normalize-space('-P3M'))))
        =
        date:year(normalize-space(date:add(normalize-space('2024-05-31'),normalize-space('-P3M')))) "/>
            </test>
        </xsl:template>
        
        </xsl:stylesheet>
      XSLT
      
      puts xslt.transform(xml).to_s