Thursday, January 18, 2007

Creating namespace nodes in XSLT 1.0

Introduction and motivating example

This entry discusses the better way to add namespace nodes to an element in XSLT 1.0. The goal is to try to have the right namespace bindings in context for content using them, as attribute values storing XPath expressions. There is no way to guarantee it, but the code developed below is reasonable enough to work in most cases.

In addition to showing how to generate a namespace node in the result tree, this entry discusses specific problems and tricks when employed in a meta-stylesheet, and how to use the XSLT 2.0 instruction xsl:namespace when available.

The motivating case is the base meta-stylesheet (the skeleton) currently developped for ISO Schematron. Schematron Love In, the mailing list for Schematron, was very active these last days, after Rick posted a first draft of the skeleton for ISO Schematron. One of the most discussed points was How to have the correct namespace bindings in the generated stylesheet? But here is an example first, with a little explanation, before going further:

<schema xmlns="http://purl.oclc.org/dsdl/schematron">

  <ns prefix="fg" uri="http://www.fgeorges.org/"/>

  <title>Sample schema</title>

  <pattern name="Test schema">
    <rule context="fg:elem">
      <assert test="@id">'fg:elem' must have an @id.</assert>
    </rule>
  </pattern>

</schema>

Namespaces used in the attribute contents are declared by the element ns. For each node in the tested document matching the rule/@context match pattern, the XPath expression in assert/@test is evaluated and must be true. If not, an error is reported. The more natural way to implement this language is generating an XSLT stylesheet, and the more natural way to generate it is using an XSLT meta-stylesheet.

Generating a namespace binding in XSLT 1.0

There is no way to guarantee, in an XSLT 1.0 stylesheet, that the output tree will contain a namespace binding for a specified prefix. The serializer is permitted to change any prefix in the output tree, providing that the prefix is still bound to the right URI. This is convenient to not worry about to bindings to the same prefix. The serializer change one prefix, and all is fine.

All? Really? All is fine regarding names of elements and attributes in the output tree (this is what nemspaces are about), but not regarding the attribute values using them, for example. If one attribute value is "fg:elem" as above, and the serializer change the prefix to ns1 (changing also all element and attribute names in this namespace), the attribute value cannot be interpreted as an XPath expression anymore. And if a nemspace binding is not used (if no element or attribute is n this namespace), the serializer can drop the namespace binding.

The usual trick to add a namespace node to an element is to use one of the xxx:node-set() functions like the following:

<!-- In the stylesheet. -->
<elem>
  <xsl:variable name="dummy">
    <ns:elem xmlns:ns="http://uri"/>
  </xsl:variable>
  <xsl:copy-of select="exsl:node-set($dummy)/*/namespace::*"/>
</elem>

<!-- In the result tree (in most cases). -->
<elem xmlns:ns="http://uri"/>

If a xxx:node-set() extension function is not available, the only way to add a namespace binding in the result tree is to add an element or an attribute in the namespace to the result tree. In a meta-stylesheet, we can add an attribute to an XSLT instruction, if this attribute is not in the null namespace or in the XSLT namespace (say we don't use extensions in the generated stylesheet):

<!-- In the stylesheet. -->
<elem>
  <xsl:attribute name="ns:dummy-for-xmlns" xmlns:ns="http://uri"/>
</elem>

<!-- In the result tree (in most cases). -->
<elem xmlns:ns="http://uri" ns:dummy-for-xmlns=""/>

So we can write a named template to handle all of this:

<xsl:template name="make-namespace-node">
  <xsl:param name="prefix"/>
  <xsl:param name="uri"/>
  <xsl:choose>
    <!-- All supported node-set() functions. -->
    <xsl:when test="function-available('exsl:node-set')">
      <xsl:variable name="dummy">
        <xsl:element name="{ $prefix }:e" namespace="{ $uri }"/>
      </xsl:variable>
      <xsl:copy-of select="exsl:node-set($dummy)/*/namespace::*"/>
    </xsl:when>
    <xsl:when test="function-available('msxsl:node-set')">
      <xsl:variable name="dummy">
        <xsl:element name="{ $prefix }:e" namespace="{ $uri }"/>
      </xsl:variable>
      <xsl:copy-of select="msxsl:node-set($dummy)/*/namespace::*"/>
    </xsl:when>
    <!-- If no node-set() available. -->
    <xsl:otherwise>
      <xsl:attribute name="{ concat($prefix, ':dummy-for-xmlns') }" namespace="{ $uri }"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

Back to Schematron

The skeleton for ISO Schematron is a meta-stylesheet. That means the result tree of the transformation is itself an XSLT stylesheet. The ns element in Schematron appear as childs of the root element. The goal is to generate namespace declarations on the generated element xsl:stylesheet. Are there some specificities we have to take care to?

There is one for sure. In the case a xxx:node-set() function is not available, we are creating a dummy attribute on the xsl:stylesheet. But this cannot be an attribute in the XSLT namespace. If it was, the XSLT processor would say it doesn't know this attribute when running the generated stylesheet, and report an error.

An error that can occurs is trying to add a namespace node with the same name of a namespace node already existsing on the element, but bound to another URI. But we cannot know all prefixes used in the generated stylesheet. The only thing we can do, I think, is asking schema authors to not bind usual prefixes to other URIs they are usually bound to. For example, if you really want to use the xsl prefix for the XSLT namespace, no problem. If you want to use it for your own namespace URI, accept all consequences.

But I'm affraid no check can be done formally. So here is what the template now looks for:

<xsl:template name="make-namespace-node">
  <xsl:param name="prefix"/>
  <xsl:param name="uri"/>
  <xsl:choose>
    <!-- EXSLT's node-set(). -->
    <xsl:when test="function-available('exsl:node-set')">
      <xsl:variable name="dummy">
        <xsl:element name="{ $prefix }:e" namespace="{ $uri }"/>
      </xsl:variable>
      <xsl:copy-of select="exsl:node-set($dummy)/*/namespace::*"/>
    </xsl:when>
    <!-- MSXSL's node-set(). -->
    <xsl:when test="function-available('msxsl:node-set')">
      <xsl:variable name="dummy">
        <xsl:element name="{ $prefix }:e" namespace="{ $uri }"/>
      </xsl:variable>
      <xsl:copy-of select="msxsl:node-set($dummy)/*/namespace::*"/>
    </xsl:when>
    <!-- If no node-set(), cannot handle XSLT namespace. -->
    <xsl:when test="@uri = 'http://www.w3.org/1999/XSL/Transform'">
      <xsl:message terminate="yes">
          <xsl:text>Using the XSLT namespace in Schematron </xsl:text>
          <xsl:text>rules is not supported in this processor: </xsl:text>
        <xsl:value-of select="system-property('xsl:vendor')"/>
      </xsl:message>
    </xsl:when>
    <!-- If no node-set(), dummy attribute trick. -->
    <xsl:otherwise>
      <xsl:attribute name="{ concat($prefix, ':dummy-for-xmlns') }" namespace="{ $uri }"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

Is it possible to use xsl:namespace if available?

This entry discusses only generating namespace nodes in an XSLT 1.0 stylesheet, because in XSLT 2.0 tere is the xsl:namespace that does this job. But an XSLT 1.0 stylesheet can be run by an XSLT 2.0 processor. And in this case it can uses XSLT 2.0 instructions. How to do that in a reliabe way?

If we add xsl:namespace in the XSLT 1.0 stylesheet, XSLT 1.0 processors will generate a compilation error, telling you that you probably made a mistake because you use an unknown XSLT instruction. Don't try to lie to your processor, and tell it the truth: that piese of code is actually XSLT 2.0 code. How? Simply by creating an external stylesheet module with @version="2.0".

Obviously, XSLT 2.0 processors will not have problem here. And XSLT 1.0 processors? They will run (for this module) in forwards-compatible mode. So they will not generate compilation error for XSLT instructions they don't know, but you still have to prevent them to actually instantiate these instructions. It is easy with element-available():

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
  <xsl:template name="make-namespace-node">
    <xsl:param name="prefix"/>
    <xsl:param name="uri"/>
    <xsl:choose>
      <!-- In XSLT 2.0. -->
      <xsl:when test="element-available('xsl:namespace')">
        <xsl:namespace name="{ $prefix }" select="$uri"/>
      </xsl:when>
      <!-- EXSLT's node-set(). -->
      <xsl:when test="function-available('exsl:node-set')" use-when="false()">
        <xsl:variable name="dummy">
          <xsl:element name="{ $prefix }:e" namespace="{ $uri }"/>
        </xsl:variable>
        <xsl:copy-of select="exsl:node-set($dummy)/*/namespace::*"/>
      </xsl:when>
      <!-- MSXSL's node-set(). -->
      <xsl:when test="function-available('msxsl:node-set')" use-when="false()">
        <xsl:variable name="dummy">
          <xsl:element name="{ $prefix }:e" namespace="{ $uri }"/>
        </xsl:variable>
        <xsl:copy-of select="msxsl:node-set($dummy)/*/namespace::*"/>
      </xsl:when>
      <!-- If no node-set(), cannot handle XSLT namespace. -->
      <xsl:when test="@uri = 'http://www.w3.org/1999/XSL/Transform'" use-when="false()">
        <xsl:message terminate="yes">
          <xsl:text>Using the XSLT namespace in Schematron </xsl:text>
          <xsl:text>rules is not supported in this processor: </xsl:text>
          <xsl:value-of select="system-property('xsl:vendor')"/>
        </xsl:message>
      </xsl:when>
      <!-- If no node-set(), dummy attribute trick. -->
      <xsl:otherwise use-when="false()">
        <xsl:attribute name="{ concat($prefix, ':dummy-for-xmlns') }" namespace="{ $uri }"/>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
</xsl:stylesheet>

Note the @version="2.0" and the use of @use-when. With an XSLT 2.0 processor, that will cause the other branches to be even not compiled. With an XSLT 1.0 processor, that will have no effect, because it doesn't know this attribute.

Labels:

1 Comments:

Blogger a.in.the.k (@ainthek) said...

Inspiring, thank you

07:56  

Post a Comment

<< Home