Thinking in XSLT: An introduction to XPath

I posted the first installment of my Thinking in XSLT series back in February 2020. Then the pandemic happened and I put off writing the second post for a while and then put it off some more aaaand then some more. But no longer! We’re doing BloPoWriMo and now I have committed in writing to getting it done 🙂

The topic of the second post in the series is XPath. XPath is short for XML Path Language and in a nutshell it’s a language you use to pull content out of an XML document. There are other ways to accomplish the same goal, but with XPath you can do it in a very succinct and elegant way that is very powerful once you get the hang of it.

XPath is a core part of XSLT. In fact, the two are so intertwined that you’d be excused for assuming that they are one and the same. But XPath is actually a separate standard (or recommendation in the terminology of the World Wide Web Consortium).

My goal with this blog post is to introduce you to the most important XPath concepts. It’s not an XPath tutorial, but I hope to give you a foundation for further study of this important technology.

Here’s an example of the most basic XPath expression:

/parent-element/child-element

This is called a path expression, and this particular one selects every element called child-element that has the document’s root element (called parent-element) as its parent. Path expressions can be absolute (starting from the document root like the example above) or relative (starting from a context node). The former always start with a forward slash, the latter do not.

It’s important to understand that a path expression can return multiple nodes. In the following example, the above path expression returns the highlighted elements:

So far, path expressions sort of look like the path to a file or directory in a Unix-like file system. And like file system paths, path expressions also support wildcards. You can use an asterisk in a path expression to represent any element. The following path expression selects grandchild-element elements regardless of what their parent element is:

/parent-element/*/grandchild-element

Applied to the following example document, the above path expression selects the highlighted elements:

A path expression can contain multiple wildcards. The previous example can be rewritten as follows:

/*/*/grandchild-element

It will select the same grandchild-element elements, but now the path expression is independent of what the document’s root element is called.

Up until now we’ve seen a single forward slash separating two element names in a path. This indicates a parent/child relationship: The former element is the parent of the latter element. But what if you want to select amongst all an element’s descendants instead of just its children? There is a general answer to that, which is XPath axes (we’ll get to those later), but there is also a shortcut: The double slash.

Separating two elements in a path with the double slash means that any number of elements (including zero) can occur between them. Here’s an example:

/parent-element//grandchild-element

This path expression selects all grandchild-element elements that occur anywhere below the root element. Here I’ve applied it to a sample document:

The double slash can start from the document’s root, which means that we can select the same elements with this path:

//grandchild-element

Before we are done covering the basics, I want to add one more thing: How to select attributes. So far, the examples have all selected elements from the XML document, but sometimes you need to get at attributes too, of course. To do that, you use the at sign (@). Here’s an example:

/book/@isbn

This path expression selects the attribute called isbn from the document’s root element called book. Here I’ve applied it to a sample document:

Now, if the basic path expression was all XPath had to offer, it would still be a really useful tool. There is, however, a lot more. Read on!

The paths we’ve seen so far select elements and attributes with a certain name and/or location in the document, but what if we want to select nodes, that satisfy a certain condition instead? That is what predicates are for. Predicates live in square brackets and one of the first ones you see is probably similar to this one:

/lines/line[3]

It selects the third line element that has the document root as its parent. Here’s what the expression looks like when it’s applied to a sample document:

The number 3 in square brackets is really just a shorter version of this predicate:

/lines/line[position() = 3]

This example demonstrates more clearly what predicates are: The expression in square brackets is evaluated, and the path expression only selects nodes for which the predicate evaluates to true.

By the way: position() is an XPath function which returns a node’s position within the current context. XPath has a huge function library, that you should spend some time familiarising yourself with. You can find a list of every single supported function here.

Expressions involving comparison operators (like equals, greater than etc.) obviously evaluate to true or false, but a predicate can contain other expressions too. The only requirement is that they can be evaluated to true or false. Here is an example:

/lines/line[text()]

Here, the expression in the predicate evaluates to a sequence of nodes, specifically the line element’s text node children. In the context of a predicate, such a sequence evaluates to true if it contains at least one node and false if it’s empty. In other words: The path expression selects every line element that has text content. This is what it looks like applied to a sample document:

You can use the Boolean operators (and and or) to construct more complicated predicates.

With the basic path expression, predicates and built-in XPath functions you can get a lot of work done! There is one advanced feature, though, that I’d like to introduce: Axes (as in axis plural, not the tools you chop wood with).

Axes specify the direction when navigating through an XML document. If you think of the document as a node tree, with the document root at the top, so far we’ve only been navigating down the tree; from parent nodes to child nodes and descendants further down. But sometimes you also need to navigate up and sideways. With XPath’s axes, you can do that.

The child axis is the one we’ve been using so far. In fact it’s the default axis, which is why we haven’t had to specify it. If we were to rewrite that very first example with explicit axes, it would look like this:

/child::parent-element/child::child-element

Let’s try a new axis: following-sibling. With this axis, we can select from nodes that have the same parent as the context node and follow it in the so-called document order. An example will probably make that more clear 🙂

Let’s say we want to select all line elements except the first two. We can do this with a predicate:

/lines/line[position() > 2]

Applied to the sample document we used earlier, this is the result:

Looks good; all line elements except the first two have been selected. Now, let’s rewrite the path expression to use the following-sibling axis instead. Here goes:

/lines/line[2]/following-sibling::line

You can think of this as first navigating to the second line element, and then selecting all of the sibling line elements that follow it.

(In practice, you would always use the predicate; the rewrite was only for demonstrating the following-sibling axis.)

XPath has 13 axes in total. I’ll list them below with a brief description. For all the gory details, please refer to the W3C recommendation.

  • child: This axis contains the child nodes of the context node.
  • descendant: This axis contains all descendant nodes of the context node (i.e. its child nodes, their child nodes and so on).
  • attribute: This axis contains the attribute nodes of the context node, if the context node is an element. Otherwise it’s empty.
  • self: This axis contains just the context node.
  • descendant-or-self: Identical to the descendant axis but also includes the context node.
  • following-sibling: This axis contains those siblings of the context node that follow it in the document order.
  • following: This axis contains all nodes that come after the context node in the document order.
  • namespace: This axis contains the namespace nodes of the context node, if the context node is an element. Otherwise it’s empty.
  • parent: This axis contains the parent of the context node. It’s empty if there is no parent.
  • ancestor: This axis contains all ancestor nodes of the context node (i.e. its parent, the parent’s parent etc. all the way up to the document root).
  • preceding-sibling: This axis contains those siblings of the context node that precede it in the document order.
  • preceding: This axis contains all nodes that come before the context node in the document order.
  • ancestor-or-self: Identical to the ancestor axis but also includes the context node.

Remember how we used //grandchild-element to select all grandchild-element elements in the document? With our new knowledge of the axes, that path expression is equivalent to this one:

/descendant::grandchild-element

Don’t worry about memorising all the axes! Now that you know about the concept, you can look the right axis up when you need it.

That’s it for the second installment of the Thinking in XSLT series. I hope you enjoyed it! If you have any questions about the content, please post them in the comments and I’ll be happy to answer them. And if you have suggestions for future topics, please do share them in the comments as well.