Search code examples
xmlxsltxpathforeachreturn

Does XSLT 1.0 support intermediate results?


TL;DR: How can I represent an <apply-templates /> statement with XPath?


I've got a template that collates information. I'd like to further process this information in more than one different way, so I was wondering whether there was a way to sort of "return" from a template in XSLT.

Example: I've got an XHTML snippet:

<page html:xmlns="html namespace">
  <html:p>
    The <html:a href="/foo">Tropical Foo</html:a> uses <html:a href="bar-language">Bar</html:a> to implement <html:a href="/programs/fizzbuzz>FizzBuzz</html:a>
  </html:p>
</page>

I've got a template to extract <a> tags with an href from an HTML snippet. I'd like to reuse this twice to both prefetch the pages and add a "Linked" bar, like so:

<html>
  <head>
    <link rel="prefetch" href="/foo" />
    <link rel="prefetch" href="bar-language" />
    <link rel="prefetch" href="/programs/fizzbuzz" />
  </head>
  <body>
    <main>
      <p>
        The <a href="/foo">Tropical Foo</a> uses <a href="bar-language">Bar</a> to implement <a href="/programs/fizzbuzz>FizzBuzz</a>
      </p>
    </main>
    <aside>
      <h2>Linked</h2>
      <ul>
        <li><a href="/foo">Tropical Foo</a></li>
        <li><a href="bar-language">Bar</a></li>
        <li><a href="/programs/fizzbuzz>FizzBuzz</a></li>
      </ul>
    </aside>
  </body>
</html>

Is this possible in XSLT 1.0?


If it's easier, I've got a related variant of the problem where I'm transforming an entire document with a stylesheet, and then want to use the transformed version. I know I can <xsl:include> the other-document-transforming stylesheet and write <xsl:apply-templates select="document('other.xml')"/>, but I want to further transform the result of this.


Solution

  • XSLT 1.0 adds one data type to the four (string, number, boolean, node-set) incorporated from the data model of XPath 1.0: result tree fragments (https://www.w3.org/TR/xslt-10/#section-Result-Tree-Fragments):

    This additional data type is called result tree fragment. A variable may be bound to a result tree fragment instead of one of the four basic XPath data-types (string, number, boolean, node-set). A result tree fragment represents a fragment of the result tree. A result tree fragment is treated equivalently to a node-set that contains just a single root node. However, the operations permitted on a result tree fragment are a subset of those permitted on a node-set. An operation is permitted on a result tree fragment only if that operation would be permitted on a string (the operation on the string may involve first converting the string to a number or boolean). In particular, it is not permitted to use the /, //, and [] operators on result tree fragments.

    So you can have intermediary results as result tree fragments but if you expect to use anything more than xsl:copy-of or xsl:value-of or taking the string value you need to go beyond XSLT 1.0 and in most XSLT 1.0 processors you have support for an extension function like exsl:node-set to do that, it converts a result tree fragment to a node-set.

    Here is a simple example that first processes some elements from an input to add an attribute and converts that result tree fragment into a node-set using exsl:node-set (http://exslt.org/exsl/functions/node-set/index.html) to then use that node-set twice for two different modes:

    <?xml version="1.0" encoding="UTF-8"?>
    <xsl:stylesheet
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
        xmlns:exsl="http://exslt.org/common"
        xmlns:msxml="urn:schemas-microsoft-com:xslt"
        exclude-result-prefixes="exsl msxml"
        version="1.0">
    
      <xsl:output method="html" indent="yes" version="5" doctype-system="about:legacy-doctype"/>
    
      <xsl:variable name="numbered-items-rtf">
          <xsl:apply-templates select="//item" mode="number"/>
      </xsl:variable>
    
      <xsl:variable name="numbered-items" select="exsl:node-set($numbered-items-rtf)/item"/>
    
      <xsl:template match="item" mode="number">
          <xsl:copy>
              <xsl:attribute name="index">
                  <xsl:number/>
              </xsl:attribute>
              <xsl:copy-of select="node()"/>
          </xsl:copy>
      </xsl:template>
    
      <xsl:template match="@* | node()">
        <xsl:copy>
          <xsl:apply-templates select="@* | node()"/>
        </xsl:copy>
      </xsl:template>
    
      <xsl:template match="/">
        <html>
          <head>
            <title>.NET XSLT Fiddle Example</title>
            <style>
                nav ul li { display: inline }
                nav ul { list-item-type: none }
            </style>
          </head>
          <body>
              <h1>Example</h1>
              <nav>
                  <ul>
                      <xsl:apply-templates select="$numbered-items" mode="nav"/>
                  </ul>
              </nav>
              <section>
                  <h2>List</h2>
                  <ul>
                      <xsl:apply-templates select="$numbered-items"/>
                  </ul>
              </section>
          </body>
        </html>
      </xsl:template>
    
      <xsl:template match="item">
          <li id="item-{@index}">
              <xsl:apply-templates/>
          </li>
      </xsl:template>
    
      <xsl:template match="item" mode="nav">
          <li>
              <a href="#item-{@index}">
                  <xsl:apply-templates/>
              </a>
          </li>
      </xsl:template>
    
    </xsl:stylesheet>
    

    A sample input like

    <root>
        <items>
            <item>foo</item>
            <item>bar</item>
            <item>baz</item>
        </items>
    </root>
    

    is transformed to

    <!DOCTYPE html SYSTEM "about:legacy-doctype">
    <html>
      <head>
        <META http-equiv="Content-Type" content="text/html; charset=utf-16">
        <title>.NET XSLT Fiddle Example</title>
        <style>
                nav ul li { display: inline }
                nav ul { list-item-type: none }
            </style>
      </head>
      <body>
        <h1>Example</h1><nav><ul>
            <li><a href="#item-1">foo</a></li>
            <li><a href="#item-2">bar</a></li>
            <li><a href="#item-3">baz</a></li>
          </ul></nav><section><h2>List</h2>
          <ul>
            <li id="item-1">foo</li>
            <li id="item-2">bar</li>
            <li id="item-3">baz</li>
          </ul></section></body>
    </html>
    

    https://xsltfiddle.liberty-development.net/pPqsHUd/1

    The draw back is that some XSLT 1.0 processors don't support the exsl:node-set function but a similar in a proprietary namespace (for instance Microsoft's COM based MSXML (3,4,5,6) processors only support msxml:node-set in the namespace xmlns:msxml="urn:schemas-microsoft-com:xslt", like does the (obsolete) .NET based XslTransform). As long as you target a single XSLT 1.0 processor you can of course adapt your code to use the right namespace/extension function but if you want to target different ones you will struggle to find a compact and elegant way to use different extension functions based on function-available as you don't have an if expression in XPath 1.0.

    So https://xsltfiddle.liberty-development.net/pPqsHUd/2 works with e.g. Chrome and with Mozilla browsers like Firefox as these browsers do support exsl:node-set but fails in Microsoft IE and Edge as these use MSXML and don't support exsl:node-set, for them you need <xsl:variable name="numbered-items" select="msxml:node-set($numbered-items-rtf)/item"/> as done in https://xsltfiddle.liberty-development.net/pPqsHUd/3.

    In IE you can use a script extension to have it support exsl:node-set but unfortunately in Edge this doesn't work: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7598626/.