Digital humanities


Maintained by: David J. Birnbaum (djbpitt@gmail.com) [Creative Commons BY-NC-SA 3.0 Unported License] Last modified: 2022-10-31T21:16:58+0000


XSLT, part 2: Advanced features

This supplementary XSLT tutorial concentrates on four topics: variables, keys, conditionals (<xsl:if> and <xsl:choose>), and the difference between push processing (<xsl:apply-templates> and <xsl:template>) and pull processing (<xsl:for-each> and <xsl:value-of>).

The <xsl:variable> element

If you’ve used variables in other programming languages before, be aware that variables in XSLT do not work like variables in most other languages. The value of a variable in XSLT cannot be updated once the variable has been declared.

The <xsl:variable> element requires a @name attribute, which names the variable for future use. The value of the variable is typically assigned through a @select attribute (in which case <xsl:variable> is an empty element, e.g., <xsl:variable name="author" select="'William Shakespeare'"/>), but it can also be specified as the content of the <xsl:variable> (in which case the element cannot have a @select attribute, e.g., <xsl:variable name="author">William Shakespeare</xsl:variable>). It’s usually easier to use @select, since this typically produces less complicated code, but if you need to do anything particularly involved, you may not be able to use the @select method of assigning your value. To reference a variable later on, precede the name with a dollar sign, that is, use $variableName, where variableName is the value of the @name attribute you wrote when creating the variable. For example, when you declare a variable to, say, count the paragraphs (<p> elements) in your input, you might give it the name paragraphCount with something like:

<xsl:variable name="paragraphCount" as="xs:integer" select="count(//p)"/>

Note that there is no leading dollar sign associated with the name when you declare and define a variable. But should you later refer to the variable, you need the leading dollar sign. For example, to get the value of the variable, you might use:

<xsl:value-of select="$paragraphCount"/>

The @as attribute is technically optional, but there are situations where omitting it can produce unwanted behavior, we use it consistently in our own work, and it is required for all variable definitions in this course. Common values for @as when setting variables equal to atomic values are xs:integer, xs:double (non-integer numbers), xs:decimal (non-integer numbers where exact precision mattters, as in dollar-and-cents notation). You can also set variables equal to one or more nodes; for example:

<xsl:variable name="allParagraphs" as="element(p)*" select="//p">

sets the value of the variable $allParagraphs equal to a sequence of all <p> elements in the document. The @as values accept the same repetition indicators as Relax NG: no repetition indicator means exactly one item, a question mark means zero or one, a plus sign means one or more, and an asterisk means zero or more.

The <xsl:variable> element may be defined in different locations in the stylesheet, and the location makes a difference. When you define a variable (that is, use the <xsl:variable> element) as an immediate child of the root <xsl:stylesheet> element, the variable becomes available for use anywhere in the stylesheet. This is called a stylesheet variable (comparable to what other programming languages call a global variable). When, on the other hand, you define a variable inside a template rule, it’s available only within that template rule. Where a variable is available is referred to as its scope, and the general rule is that variables are scoped to all descendants of their parent. For example, if the parent is a template rule, the variable is available anywhere inside the template rule, but not outside.

We often use a top-level <xsl:variable> element to avoid having to recalculate a value that is used frequently, as well as to access the tree from an atomized context (such as when you've used <xsl:for-each>, which we’ll explain when the situation arises). You might also use variables just to improve legibility, that is, to avoid typing a long XPath expression within some other complicated instruction. Variables that are not strictly necessary and that are created for the convenience of the developer are called, not surprisingly, convenience variables.

For more information about variables, see Michael Kay, 500 ff.

The <xsl:key> element

<xsl:key> may be overlooked in situations where comparable functionality is available through other means, but it is often simpler (and almost always faster) to use <xsl:key> than the alternatives (we once reduced the run time for a transformation from twenty minutes to just a few seconds by switching to an implementation that used <xsl:key>!). <xsl:key> is an empty element that requires three attributes. Consider, for example, XML structured like:

<book>
  <title>XSLT 2.0 and XPath 2.0 programmer’s reference</title>
  <author>Michael Kay</author>
  <publisher>Wrox</publisher>
  <edition>4</edition>
  <year>2008</year>
</book>

where you want to be able to find books by their authors. You could define a key as:

<xsl:key name="bookByAuthor" match="book" use="author"/>

The three required attributes in this case are:

The @match attribute value of the key is the object (typically an element) that the processor will return when the key is referenced (see below), while the @use attribute value tells the processor what to use to look up those values. In the example above, you would be able to use the key to retrieve <book> elements according to their <author> child elements. To retrieve information with the help of a key, you use the key() XPath function, which takes two or three arguments. The first argument is the name of the key (matching the @name value from the <xsl:key> element), and it must be in quotation marks (single or double). The second argument is the value to look up; for example, in the sample above, if you were to specify Michael Kay as the second argument to the key() function (key("bookByAuthor","Michael Kay")), you would retrieve all <book> elements with <author> children that have the value Michael Kay. The (optional) third argument is the document root of the document in which to look. When the third argument is omitted, the function searches in the current document, which is what we want to do most often. For further discussion of <xsl:key>, consult Michael Kay, page 376.

Conditionals

<xsl:if>

<xsl:if> is useful when you treat the same node differently under different circumstances. For example, you might use <xsl:if> to color all <speaker> elements with the @who value Hamlet differently from all other <speaker> elements. For that sort of task, you would have a templates that matches all <speaker> elements and inside it you would test the value and do something extra if the value is equal to the string Hamlet.

<xsl:if> takes a required attribute @test, which is an XPath expression that evaluates to a Boolean value (either True or False), just like a predicate expression in XPath or a @test attribute on <sch:assert> and <sch:report> in Schematron. The contents of the <xsl:if> element, then, describe what the system is to do if the result of @test is True: for example, you might want to apply templates or use <xsl:value-of> to display the results of a particular function, or you might want to create a special @class attribute value (if you are generating HTML) using <xsl:attribute> that can be styled with CSS (see our Using <span> and @class to style your HTML to refresh your memory about the @class attribute). Consider:

<xsl:template match="sp">
  <p>
    <xsl:if test="speaker='Hamlet'">
      <xsl:attribute name="class">mainCharacter</xsl:attribute>
    </xsl:if>
    ...
  </p>
</xsl:template>

In this example we are checking each <sp> (because we’re doing this inside the template rule for <sp> elements) to see whether its child <speaker> (remember that we default to the child axis) is equal to the string Hamlet. If the result of this test is True, we’ll go on to perform whatever is inside <xsl:if>. If it isn’t, we won’t do anything special with it. In this case, everywhere this test is True we’ll create an attribute using <xsl:attribute>, and we use the @name attribute to specify what name this attribute should have: in this case we’re creating the attribute @class. This attribute gets attached to the parent element: in this case, <p>. The contents of <xsl:attribute> indicate the value to be assigned to this new attribute: in this case the value of @class will be mainCharacter. This means that anywhere there’s a speech by Hamlet, we’re mapping it to something like:

<p class="mainCharacter"> … </p>

If we then have a rule in our CSS like:

.mainCharacter { color: red; }

any <p> element that contains a speech by Hamlet will have this attribute and will now be colored red.

<xsl:choose>

<xsl:if> is useful to trigger special behavior under specific individual circumstances, such as whether a speech is or is not by Hamlet, but sometimes we need to code for alternative possible conditions, or we care about what should happen when the results of our conditional are False, and this is where <xsl:choose> comes in. <xsl:if> can run only one test and can have only one result action, which it performs if the test evaluates to True. On the other hand, you can use <xsl:choose> to specify a number of alternative conditions, as well as a fallback action if none of the conditions is True. <xsl:choose> takes at least one child <xsl:when> element (and up to as many as you want) and one optional <xsl:otherwise> element. <xsl:when> requires the same type of @test attribute that we discussed above for <xsl:if>. Since <xsl:otherwise> is the fallback condition, it doesn’t take this @test attribute; it applies only when all <xsl:when> tests return False.

Here’s an example that does one thing with Hamlet’s speeches, a different thing with Ophelia’s speeches, and third thing (the otherwise) for speeches by anyone else:

<xsl:template match="sp">
  <p>
    <xsl:choose>
      <xsl:when test="speaker='Hamlet'">
        <xsl:text>[Hi, Hamlet!] </xsl:text>
      </xsl:when>
      <xsl:when test="speaker='Ophelia'">
        <xsl:text>[Hi, Ophelia!] </xsl:text>
      </xsl:when>
      <xsl:otherwise>
        <xsl:text>[Neither Hamlet nor Ophelia] </xsl:text>
      </xsl:otherwise>
    </xsl:choose>
    <xsl:apply-templates/>
  </p>
</xsl:template>

In this example, we have two tests (<xsl:when>) and one fallback (<xsl:otherwise>), which is used if neither test returns True. The first test checks whether the child <speaker> element (remember that we’re in the template rule for <sp>) is equal to Hamlet. If it is, we use the <xsl:text> element to create a text node with the content [Hi, Hamlet!] , which means that we return the plain text: [Hi, Hamlet!] . The second test works along the same lines, except that it checks whether the child <speaker> is equal to Ophelia. If this test is True, then we return plain text reading [Hi, Ophelia!] . If neither of these tests returns True (that is, if the speaker is anyone other than Hamlet or Ophelia), then the <xsl:otherwise> condition kicks in. In this case, that means that we return the plain text [Neither Hamlet nor Ophelia] . Note that we’ve put in a space at the end of each of these strings of plain text because we apply templates at the end of this block of conditionals in order to output the speech, and we want a space before it. If you run this code, your output should look like this (we’ve added bolding to the speaker names to make them easier to see here):

[Neither Hamlet nor Ophelia] Osric: It is indifferent cold, my lord, indeed.

[Hi, Hamlet!] Hamlet: But yet methinks it is very sultry and hot for my
complexion.

Differences between <xsl:if> and <xsl:choose>

An <xsl:if> element is equivalent to an <xsl:choose> that has exactly one <xsl:when> child and no <xsl:otherwise>. In that case it’s obviously better to use <xsl:if> because it says what it means more directly and clearly than the alternative.

If you have multiple <xsl:if> elements, one after another, they all fire, while the options in <xsl:choose> are mutually exclusive, which means that as soon as one test is true, the others are abandoned. If all you want to do is style Hamlet’s speeches one way and Ophelia’s speeches a different way and do nothing special for speeches by other candidates, you would get the same behavior from two consecutive <xsl:if> elements or from two <xsl:when> children of a single <xsl:choose>. In this situation you should use <xsl:choose> because the options are mutually exclusive, so if a speech is by Hamlet, there’s no reason to test whether it’s by Ophelia. Although you would get the same behavior from both approaches, the <xsl:choose> is easier to understand and more self-documenting. (It is also more efficient computationally, although the difference in processing time is unlikely to be noticeable with such a simple example.)

The examples above of <xsl:if> and <xsl:choose> came from the following stylesheet, which is included in its entirety for your reference. It outputs all of the speeches in Bad Hamlet normally, but we have the system do some extra formatting depending on whether the speaker is Hamlet, Ophelia, or anyone else. If you use it to transform the play, you’ll see how the formatting works, and how new content is created before each speech.

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://www.w3.org/1999/xhtml"
  xmlns:math="http://www.w3.org/2005/xpath-functions/math" 
  exclude-result-prefixes="#all" version="3.0">
  <xsl:output method="xhtml" html-version="5" omit-xml-declaration="no" 
   include-content-type="no" indent="yes"/>
  <xsl:template match="/">
    <html>
      <head>
        <title>XSLT conditional practice</title>
      </head>
      <body>
        <h1>XSLT conditional practice</h1>
        <xsl:apply-templates select="//sp"/>
      </body>
    </html>
  </xsl:template>
  <xsl:template match="sp">
    <p>
      <xsl:if test="speaker='Hamlet'">
        <xsl:attribute name="class">mainCharacter</xsl:attribute>
      </xsl:if>
      <xsl:choose>
        <xsl:when test="speaker='Hamlet'">
          <xsl:text>[Hi, Hamlet!] </xsl:text>
        </xsl:when>
        <xsl:when test="speaker='Ophelia'">
          <xsl:text>[Hi, Ophelia!] </xsl:text>
        </xsl:when>
        <xsl:otherwise>
          <xsl:text>[Neither Hamlet nor Ophelia] </xsl:text>
        </xsl:otherwise>
      </xsl:choose>
      <xsl:apply-templates/>
    </p>
  </xsl:template>
  <xsl:template match="speaker">
    <strong>
      <xsl:apply-templates/>
      <xsl:text>: </xsl:text>
    </strong>
  </xsl:template>
  <xsl:template match="l | ab">
    <xsl:apply-templates/>
    <xsl:if test="following-sibling::l or following-sibling::ab">
      <br/>
    </xsl:if>
  </xsl:template>
</xsl:stylesheet>

As an alternative to using <xsl:if> or <xsl:choose> to handle speeches by Hamlet specially, you can use alternative templates, one to match speeches by Hamlet and one to match all other speeches:


  

]]>

Both templates technically match Hamlet’s speeches, but the template with the predicate has higher priority, so it will process all of Hamlet’s speeches and the other template will process all speeches not by Hamlet.

Whether you use templates with different @match values or the conditional elements <xsl:if> or <xsl:choose>for this sort of subtype handling is up to you. In our own work we usually favor <xsl:if> if the processing is largely the same and the difference can be described with an isolated code snippet, and we use different templates if the differences are more comprehensive. Templates are one of the most distinctively and characteristically powerful yet simple features of XSLT, and becoming comfortable with templates is an important aspect of learning to code idiomatically in XSLT.

Push and pull design

The XSLT processing model supports both push and pull design. The push model, which is what we’ve been using exclusively so far, relies on <xsl:apply-templates> to identify what is supposed to get processed where, but not to do the processing; the actual processing is handled by <xsl:template> elements. This is called push because you push the elements and other components out into the stylesheet and rely on the templates to grab the individual pieces and process them. For example, you don’t say take all the paragraphs and paint them blue; what you do instead is say in one place here are some paragraphs; take care of them and in another whenever you happen to run into a paragraph, paint it blue. The great strength of push processing is that you don’t have to know the structure of your input document—that is, you don’t have to know which elements will be encountered where. The declarative template rules ensure that no matter where an element pops up, you’ll have a template around that will know what to do with it. Since the structure of humanities documents involves a lot of variable mixed content, this declarative approach creates a flexibility that is difficult to achieve with the sort of imperative programming that requires you to know at each moment exactly what is supposed to happen next.

The pull model, on the other hand, is imperative in nature, and relies primarily on <xsl:for-each> and <xsl:value-of>. Pull processing is helpful when you need to round up specific information, instead of dealing with it on the fly whenever it happens to come up. Pull processing is useful, for example, if you want to count the speeches for each character in a play. In this case you don’t want to process each speech where it occurs (to do that you would apply templates to <sp> elements); you want instead of get a sequence of distinct speaker names (with distinct-values()), iterate over them (with <xsl:for-each>), and count the speeches by each of them in turn. The pull model would work poorly, on the other hand, for rendering each speech as it occurs, since any speech could contain an unpredictable variety of in-line elements, and push lets you deal with those as they arise, without having to know in advance which ones to call for explicitly.

An alternative way to count the speeches for each character uses <xsl:for-each-group>. See Kay pp. 326 ff.

About pull

Pull design is frequently overused by beginning XSLT programmers, especially if they have experience with imperative programming. In many cases the end result of using pull will be the same as the result of using push, but pull design is often harder to maintain because it is less consistent with the declarative nature of XSLT as a programming language. With that said, there are situations where pull design is the more appropriate choice, and the two principal elements used in pull coding are <xsl:for-each> and <xsl:value-of>.

<xsl:for-each>

The <xsl:for-each> element is used to iterate over a sequence of items (nodes or atomic values). It requires one attribute, @select, the value of which is a full XPath expression (just like the value of the @select attribute with <xsl:apply-templates>). Whatever @select identifies becomes the sequence of current context items, so any XPath expressions used in children of <xsl:for-each> begin at the current context node, not at the document node.

This is an advanced detail that you’re welcome to skip over, but: Strictly speaking, <xsl:for-each> mimics iteration, but it is not actually iterative, and since version 3.0 XSLT provides an <xsl:iterate> element that behaves in a way that is more similar to how a for loop operates in imperative programming. Ask us if you’re curious about the details.

We often use <xsl:for-each> with scalable vector graphics (SVG), which we’ll be introducing later in the semester.

<xsl:value-of>

Although the results of <xsl:value-of> and <xsl:apply-templates> are often the same, the real usefulness of <xsl:value-of> is that it allows you to output, in a transparent and self-documenting way, both the results of functions and non-node values, on the one hand, and the value of a node instead of the node itself. Here is an example that produces a list of all distinct speakers in a play, without duplicates:

<xsl:for-each select="distinct-values(//speaker)">
  <li><xsl:value-of select="."/></li>
</xsl:for-each>

If you want to do something to every instance of a <speaker> element in a play, though, repeats and all, you should prefer <xsl:apply-templates>. The difference is that each instance of a <speaker> element is a node in the tree, but the sequence produced by applying the distinct-values() function to all of the <speaker> nodes is a sequence of atomic values, and not of nodes.

The following example creates an HTML page that lists the number of speeches by each speaker in Bad Hamlet:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://www.w3.org/1999/xhtml"
  xmlns:math="http://www.w3.org/2005/xpath-functions/math" 
  xpath-default-namespace="http://www.tei-c.org/ns/1.0" 
  exclude-result-prefixes="#all" version="3.0">
  <xsl:output method="xhtml" html-version="5" omit-xml-declaration="no" 
    include-content-type="no" indent="yes"/>
  <xsl:template match="/">
    <html>
      <head>
        <title>Bad Hamlet Speeches</title>
      </head>
      <body>
        <xsl:for-each select="//role">
          <p>
            <xsl:value-of select="."/>
            <xsl:text>: </xsl:text>
            <xsl:value-of select="count(//sp[contains-token(@who, current()/@xml:id)])"/>
          </p>
        </xsl:for-each>
      </body>
    </html>
  </xsl:template>
</xsl:stylesheet>

Here we loop through each <role> element in the whole of Bad Hamlet (at any depth, as specified by the // at line 14) and create a <p> element for each one (lines 15–19). We know (because that’s how cast lists work in TEI, and that’s one reason why we’re getting our character list from the <role> elements, instead of by collecting and deduplicating <speaker> elements or @who attributes) that the <role> elements are unique, so we don’t need to deduplicate them. Inside each <p> we return the value of the context node; the node is represented by the . and the value is a distinct speaker name, as a string (line 16). We then output a colon followed by a space, just as plain text (line 17). Finally we return a count of all the <sp> elements that meet the condition of having a @who value that contains the @xml:id of the speaker we are looking at at the moment (line 18).

We use contains-token() instead of testing for equality because some speeches are uttered by more than one person, in unison, and we want to count those toward the total for each of the speakers. We use contains-token() instead of the more familiar contains() because contains() will match on substrings, while contains-token() starts by atomizing its first argument into whitespace-delimited word tokens (that is, words) and then tests only for matches against complete tokens. In this document it happens to be that there are no @xml:id values on <role> elements that are subsequences of one another, but that situation isn’t impossible, so we don’t make a risky and unnecessary assumption. The contains-token() function is new in XPath 3.1, so it isn’t in Michael Kay, but it is in the more recent XPath function references listed in the XPath section of our main course page.

The predicate in line 18 says get all the <sp> elements in the entire play and check to see whether their @who attributes contain some substring. When we’re executing an <xsl:for-each> loop, the value of each item in the loop can be represented by current(). This means that we’re comparing the value of the @who attribute of each <sp> element to the @xml:id attribute of the <role> element that we’re processing at the moment. For example, when we process <role xml:id="Hamlet">Hamlet</role>, we check the @who attribute of every <sp> in the play to see whether it contains, as a substring, the value of the @xml:id attribute of that <role>. If it does, that’s a speech by Hamlet, so it gets included in our count. After we’ve gone through every <sp> and checked who the speaker is, we output the count of the speeches for the <role> we’re looking at at the moment. Then we move on to the next <role>. When we run out of roles, the <xsl:for-each> terminates gracefully.

About current()

This is the first time you’ve seen the function current(), and you may be wondering why you can’t write:

count(//sp[contains-token(@who, ./@xml:id)])

(with a dot instead of current()). The problem is that the dot refers to wherever you are at that moment in your current XPath expression. Since you’re inside a predicate that is being applied to a preceding <sp>, a dot would check the @xml:id attribute of the <sp>, and not of the <role>. Since the <sp> doesn’t have an @xml:id attribute (it’s the <role> that does), this wouldn’t find the matches we care about. That is:

You may find the following distinction helpful: current() refers to the current context at the XSLT level and the dot refers to the current context in an XPath expression. At the first step of an XPath path expression, the two mean the same thing. In the example above, when we output <xsl:value-of select="."/> we could instead have said <xsl:value-of select="current()"/>, since in this simple path the XSLT and XPath contexts are the same. We don’t have that choice in count(//sp[contains(@who, current()/@xml:id)]), though; here the more complicated XPath includes a new step, //sp, which changes the XPath context. Here we need to use current() because the XSLT context was set at the <xsl:for-each> stage, and is unaffected by the comparison. For more discussion, with examples, see Michael Kay, p. 735.

If you try to run this code, you’ll notice that it takes a bit longer than usual to finish. That’s because it’s looping through the entire play repeatedly, looking at every speech once for every role in the play. At 1137 <sp> elements and 37 <role> elements, that’s 42069 comparisons. This is part of why we usually avoid using <xsl:for-each> unless the problem really calls for it, and in those cases there are ways to speed it up (such as by using a key, as described above).

There are situations that can be managed with either push or pull strategies. In most of those cases, your instinct, unless you are a veteran XSLT programmer, will draw you toward pull. It’s much more common in humanities-oriented XSLT to use push programming, so where there’s a choice, we’d encourage you to train yourselves to think of push first, and fall back on pull only where it is truly more appropriate.