XSLT cheat sheet

A scannable XSLT reference: 25 short snippets across 13 topics, each linking back to the lesson it came from.

At a glance

TopicWhat it covers
Templates and apply-templatesXSLT is not a script that loops over a document. A stylesheet is a collection of template rules, and the processorlesson
Advanced matching and sortingSeveral templates can match the same node. XSLT resolves the conflict by priority: a more specific pattern wins, andlesson
Output methods and real use casesEach stage keeps XML as its interface, so a change in the normalisation rules never touches the presentationlesson
XSLT versions, engines and where transformations runXSLT 1.0 is universal and XSLT 3.0 is far more capable, but the version you can use is decided by the engine you deploylesson
Match patterns versus select expressionsA pattern is matched against a node with the document as the starting point, so match="list/item" means "an item whoselesson
Conditionals, loops and variablesXSLT has no assignment. Every value is bound once, and that single constraint explains most of how stylesheets arelesson
Reusing stylesheets: includes, imports and named templatesSharing templates across stylesheets relies on precedence rules that decide which duplicate wins, so know thelesson
Whitespace, text and special charactersIndentation from the source document leaks into the output, and the standard fix for escaping is also the standardlesson
Numbering and formatting outputAuto-numbering headings, formatting money and percentages, and the date formatting that only exists from XSLT 2.0lesson
Debugging and testing stylesheetsThe two hard parts of XSLT are knowing which template ran and knowing which one should have. Both have cheaplesson
Streaming very large XML documentsA DOM-like tree costs several times the size of the file because every element becomes an object. A 500 MB export canlesson
Calling XSLT from Java, .NET and PythonCompile the stylesheet once, pass parameters in, and treat any stylesheet that came from outside as executable codelesson
XSL-FO and PDF generation pipelinesThe usual shape is: source XML plus a stylesheet produce a formatting-object document, and a formatting-objectlesson

Quick snippets

Templates and apply-templates

Running a transformation

# libxslt (XSLT 1.0)
xsltproc --output out.html catalog.xsl catalog.xml

# pass a parameter into the stylesheet
xsltproc --stringparam title "Q3 Catalog" catalog.xsl catalog.xml

# Saxon-HE runs XSLT 3.0 from the command line
java -jar saxon-he.jar -s:catalog.xml -xsl:catalog.xsl -o:out.html

# sanity-check both inputs first
xmllint --noout catalog.xml && xmllint --noout catalog.xsl

Full lesson: Templates and apply-templates →

Advanced matching and sorting

How the processor chooses a rule

<xsl:template match="book[price &gt; 50]" priority="3">
  <tr class="expensive"><xsl:apply-templates/></tr>
</xsl:template>

<xsl:template match="book">
  <tr><xsl:apply-templates/></tr>
</xsl:template>

Sorting with more than one key

<xsl:apply-templates select="book">
  <xsl:sort select="price" data-type="number" order="ascending"/>
  <xsl:sort select="title" lang="en" case-order="upper-first"/>
</xsl:apply-templates>

<!-- inside for-each, direct children of the current node are pre-sorted too -->
<xsl:for-each select="catalog/book">
  <xsl:sort select="@id"/>
  <xsl:value-of select="title"/>
</xsl:for-each>

Keys and grouping

<!-- XSLT 2.0 and later replace all of the above with two lines -->
<xsl:for-each-group select="book" group-by="genre">
  <h2><xsl:value-of select="current-grouping-key()"/></h2>
  <ul>
    <xsl:for-each select="current-group()">
      <li><xsl:value-of select="title"/></li>
    </xsl:for-each>
  </ul>
</xsl:for-each-group>

Full lesson: Advanced matching and sorting →

Output methods and real use cases

Controlling the output

<xsl:output method="xml"
            version="1.0"
            encoding="UTF-8"
            indent="yes"
            omit-xml-declaration="no"/>

<xsl:output method="text"/>
<xsl:strip-space elements="*"/>   <!-- ignore source indentation -->
<xsl:preserve-space elements="pre code"/>

Chaining transformations

# stage 1: normalise, stage 2: render - a common publishing pipeline
xsltproc normalise.xsl raw.xml | xsltproc render.xsl - > page.html

# render to print via XSL-FO, then to PDF
xsltproc fo.xsl catalog.xml > catalog.fo
fop -fo catalog.fo -pdf catalog.pdf

# generate a sitemap from a content index
xsltproc sitemap.xsl index.xml > sitemap.xml && xmllint --noout sitemap.xml

Full lesson: Output methods and real use cases →

XSLT versions, engines and where transformations run

The three versions in practice

# xsltproc (libxslt) -- XSLT 1.0, installed on most Linux systems
xsltproc --output out.html transform.xsl input.xml

# Saxon-HE on the command line -- XSLT 3.0, free edition
java -cp saxon-he-12.5.jar net.sf.saxon.Transform -s:input.xml -xsl:transform.xsl -o:out.html

# Passing a parameter from the shell
xsltproc --stringparam locale en-GB transform.xsl input.xml

Where the transform should run

from lxml import etree

# Server-side transform, 1.0 via libxslt, compiled once and reused
transform = etree.XSLT(etree.parse("transform.xsl"))
result = transform(etree.parse("input.xml"), locale="en-GB")
print(str(result))

Full lesson: XSLT versions, engines and where transformations run →

Match patterns versus select expressions

Two dialects, one syntax

<xsl:template match="item">            <!-- fires for any item element -->
<xsl:template match="list/item">       <!-- only items whose parent is list -->
<xsl:template match="item[@status='new']">
<xsl:template match="//item">          <!-- legal in 1.0 only as id()/key() sugar;
                                            prefer match="item", which also matches at any depth -->

<xsl:apply-templates select="item[price > 20]"/>   <!-- select is a real expression -->
<xsl:apply-templates select="item" mode="summary"/>

The context item, position and last()

<xsl:template match="li">
  <!-- position() is the position among the nodes being processed by apply-templates -->
  <xsl:if test="position() = 1">
    <p>First item: <xsl:value-of select="."/></p>
  </xsl:if>
  <xsl:if test="position() = last()">
    <p>Last item: <xsl:value-of select="."/></p>
  </xsl:if>
</xsl:template>

The context item, position and last()

<!-- current() keeps the outer node available while a predicate moves the focus -->
<xsl:template match="product">
  <xsl:value-of select="//price[@sku = current()/@sku]"/>
</xsl:template>

Full lesson: Match patterns versus select expressions →

Conditionals, loops and variables

Variables are bindings, not slots

# Passing parameters in from the environment
xsltproc --stringparam locale en-GB --param limit 25 transform.xsl input.xml
java -cp saxon-he.jar net.sf.saxon.Transform -s:input.xml -xsl:t.xsl \
     -o:out.html locale=en-GB limit=25

Full lesson: Conditionals, loops and variables →

Reusing stylesheets: includes, imports and named templates

include versus import

<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <xsl:import href="base.xsl"/>
  <xsl:include href="utilities.xsl"/>

  <!-- Beats the same match rule in base.xsl without any priority attribute -->
  <xsl:template match="item">
    <div class="item"><xsl:apply-templates select="*"/></div>
  </xsl:template>
</xsl:stylesheet>

Named templates, modes and attribute sets

<!-- Modes let one element render two different ways -->
<xsl:template match="item" mode="nav"><a href="{@url}"><xsl:value-of select="title"/></a></xsl:template>
<xsl:template match="item" mode="body"><h2><xsl:value-of select="title"/></h2></xsl:template>

<!-- Attribute sets collect repeated attribute groups -->
<xsl:attribute-set name="table">
  <xsl:attribute name="border">0</xsl:attribute>
  <xsl:attribute name="cellpadding">4</xsl:attribute>
</xsl:attribute-set>
<xsl:template match="table"><table xsl:use-attribute-sets="table">...</table></xsl:template>

Full lesson: Reusing stylesheets: includes, imports and named templates →

Whitespace, text and special characters

Strip, preserve and control

<xsl:strip-space elements="*"/>                 <!-- remove all whitespace-only text nodes -->
<xsl:preserve-space elements="pre code"/>      <!-- never strip inside these -->

<!-- Emit a literal space that would otherwise be stripped from the stylesheet -->
<xsl:text> </xsl:text>

<!-- Control indentation of the result -->
<xsl:output method="xml" indent="yes"/>
<xsl:output method="html" indent="no"/>

Full lesson: Whitespace, text and special characters →

Numbering and formatting output

xsl:number

<xsl:template match="h2">
  <h2>
    <xsl:number level="multiple" count="h1|h2" format="1.1." />
    <xsl:value-of select="."/>
  </h2>
</xsl:template>

<!-- Let the source carry the number, formatting it as roman numerals -->
<xsl:number value="@part" format="I" />

<!-- Numbering list items per section, restarting inside each section -->
<xsl:number level="any" count="li" from="section" format="1" />

format-number and dates

<xsl:decimal-format name="eu" decimal-separator="," grouping-separator="."/>

<xsl:value-of select="format-number(1234.5, '#,##0.00')"/>                    <!-- 1,234.50 -->
<xsl:value-of select="format-number(0.075, '0.0%')"/>                        <!-- 7.5% -->
<xsl:value-of select="format-number(42, '000')"/>                            <!-- 042 -->
<xsl:value-of select="format-number(1234.5, '#.##0,00', 'eu')"/>             <!-- 1.234,50 -->
<xsl:value-of select="format-number(-12.3, '0.00;(0.00)')"/>                 <!-- (12.30) -->

<!-- XSLT 2.0 date formatting; in 1.0 use substring() or a host extension -->
<xsl:value-of select="format-date(current-date(), '[D01] [MNn] [Y0001]')"/>
<xsl:value-of select="format-dateTime(@updated, '[Y0001]-[M01]-[D01] [H01]:[m01]')"/>

Full lesson: Numbering and formatting output →

Debugging and testing stylesheets

Tracing without a debugger

<xsl:template match="item">
  <xsl:message>item id=<xsl:value-of select="@id"/> price=<xsl:value-of select="price"/></xsl:message>

  <!-- Fail loudly when the input breaks an assumption -->
  <xsl:if test="not(@id)">
    <xsl:message terminate="yes">item without an id: <xsl:value-of select="."/></xsl:message>
  </xsl:if>

  <li><xsl:value-of select="title"/></li>
</xsl:template>

Tracing without a debugger

<!-- Identity template: the starting point for any debugging fixture.
     Anything that survives to the output was not handled by a real template. -->
<xsl:template match="@*|node()">
  <xsl:copy>
    <xsl:apply-templates select="@*|node()"/>
  </xsl:copy>
</xsl:template>

Fixture-based testing in CI

#!/bin/sh
set -e
# One fixture per behaviour; the expected output is checked in next to it
for input in tests/fixtures/*.xml; do
  name=$(basename "$input" .xml)
  xsltproc transform.xsl "$input" > "build/$name.html"
  diff -u "tests/expected/$name.html" "build/$name.html"
done
# Then validate the output so a well-formedness bug fails the build
xmllint --noout --html build/*.html
echo "stylesheets ok"

Full lesson: Debugging and testing stylesheets →

Streaming very large XML documents

Why memory is the constraint

# Saxon streaming mode, with a heap big enough for your buffers but not the document
java -Xmx512m -cp saxon-he.jar net.sf.saxon.Transform \
     -s:huge.xml -xsl:stream.xsl -o:out.html

# Measure peak memory rather than guessing
/usr/bin/time -v java -Xmx512m -cp saxon-he.jar net.sf.saxon.Transform \
     -s:huge.xml -xsl:stream.xsl -o:out.html 2>&1 | grep Maximum

Writing a streamable stylesheet

<!-- xsl:iterate keeps state forward, which a stream cannot do with recursion -->
<xsl:iterate select="record">
  <xsl:param name="total" select="0"/>
  <xsl:on-completion><p>Total: <xsl:value-of select="$total"/></p></xsl:on-completion>
  <xsl:next-iteration>
    <xsl:with-param name="total" select="$total + number(price)"/>
  </xsl:next-iteration>
</xsl:iterate>

Full lesson: Streaming very large XML documents →

Calling XSLT from Java, .NET and Python

The three APIs

from lxml import etree

# Compile once, then call it many times
transform = etree.XSLT(etree.parse("transform.xsl"))

for path in ["a.xml", "b.xml"]:
    result = transform(etree.parse(path), locale="en-GB")
    if not result:                       # an empty tree means the transform failed
        raise RuntimeError(str(transform.error_log))
    open(path.replace(".xml", ".html"), "wb").write(bytes(result))

Hardening an untrusted stylesheet

from lxml import etree

# A parser that refuses to fetch or expand anything, for untrusted XML
parser = etree.XMLParser(resolve_entities=False, no_network=True,
                         load_dtd=False, huge_tree=False)

doc = etree.parse("uploaded.xml", parser)
transform = etree.XSLT(etree.parse("trusted.xsl"))
result = transform(doc)

Full lesson: Calling XSLT from Java, .NET and Python →

XSL-FO and PDF generation pipelines

The two-stage pipeline

# Stage 1: XML to XSL-FO
xsltproc --output book.fo to-fo.xsl book.xml

# Stage 2: XSL-FO to PDF with Apache FOP
fop -fo book.fo -pdf book.pdf

# One command, two stages, for a build script
fop -xml book.xml -xsl to-fo.xsl -pdf book.pdf

Full lesson: XSL-FO and PDF generation pipelines →

FAQ

Is this XSLT cheat sheet free to use?
Yes. No sign-up and no tracking: the page is static, every example is on the page itself, and you can print it or save it as a one-page reference.
Where do the examples come from?
Every snippet is taken from the 13 lessons of the XSLT course on this site, and each section links back to the lesson it was pulled from.
How do I go deeper than a cheat sheet?
Open the full XSLT course — it carries the worked explanations, the edge cases and the exercises behind every line here.

XML XPath SOAP RESTful APIs RSS & Atom

Last refreshed 2026-09-27.