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
| Topic | What it covers | |
|---|---|---|
| Templates and apply-templates | XSLT is not a script that loops over a document. A stylesheet is a collection of template rules, and the processor | lesson |
| Advanced matching and sorting | Several templates can match the same node. XSLT resolves the conflict by priority: a more specific pattern wins, and | lesson |
| Output methods and real use cases | Each stage keeps XML as its interface, so a change in the normalisation rules never touches the presentation | lesson |
| XSLT versions, engines and where transformations run | XSLT 1.0 is universal and XSLT 3.0 is far more capable, but the version you can use is decided by the engine you deploy | lesson |
| Match patterns versus select expressions | A pattern is matched against a node with the document as the starting point, so match="list/item" means "an item whose | lesson |
| Conditionals, loops and variables | XSLT has no assignment. Every value is bound once, and that single constraint explains most of how stylesheets are | lesson |
| Reusing stylesheets: includes, imports and named templates | Sharing templates across stylesheets relies on precedence rules that decide which duplicate wins, so know the | lesson |
| Whitespace, text and special characters | Indentation from the source document leaks into the output, and the standard fix for escaping is also the standard | lesson |
| Numbering and formatting output | Auto-numbering headings, formatting money and percentages, and the date formatting that only exists from XSLT 2.0 | lesson |
| Debugging and testing stylesheets | The two hard parts of XSLT are knowing which template ran and knowing which one should have. Both have cheap | lesson |
| Streaming very large XML documents | A DOM-like tree costs several times the size of the file because every element becomes an object. A 500 MB export can | lesson |
| Calling XSLT from Java, .NET and Python | Compile the stylesheet once, pass parameters in, and treat any stylesheet that came from outside as executable code | lesson |
| XSL-FO and PDF generation pipelines | The usual shape is: source XML plus a stylesheet produce a formatting-object document, and a formatting-object | lesson |
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.xslFull lesson: Templates and apply-templates →
Advanced matching and sorting
How the processor chooses a rule
<xsl:template match="book[price > 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.xmlFull 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=25Full 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.pdfFull lesson: XSL-FO and PDF generation pipelines →
FAQ
Is this XSLT cheat sheet free to use?
Where do the examples come from?
How do I go deeper than a cheat sheet?
Related cheat sheets
XML XPath SOAP RESTful APIs RSS & Atom
Last refreshed 2026-09-27.