Skip to content
datahacks.dev
XML

Debugging an XSLT Transform That Outputs Nothing or Too Much

Five stylesheets that output nothing or the whole file as text, with the cause of each: a namespace, a built-in rule, an absolute path, and a literal id.

7 min read
Summarize in AI
Debugging an XSLT Transform That Outputs Nothing or Too Much

"The stylesheet produces nothing" is the most common XSLT problem, and its cousin, "the stylesheet produces the whole file as text", has the same causes. The second is the built-in rules at work (XSLT basics: templates, match and apply). In both cases the processor found no template where you expected one, or a path that selected nothing. Here are five minimal stylesheets that fail, the output each gives and how I found the cause. They cover most of what I have met.

1. The namespace

dbg_ns.xmlXML
<feed xmlns="http://www.w3.org/2005/Atom"><entry><title>One</title></entry><entry><title>Two</title></entry></feed>
dbg1.xslXML
<xsl:template match="/feed">
  <xsl:for-each select="entry"><xsl:value-of select="title"/><xsl:text>;</xsl:text></xsl:for-each>
</xsl:template>

Output: OneTwo. Not nothing, and not what was asked for. The expected output was One;Two;. The feed's elements are in the Atom namespace, and match="/feed" means an element called feed in no namespace. It matched nothing, so the built-in rules ran, walked the tree, and copied the text nodes: One and Two, with no separators, since my ; was in the template that never ran. The telltale of this case is output that is the document's text without the markup, no matter what the stylesheet says. The fix declares the namespace on the stylesheet and uses a prefix in every path:

dbg2.xslXML
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:a="http://www.w3.org/2005/Atom">
  <xsl:template match="/a:feed">
    <xsl:for-each select="a:entry"><xsl:value-of select="a:title"/><xsl:text>;</xsl:text></xsl:for-each>
  </xsl:template>

Output: One;Two;. The prefix a is yours; only the URI matters. Every element name in a path needs it, a:entry and a:title as well. Attributes are not affected (an unprefixed attribute is in no namespace). Forgetting one is a common second bug: with a:feed and a:entry right but a bare title, the selection finds no title and prints empty strings, giving ;;.

2. An absolute path inside a template

dbg3.xslXML
<xsl:template match="/library">
  <xsl:message>matched library with <xsl:value-of select="count(book)"/> books</xsl:message>
  <xsl:for-each select="/book"><xsl:value-of select="title"/></xsl:for-each>[end]
</xsl:template>

On the library file the standard output was [end], and standard error printed matched library with 3 books. A path that begins with / starts at the document root, not at the current node, so /book asks for a book element that is a child of the root, and the real ones are children of library. The loop ran zero times. The xsl:message is the instrument that located it: it printed, so the template ran and the count of books was 3, which cleared the match pattern and the input and left the select as the suspect. Fix it with a relative path (select="book"), which is relative to the current node (library). A rule of thumb: inside a template, use relative paths; reserve the leading slash for the root template.

3. No template for the root, and whitespace

dbg4.xslXML
<xsl:output method="text"/>
<xsl:template match="book"><xsl:value-of select="title"/></xsl:template>

The output had the three titles, and also a line break and spaces before the first and between the books. The processor started at the root with no template, used the built-in rule (process the children), reached each book and ran my template; but the text nodes between the books, which are whitespace from the indentation, went through the built-in text rule and were copied. This is why text output from stylesheets often has stray blank lines. Add <xsl:strip-space elements="*"/> to drop whitespace-only text nodes from the input, or write a root template that selects exactly what you want and nothing else.

4. A literal that matches nothing

dbg5.xslXML
<xsl:template match="/library/book[@id='b9']"><xsl:value-of select="title"/></xsl:template>

There is no book b9. The output was not empty: it began with a newline and indentation and then Cache Patterns, continuing through the document's text. Nothing matched, so the built-in rules copied the entire document's text, exactly as in the namespace case. If you see "the whole file, as text" in the output, the stylesheet's templates did not match; it is not the data that is wrong. The test is an xsl:message in the template or a deliberately wrong value that you expect to produce output, to check the pattern fires at all.

5. The tools: how to find where it stops

A short list of instruments, in the order I use them.

Make the empty case visible. Add a template at low priority that matches * and writes a message with the name of the node: <xsl:template match="*" priority="-10"><xsl:message>unhandled <xsl:value-of select="name()"/></xsl:message><xsl:apply-templates/></xsl:template>. Any element no other template handles shows up on standard error. On the namespaced feed it printed unhandled feed in http://www.w3.org/2005/Atom, then the same for entry and title: the namespace URI in the message is the answer, because your template for /feed asks for a feed in no namespace.

Print the question. xsl:message with the count of what a select found (count(entry)) tells you in one run whether the path is right. Put one after each suspect step.

Print the node name. <xsl:value-of select="name()"/> and namespace-uri() show what the processor sees as the current node. A namespace mismatch is obvious in the output of namespace-uri().

Shrink the input. Cut the document to the smallest piece that still fails. Five lines of XML are easy to read, and a failing pattern on five lines is a failing pattern on five million.

Change the output method. Switch to method="xml" and wrap values in tags while debugging, so empty strings show as empty elements instead of nothing.

Check the processor's own diagnostics. xsltproc prints errors for a stylesheet that does not compile (an unknown element or function, an unbound prefix) and exits non-zero. An empty output with exit 0 means the stylesheet was valid and matched nothing. A missing stylesheet gave exit 4 in an earlier test (running XSLT from the command line).

Other causes of empty output

A few that are not in the five above, in my experience: xsl:output method="text" on a stylesheet that only produces elements, so tags vanish; a select that uses the wrong axis; a comparison of a string with a number that is never true; a variable that holds a node-set from the wrong context; and a stylesheet that depends on a parameter that was passed as an expression instead of a string and came out empty (passing parameters into an XSLT stylesheet). In every one, the instruments above (message, name, count) find it in a few minutes.

For the model behind the built-in rules, see XSLT basics: templates, match and apply; for the cluster, XSLT and XQuery for real transformations. Namespaces are explained in how XML namespaces work. The built-in template rules are in section 5.8 of the XSLT 1.0 specification, and xsl:message in section 15. Test the XPath you use in a select in the XPath tester.

When the stylesheet is correct and the data is the problem

The input is not what you think. The file is a different version of the feed, in another namespace, or has an extra wrapper element. Print the root's name and namespace first.

Encoding or entity problems. A document that does not parse never reaches your templates. The processor will say so, with a line number.

The data really has no matching records. An empty result for a valid filter is an answer. Check with a count before you debug the stylesheet.

The processor differs. A stylesheet written for XSLT 2.0 and run on a 1.0 engine fails or behaves differently. Check the version in the stylesheet and the processor you run.

Also inDebugging
Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.