XML Tutorial · XPath, XSLT and XQuery · Lesson 15 of 28
XSLT: Advanced Techniques
The identity transform, modes, parameters, keys and grouping, named templates, and what XSLT 2.0 and 3.0 add.
The identity transform
Copy everything unchanged, then override only what should change. It's the starting point of most real stylesheets:
from lxml import etree
shop = etree.fromstring(b"""<shop>
<product sku="TEA-01" category="drinks"><name>Green tea</name><price>3.50</price><stock>40</stock></product>
<product sku="COF-02" category="drinks"><name>Coffee beans</name><price>9.90</price><stock>0</stock></product>
<product sku="MUG-07" category="kitchen"><name>Camel mug</name><price>12.00</price><stock>15</stock></product>
<product sku="PAN-03" category="kitchen"><name>Frying pan</name><price>29.00</price><stock>4</stock></product>
</shop>""")
transform = etree.XSLT(etree.fromstring(b"""
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"/>
<xsl:strip-space elements="*"/>
<!-- identity: copy every node and attribute -->
<xsl:template match="@* | node()">
<xsl:copy><xsl:apply-templates select="@* | node()"/></xsl:copy>
</xsl:template>
<!-- overrides -->
<xsl:template match="stock"/> <!-- drop stock -->
<xsl:template match="@category"> <!-- rename an attribute -->
<xsl:attribute name="type"><xsl:value-of select="."/></xsl:attribute>
</xsl:template>
<xsl:template match="product[stock = 0]"/> <!-- drop sold-out products -->
</xsl:stylesheet>"""))
print(etree.tostring(transform(shop)).decode())
Output
<shop><product sku="TEA-01" type="drinks"><name>Green tea</name><price>3.50</price></product><product sku="MUG-07" type="kitchen"><name>Camel mug</name><price>12.00</price></product><product sku="PAN-03" type="kitchen"><name>Frying pan</name><price>29.00</price></product></shop>
Parameters, keys and grouping
xsl:paramtakes values from the caller (xslt(doc, currency="'EUR'")in lxml; note the quotes, because the value is an XPath expression).xsl:keybuilds an index for fast lookups:key('by-category', 'drinks').- XSLT 1.0 has no grouping instruction; the classic Muenchian method uses a key and picks the first node of
each group. XSLT 2.0 replaced it with
xsl:for-each-group.
from lxml import etree
shop = etree.fromstring(b"""<shop>
<product sku="TEA-01" category="drinks"><name>Green tea</name><price>3.50</price><stock>40</stock></product>
<product sku="COF-02" category="drinks"><name>Coffee beans</name><price>9.90</price><stock>0</stock></product>
<product sku="MUG-07" category="kitchen"><name>Camel mug</name><price>12.00</price><stock>15</stock></product>
<product sku="PAN-03" category="kitchen"><name>Frying pan</name><price>29.00</price><stock>4</stock></product>
</shop>""")
report = etree.XSLT(etree.fromstring(b"""
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:param name="currency" select="'USD'"/>
<xsl:key name="by-category" match="product" use="@category"/>
<xsl:template match="/shop">
<!-- Muenchian grouping: the first product of each category -->
<xsl:for-each select="product[generate-id() = generate-id(key('by-category', @category)[1])]">
<xsl:variable name="group" select="key('by-category', @category)"/>
<xsl:value-of select="concat(@category, ': ', count($group), ' products, ',
format-number(sum($group/price), '0.00'), ' ', $currency, ' ')"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>"""))
print(str(report(shop)), end="")
print(str(report(shop, currency="'EUR'")), end="")
Output
drinks: 2 products, 13.40 USD kitchen: 2 products, 41.00 USD drinks: 2 products, 13.40 EUR kitchen: 2 products, 41.00 EUR
Modes and named templates
mode="toc" lets the same nodes be processed differently in different passes (a table of contents, then the
body). xsl:call-template name="…" calls a template like a function.
XSLT 2.0 and 3.0
| Feature | 1.0 | 2.0 / 3.0 |
|---|---|---|
| Grouping | Muenchian keys | xsl:for-each-group |
| Functions | Named templates | xsl:function, higher-order functions (3.0) |
| Several outputs | Extensions | xsl:result-document |
| Regular expressions | No | xsl:analyze-string, matches(), replace() |
| JSON | No | json-to-xml(), xml-to-json(), maps and arrays (3.0) |
| Huge files | Whole tree in memory | Streaming (3.0) |
Saxon implements XSLT 3.0 for Java, .NET, C, Python (saxonche) and JavaScript (SaxonJS).
Test yourself
What does the identity transform do on its own?
Copies the input document unchangedDeletes every elementValidates the inputConverts XML to JSON
Show answer
Answer: Copies the input document unchanged
It matches every node and copies it, so you only write templates for the parts that change.