facebook google plus twitter
Webucator's Free XSLT 1.0 and XPath 1.0 Tutorial

Lesson: Advanced Techniques

Welcome to our free XSLT 1.0 and XPath 1.0 tutorial. This tutorial is based on Webucator's XSLT 1.0 and XPath 1.0 Training course.

Lesson Goals

  • To work with numbered lists.
  • To output Processing Instructions.
  • To create exact copies of matched nodes with xsl:copy and xsl:copy-of.

Working with Numbered Lists

The position() function

The position() function returns the position of the context node in a selected set of nodes. The following XML and XSLT documents illustrate how position() works.

Code Sample:

AdvancedXsltTechniques/Demos/BeatlesPosition.xml
<?xml version="1.0"?>
<?xml-stylesheet href="BeatlesPosition.xsl" type="text/xsl"?>
<beatles>
	<beatle link="http://www.paulmccartney.com">
		<name>
			<firstname>Paul</firstname>
			<lastname>McCartney</lastname>
		</name>
	</beatle>
	<beatle link="http://www.johnlennon.com">
		<name>
			<firstname>John</firstname>
			<lastname>Lennon</lastname>
		</name>
	</beatle>
	<beatle link="http://www.georgeharrison.com">
		<name>
			<firstname>George</firstname>
			<lastname>Harrison</lastname>
		</name>
	</beatle>
	<beatle link="http://www.ringostarr.com">
		<name>
			<firstname>Ringo</firstname>
			<lastname>Starr</lastname>
		</name>
	</beatle>
	<beatle link="http://www.webucator.com" real="no">
		<name>
			<firstname>Nat</firstname>
			<lastname>Dunn</lastname>
		</name>
	</beatle>
</beatles>

Code Sample:

AdvancedXsltTechniques/Demos/BeatlesPosition.xsl
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" 
				xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/beatles">
	<xsl:apply-templates select="*"/>
</xsl:template>

<xsl:template match="beatle">
	<xsl:value-of select="position()"/>
	<xsl:text>. </xsl:text>
	<xsl:value-of select="name/lastname"/>
	<xsl:text>&#10;</xsl:text>
</xsl:template> 

</xsl:stylesheet>

The result of transforming AdvancedXsltTechniques/Demos/BeatlesPosition.xml against AdvancedXsltTechniques/Demos/BeatlesPosition.xsl is shown below.

1. McCartney 2. Lennon 3. Harrison 4. Starr 5. Dunn

Notice that the resulting list is numbered correctly. So far, so good. But look what happens when we add non-beatle sibling to the beatle elements.

Code Sample:

AdvancedXsltTechniques/Demos/BeatlesPosition-Oops.xml
<?xml version="1.0"?>
<?xml-stylesheet href="BeatlesPosition.xsl" type="text/xsl"?>
<beatles>
	<beatle link="http://www.paulmccartney.com">
		<name>
			<firstname>Paul</firstname>
			<lastname>McCartney</lastname>
		</name>
	</beatle>
	<beatle link="http://www.johnlennon.com">
		<name>
			<firstname>John</firstname>
			<lastname>Lennon</lastname>
		</name>
	</beatle>
	<oops />
	<beatle link="http://www.georgeharrison.com">
		<name>
			<firstname>George</firstname>
			<lastname>Harrison</lastname>
		</name>
	</beatle>
	<beatle link="http://www.ringostarr.com">
		<name>
			<firstname>Ringo</firstname>
			<lastname>Starr</lastname>
		</name>
	</beatle>
	<beatle link="http://www.webucator.com" real="no">
		<name>
			<firstname>Nat</firstname>
			<lastname>Dunn</lastname>
		</name>
	</beatle>
</beatles>

As the result below shows, when we transform the file above against the same XSLT, the numbering is incorrect.

1. McCartney 2. Lennon 4. Harrison 5. Starr 6. Dunn

This is because position() function returns the position of the current node within the selected node set and the <Oops/> element is in the selected node set, right between John Lennon and George Harrison.

This can be corrected by selecting a specific node-set with the xsl:apply-templates tag, but that can cause other problems.

xsl:number

The <xsl:number/> tag provides another mechanism for numbering elements. It counts nodes that match a specified XPath. It works as follows.

First, xsl:number takes a count attribute, which takes an XPath indicating which elements to count. When not specified, the default of count is the name of the current element. When specified, the processor first looks at the current node and then looks up the tree at the current node's ancestors until it finds an element that matches the XPath specified in count. It stops as soon as it finds a match.

Second, xsl:number takes a level attribute, which determines how many levels of the XML document should be examined for nodes that match the pattern specified in the count attribute. Possible values are single, multiple and any. We'll look at examples of each of these.

level = "single"

When level="single" (the default), a count is done of the node matched in the count attribute plus all preceding siblings that also match the XPath specified in count.

Code Sample:

AdvancedXsltTechniques/Demos/BeatlesNumbered.xsl
<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
				xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/beatles">
	<xsl:apply-templates/>
</xsl:template>

<xsl:template match="beatle">
	<xsl:number level="single" count="beatle"/>
	<xsl:text>. </xsl:text>
	<xsl:value-of select="name/lastname"/>
	<xsl:text>&#10;</xsl:text>
</xsl:template> 

</xsl:stylesheet>

Notice that AdvancedXsltTechniques/Demos/BeatlesNumbered.xsl has the <oops/> element between John Lennon and George Harrison; however, unlike with xsl:position, that element does not affect the output. This is because xsl:number is only counting the beatle elements. The result of transforming AdvancedXsltTechniques/Demos/BeatlesNumbered.xml against AdvancedXsltTechniques/Demos/BeatlesNumbered.xsl is shown below.

1. McCartney 2. Lennon 3. Harrison 4. Starr 5. Dunn

If xsl:number were changed to look like this: <xsl:number level="single" count="*"/>, then <oops/> would also be counted and the numbering would again be wrong.

level="any"

When level="any", a count is done of the node matched in the count attribute plus all preceding elements that also match the XPath specified in count.

To illustrate how any works, we'll first take a look at another example using level="single".

Code Sample:

AdvancedXsltTechniques/Demos/RockStars-Single.xml
<?xml version="1.0"?>
<?xml-stylesheet href="RockStars-Single.xsl" type="text/xsl"?>
<rockstars>
<beatles>
	<beatle link="http://www.paulmccartney.com">
		<name>
			<firstname>Paul</firstname>
			<lastname>McCartney</lastname>
		</name>
	</beatle>
	<beatle link="http://www.johnlennon.com">
		<name>
			<firstname>John</firstname>
			<lastname>Lennon</lastname>
		</name>
	</beatle>
</beatles>
<stones>
	<stone link="http://www.mickjagger.com" >
		<name>
			<firstname>Mick</firstname>
			<lastname>Jagger</lastname>
		</name>
	</stone>
	<stone link="http://www.keithrichards.com" >
		<name>
			<firstname>Keith</firstname>
			<lastname>Richards</lastname>
		</name>
	</stone>
</stones>
</rockstars>

Code Sample:

AdvancedXsltTechniques/Demos/RockStars-Single.xsl
<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
				xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/beatles">
	<xsl:apply-templates/>
</xsl:template>

<xsl:template match="beatle | stone">
	<xsl:number level="single" count="beatle | stone"/>
	<xsl:text>. </xsl:text>
	<xsl:value-of select="name/lastname"/>
	<xsl:text>&#10;</xsl:text>
</xsl:template> 

</xsl:stylesheet>

The result of transforming AdvancedXsltTechniques/Demos/RockStars-Single.xml against AdvancedXsltTechniques/Demos/RockStars-Single.xsl is shown below.

1. McCartney 2. Lennon 1. Jagger 2. Richards

Notice that the counting is done at a single level. In other words, only siblings are counted.

Now let's see what happens when level="single" is changed to level="any" as shown in AdvancedXsltTechniques/Demos/RockStars-Any.xsl.

Code Sample:

AdvancedXsltTechniques/Demos/RockStars-Any.xsl
<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
				xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/beatles">
	<xsl:apply-templates/>
</xsl:template>

<xsl:template match="beatle | stone">
	<xsl:number level="any" count="beatle | stone"/>
	<xsl:text>. </xsl:text>
	<xsl:value-of select="name/lastname"/>
	<xsl:text>&#10;</xsl:text>
</xsl:template> 

</xsl:stylesheet>

The result of transforming AdvancedXsltTechniques/Demos/RockStars-Any.xml against AdvancedXsltTechniques/Demos/RockStars-Any.xsl is shown below.

1. McCartney 2. Lennon 3. Jagger 4. Richards

As you can see, the counting is now done across levels.

level="multiple"

When level="multiple", multiple counts are done at different levels. To determine the results, the processor goes through the following steps.

  1. It creates a list of all the ancestors of the current node and the current node itself.
  2. It throws out any nodes in the list that do not match the XPath specified in the count attribute.
  3. It maps each node in the list to a number, which is calculated by counting the node's preceding siblings that match the XPath specified in the count attribute and adding one (for the node itself).

Code Sample:

AdvancedXsltTechniques/Demos/Outline.xml
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="Outline.xsl"?>
<outline>
	<topic>XML Basics<topic>What is XML?</topic>
		<topic>XML Benefits<topic>XML Holds Data, Nothing More</topic>
			<topic>XML Separates Structure from Formatting</topic>
			<topic>XML Promotes Data Sharing</topic>
			<topic>XML is Human-Readable</topic>
			<topic>XML is Free</topic>
		</topic>
		<topic>XML Documents<topic>The Prolog</topic>
			<topic>Elements</topic>
			<topic>Attributes</topic>
			<topic>CDATA</topic>
			<topic>XML Syntax Rules</topic>
			<topic>Special Characters</topic>
		</topic>
		<topic>Creating a Simple XML File</topic>
	</topic>
</outline>

Code Sample:

AdvancedXsltTechniques/Demos/Outline.xsl
<?xml version='1.0'?>
<xsl:stylesheet version="1.0"
				xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>

<xsl:template match="/">
	<xsl:apply-templates select="outline//*"/>
</xsl:template>

<xsl:template match="outline//*">
	<xsl:for-each select="ancestor::*">
		<xsl:text>&#09;</xsl:text>
	</xsl:for-each>
	<xsl:number level="multiple" count="*"/>
	<xsl:text>. </xsl:text>
	<xsl:value-of select="text()"/>
	<xsl:text>&#10;</xsl:text>
</xsl:template>

</xsl:stylesheet>

The result of transforming AdvancedXsltTechniques/Demos/Outline.xml against AdvancedXsltTechniques/Demos/Outline.xml is shown below.

1.1. XML Basics 1.1.1. What is XML? 1.1.2. XML Benefits 1.1.2.1. XML Holds Data, Nothing More 1.1.2.2. XML Separates Structure from Formatting 1.1.2.3. XML Promotes Data Sharing 1.1.2.4. XML is Human-Readable 1.1.2.5. XML is Free 1.1.3. XML Documents 1.1.3.1. The Prolog 1.1.3.2. Elements 1.1.3.3. Attributes 1.1.3.4. CDATA 1.1.3.5. XML Syntax Rules 1.1.3.6. Special Characters 1.1.4. Creating a Simple XML File

The tabbing is generated with the <xsl:for-each>, which renders one tab for each ancestor of the current element. As the processor looks deeper into the document, the elements have more ancestors and, therefore, are preceded by more tabs.

The first number "1" of each line in the output above represents the Outline element, which is the document element. Because a well-formed XML document can only have one document element, every list generated with xsl:number in this way will start every list item with the number "1". This can be fixed with the from attribute.

The from Attribute

The from attribute takes a regular expression specifying where counting should start.

Code Sample:

AdvancedXsltTechniques/Demos/OutlineFrom.xsl
<?xml version='1.0'?>
<xsl:stylesheet version="1.0"
				 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>

<xsl:template match="/">
	<xsl:apply-templates select="outline//*"/>
</xsl:template>

<xsl:template match="outline//*">
	<xsl:for-each select="ancestor::*">
		<xsl:text>&#09;</xsl:text>
	</xsl:for-each>
	<xsl:number level="multiple" count="*" from="outline"/>
	<xsl:text>. </xsl:text>
	<xsl:value-of select="text()"/>
	<xsl:text>&#10;</xsl:text>
</xsl:template>

</xsl:stylesheet>

The result of transforming AdvancedXsltTechniques/Demos/OutlineFrom.xml against AdvancedXsltTechniques/Demos/OutlineFrom.xsl is shown below.

1. XML Basics 1.1. What is XML? 1.2. XML Benefits 1.2.1. XML Holds Data, Nothing More 1.2.2. XML Separates Structure from Formatting 1.2.3. XML Promotes Data Sharing 1.2.4. XML is Human-Readable 1.2.5. XML is Free 1.3. XML Documents 1.3.1. The Prolog 1.3.2. Elements 1.3.3. Attributes 1.3.4. CDATA 1.3.5. XML Syntax Rules 1.3.6. Special Characters 1.4. Creating a Simple XML File

Notice that the initial "1" is now stripped. That's because the processor is now counting from the Outline element rather than from the root node.

Formatting Numbers

In the last example, you may have noticed that the levels are all represented with Arabic numbers. The formatting can be changed with the format attribute, which takes a format token, which is a delimited list of characters. The same delimiter used in the format token is used in the output.

Code Sample:

AdvancedXsltTechniques/Demos/OutlineFormatted.xsl
<?xml version='1.0'?>
<xsl:stylesheet version="1.0" 
				xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>

<xsl:template match="/">
	<xsl:apply-templates select="outline//*"/>
</xsl:template>

<xsl:template match="outline//*">
	<xsl:for-each select="ancestor::*">
		<xsl:text>&#09;</xsl:text>
	</xsl:for-each>
	<xsl:number level="multiple" count="*" format="I.A.i.a. "/>
	<xsl:value-of select="text()"/>
	<xsl:text>&#10;</xsl:text>
</xsl:template>

</xsl:stylesheet>

In earlier examples, we placed a period and a space after the number by putting <xsl:text>. </xsl:text> after xsl:number. In this example, we accomplished the same thing by placing a period and a space at the end of the format token.

Outputting Processing Instructions

It is sometimes necessary to output processing instructions with XSLT; for example, if you need to transform one XML document to another XML structure, which will then be transformed to HTML via XSLT. This is illustrated in the diagram below.

Processing instructions are output with the xsl:processing-instruction element as shown below.

<xsl:processing-instruction name="Name_Of_Instruction"> Name-Value Pairs of Instruction </xsl:processing-instruction>

Let's look at an example.

Code Sample:

AdvancedXsltTechniques/Demos/Beatles.xml
<?xml version="1.0"?>
<?xml-stylesheet href="Beatles2Artists.xsl" type="text/xsl"?>
<beatles>
	<beatle link="http://www.paulmccartney.com">
		<name>
			<firstname>Paul</firstname>
			<lastname>McCartney</lastname>
		</name>
	</beatle>
	<beatle link="http://www.johnlennon.com">
		<name>
			<firstname>John</firstname>
			<lastname>Lennon</lastname>
		</name>
	</beatle>
	<beatle link="http://www.georgeharrison.com">
		<name>
			<firstname>George</firstname>
			<lastname>Harrison</lastname>
		</name>
	</beatle>
	<beatle link="http://www.ringostarr.com">
		<name>
			<firstname>Ringo</firstname>
			<lastname>Starr</lastname>
		</name>
	</beatle>
	<beatle link="http://www.webucator.com" real="no">
		<name>
			<firstname>Nat</firstname>
			<lastname>Dunn</lastname>
		</name>
	</beatle>
</beatles>

Code Sample:

AdvancedXsltTechniques/Demos/Beatles2Artists.xsl
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" 
				xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
	<xsl:output method="xml" indent="yes"/>
	<xsl:template match="/beatles">
		<xsl:text>&#10;</xsl:text>
		<xsl:processing-instruction name="xml-stylesheet">
			<xsl:text>href="Artists.xsl" type="text/xsl"</xsl:text>
		</xsl:processing-instruction>
		<xsl:text>&#10;</xsl:text>
		<Artists>
			<xsl:apply-templates/>
		</Artists>
	</xsl:template>
	<xsl:template match="beatle">
		<Artist>
			<FirstName>
				<xsl:value-of select="name/firstname"/>
			</FirstName>
			<LastName>
				<xsl:value-of select="name/lastname"/>
			</LastName>
		</Artist>
	</xsl:template>
</xsl:stylesheet>

The result of transforming AdvancedXsltTechniques/Demos/Beatles.xml against AdvancedXsltTechniques/Demos/Beatles2Artists.xsl is shown below.

Code Sample:

AdvancedXsltTechniques/Demos/BeatlesOutput.xml
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="Artists.xsl" type="text/xsl"?>
<Artists>
	<Artist>
		<FirstName>Paul</FirstName>
		<LastName>McCartney</LastName>
	</Artist>
	<Artist>
		<FirstName>John</FirstName>
		<LastName>Lennon</LastName>
	</Artist>
	<Artist>
		<FirstName>George</FirstName>
		<LastName>Harrison</LastName>
	</Artist>
	<Artist>
		<FirstName>Ringo</FirstName>
		<LastName>Starr</LastName>
	</Artist>
	<Artist>
		<FirstName>Nat</FirstName>
		<LastName>Dunn</LastName>
	</Artist>
</Artists>

As you can see from the output above, the generated XML document has a processing instruction which indicates that it should be transformed against Artists.xsl (shown below).

Code Sample:

AdvancedXsltTechniques/Demos/Artists.xsl
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" 
		xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
	<xsl:output method="xml" indent="yes"/>
	<xsl:template match="/">
		<html>
			<head>
				<title>Artists</title>
			</head>
			<body>
				<h1>Artists</h1>
				<table>
						<tr>
							<th>First Name</th>
							<th>Last Name</th>
						</tr>
						<xsl:apply-templates/>
				</table>
			</body>
		</html>
	</xsl:template>
	<xsl:template match="Artist">
		<tr>
			<td><xsl:value-of select="FirstName"/></td>
			<td><xsl:value-of select="LastName"/></td>
		</tr>
	</xsl:template>
</xsl:stylesheet>

The result of transforming AdvancedXsltTechniques/Demos/BeatlesOutput.xml against AdvancedXsltTechniques/Demos/Artists.xsl is shown below.

Code Sample:

AdvancedXsltTechniques/Demos/Beatles.html
<?xml version="1.0" encoding="UTF-8"?>
<html>
	<head>
		<title>Artists</title>
	</head>
	<body>
		<h1>Artists</h1>
		<table>
			<tr>
				<th>First Name</th>
				<th>Last Name</th>
			</tr>
			<tr>
				<td>Paul</td>
				<td>McCartney</td>
			</tr>
			<tr>
				<td>John</td>
				<td>Lennon</td>
			</tr>
			<tr>
				<td>George</td>
				<td>Harrison</td>
			</tr>
			<tr>
				<td>Ringo</td>
				<td>Starr</td>
			</tr>
			<tr>
				<td>Nat</td>
				<td>Dunn</td>
			</tr>
		</table>
	</body>
</html>

Copying Nodes

Sometimes you find that there is a chunk of code from the source XML document that you want to leave exactly as is in the output. There are two tags that make this very easy to do in XSLT: xsl:copy and xsl:copy-of.

xsl:copy

The xsl:copy element is used to copy the element name from the source document. The following example illustrates how it is used.

Code Sample:

AdvancedXsltTechniques/Demos/BeatlesCopy.xsl
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
				xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
	<xsl:output method="xml" indent="yes"/>
	<xsl:template match="/beatles">
		<xsl:copy>
			<xsl:apply-templates select="//name"/>
		</xsl:copy>
	</xsl:template>
	<xsl:template match="name">
		<xsl:copy>
			<xsl:value-of select="lastname"/>, <xsl:value-of select="firstname"/>
		</xsl:copy>
	</xsl:template>
</xsl:stylesheet>

The first xsl:copy outputs a beatles node and the second xsl:copy outputs a name node for each name node in the source document. When AdvancedXsltTechniques/Demos/BeatlesCopy.xml is transformed against AdvancedXsltTechniques/Demos/BeatlesCopy.xsl, the output looks like this.

Code Sample:

AdvancedXsltTechniques/Demos/BeatlesCopyOutput.xml
<?xml version="1.0" encoding="UTF-8"?>
<beatles>
	<name>McCartney, Paul</name>
	<name>Lennon, John</name>
	<name>Harrison, George</name>
	<name>Starr, Ringo</name>
	<name>Dunn, Nat</name>
</beatles>

xsl:copy-of

The xsl:copy-of element is used to make a deep copy from the source document, including all attributes and descendant elements. The following example illustrates how it is used.

Code Sample:

AdvancedXsltTechniques/Demos/BeatlesCopyOf.xsl
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" 
				xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
	<xsl:output method="xml" indent="yes"/>
	<xsl:template match="/beatles">
		<xsl:copy>
			<xsl:apply-templates select="//name"/>
		</xsl:copy>
	</xsl:template>
	<xsl:template match="name">
		<xsl:copy-of select="."/>
	</xsl:template>
</xsl:stylesheet>

The xsl:copy-of outputs a deep copy of a name node for each name node in the source document. When AdvancedXsltTechniques/Demos/BeatlesCopyOf.xml is transformed against AdvancedXsltTechniques/Demos/BeatlesCopyOf.xsl, the output looks like this.

Code Sample:

AdvancedXsltTechniques/Demos/BeatleCopyOfOutput.xml
<?xml version="1.0" encoding="UTF-8"?>
<beatles>
	<name>
		<firstname>Paul</firstname>
		<lastname>McCartney</lastname>
	</name>
	<name>
		<firstname>John</firstname>
		<lastname>Lennon</lastname>
	</name>
	<name>
		<firstname>George</firstname>
		<lastname>Harrison</lastname>
	</name>
	<name>
		<firstname>Ringo</firstname>
		<lastname>Starr</lastname>
	</name>
	<name>
		<firstname>Nat</firstname>
		<lastname>Dunn</lastname>
	</name>
</beatles>