Website development and design blog, tutorials and inspiration

XML Transformation and Stylesheets (XSL)

Convert XML codes into easy to read HTML with these tips

By , 26th October 2011 in HTML/CSS

XML is a machine-readable format similar to HTML. Stylesheets can be used to transform this data into human readable or pretty print versions for use on the web, documents or print.

XML Stylesheets transform the XML data using transformations. This guide will use a simple project XML document to illustrate the power of stylesheets and give you the knowledge to transform your own XML documentation into a printable version. XML Stylesheets are called eXtensible Stylesheet Language and have an XSL extension.

This guide will only cover the basics of XSL Stylesheets, a later tutorial will follow with more advanced techniques. Style sheets can be used to convert XML into ANY text format including EDI messages, Text files, comma separated CSV and even other XML formats.

Firstly you need to create a text file with a .xsl extension. This file is going to be our style sheet.

Next you need to link the XSL to the XML, so open the XML file and after the XML line similar to this:

  1. <?xml version="1.0" encoding="ISO-8859-1"?>

Add the line:

  1. <?xml-stylesheet type="text/xsl" href="stylesheet.xsl"?>

You need to change stylesheet.xsl to whatever your new stylesheet file is called. This will link the XML document with the XSL stylesheet and is picked up by web browsers. Save and close the XMLfile and open up the XSL stylesheet.

You need to copy and paste the following code as a template. This template will be used in any XSL stylesheet regardless of the source XML.

  1. <?xml version="1.0" encoding="ISO-8859-1"?>
  2. <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  3. <xsl:template match="/">
  4.  
  5. </xsl:template>
  6. </xsl:stylesheet>

The XSL:template tag is the root element of the document and specifies that the document is a stylesheet. xsl:stylesheet can also be used as they are the same, however, the W3C standard uses the template so that is what we will use here.

Lets first start off with a very basic XML document:

  1. <?xml version="1.0" encoding="ISO-8859-1"?>
  2. <?xml-stylesheet type="text/xsl" href="sample.xsl"?>
  3. <sampleXml>
  4. <sampleTitle>Hello World</sampleTitle>
  5. </sampleXml>

XSL Stylesheets essentially work by substituting values, so in between the xsl:templatematch we can put any text we want, for example, HTML tags or EDI references. When we want to include data we can use one of the many XSL tags to retrieve the data from the XML. In this example, you can see that I have used an H1 HTML tag to surround a xsl:value-of tag. This value-of tag will get replaced during the transformation into the value extracted from the XML.

  1. <?xml version="1.0" encoding="ISO-8859-1"?>
  2. <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  3. <xsl:template match="/">
  4. <h1><xsl:value-of select="/sampleXml/sampleTitle"/></h1>
  5. </xsl:template>
  6. </xsl:stylesheet>

You can save both files and open them in a browser window. Internet Explorer may prompt you to accept a security risk regarding an ActiveX component. You should now see Hello World in large text.

A Real World Example

We are going to perform a transformation on a sample XML invoice between two fictitious companies. You can download the XML document and the completed XSL to use on your local machine at the end of the page. We will see how to absolutely reference elements, loop through repeating elements, maintain a counter and use recursion to generate a grand total as well as generating a printable HTML version of the Invoice.

XML is not very forgiving with errors. When an error occurs during XSL processing the messages are pretty much useless for diagnostic so it is important that you test as you go along to help locate errors as the arise.

The first thing that we need to do is to design an invoice layout in HTML that we can use to insert the XML data into. I have created a table within Frontpage that I can use as a template for the XML data.

You can paste the HTML table, or any other text, within the xsl:template tags and it will be rendered in the browser. We can now go through the XML and HTML and build up the data stage by stage.

The first thing we will see is how to absolutely reference an element and get its data. For most data retrieval we use the value-of tag and use an attribute select to specify the path. The first item to pull off is the company name of the sender, which is located in the Invoice » InvoiceHeader » Sender section. This is represented using XPath as /Invoice/InvoiceHeader/Sender and the tag we are after is CompanyName.

XSL:Value-Of

The value-of tag outputs the data specified in the select attribute, an XPath query, function or literal.

  1. <h1><xsl:value-of select="/Invoice/InvoiceHeader/Sender/CompanyName"/></h1>

If you load up the XML document in Internet Explorer you should see the company name of myInternetStore Ltd at the top in big bold letters. The rest of the header details can be filled in using the same techniques. I have added some presentational tags and CSS to the document to give it a little colour.

You can use the same line, changing the XPath location, to add all the header details. The next thing we need to do are the order lines. Because there are many lines we will use a loop to go through them all. XSL supports the foreach loop so we can use that to go through and output all of the item lines.

XSL:For-Each

The XSL Foreach loop will loop through all the elements specified. In this example, it will loop through all the OrderLine items. We use the value-of tag, this time with a relative XPath to the element we want to output. You can use ../ at the start of the XPath to go up a level, i.e. OrderDetails.

  1. <xsl:for-each select="/Invoice/OrderDetails/OrderLine">
  2. <tr>
  3. <td></td>
  4. <td><xsl:value-of select="ProductCode"/></td>
  5. <td><xsl:value-of select="ProductDescription"/></td>
  6. <td><xsl:value-of select="OrderQuantity"/></td>
  7. <td><xsl:value-of select="Price"/></td>
  8. <td><xsl:value-of select="VATAmount"/></td>
  9. <td></td>
  10. </tr>
  11. </xsl:for-each>

XSL Counters

On our simple invoice, we have a column for Order Line Number, which will hold the line number. Since this information is not contained within the XML, we need to generate the number from within the XSL. We can do this by using the number element. XSL:number will output a number, as opposed to value-of which outputs a string.

  1. <xsl:number value="position()"/>

The position is a function that returns an index of the current element within the loop, the first item is 1.

XSL and Maths

XSL can use basic mathematical operators such as addition, subtraction, multiplication and subtraction as well as more advanced functions like mod, floor, ceiling and round.

We can use the multiplication symbol within the value-of tag to create a line total.

  1. <xsl:value-of select="OrderQuantity * Price"/>

The final XSL for this section now looks like this:

  1. <xsl:for-each select="/Invoice/OrderDetails/OrderLine">
  2. <tr>
  3. <td><xsl:number value="position()"/></td>
  4. <td><xsl:value-of select="ProductCode"/></td>
  5. <td><xsl:value-of select="ProductDescription"/></td>
  6. <td><xsl:value-of select="OrderQuantity"/></td>
  7. <td><xsl:value-of select="Price"/></td>
  8. <td><xsl:value-of select="OrderQuantity * Price"/></td>
  9. <td><xsl:value-of select="VATAmount"/></td>
  10. </tr>
  11. </xsl:for-each>

Generating Totals

Below our main table, we have a row for sub and grand totals. The easiest column is the VAT total, as we only have to create a sum of the rows. XSL provides a function for calculating the sum of a recurring item, called sum. This will calculate the total of all the VAT Amount items that match the XPath - including all the repeating items.

  1. <xsl:value-of select="sum(/Invoice/OrderDetails/OrderLine/VATAmount)"/>

The subtotal and grand total are more difficult, as the line total is equal to the quantity multiplied by the unit price. If the XML document does not contain a line value tag, you can calculate quantity * price using a custom method that will recurse through the items.

Templates and Recursion

In XSL methods are known as templates, so we have to create a new template and it will call itself on each row and calculate the values. This code must go outside the first main template.

  1. <xsl:template name="Totals">
  2. <xsl:param name="curSum"/>
  3. <xsl:param name="count"/>
  4. <xsl:param name="includeVat"/>
  5.  
  6. <xsl:variable name="qty" select="number(/Invoice/OrderDetails/OrderLine[number($count)]/OrderQuantity)"/>
  7. <xsl:variable name="value" select="number(/Invoice/OrderDetails/OrderLine[number($count)]/Price)"/>
  8. <xsl:variable name="sum" select="number($qty * $value)"/>
  9. <xsl:variable name="loopSum" select="number($curSum + $sum)"/>
  10.  
  11. <xsl:choose>
  12. <xsl:when test="number($count - 1) & gt; 0">
  13. <xsl:call-template name="Totals">
  14. <xsl:with-param name="curSum"><xsl:value-of select="number($loopSum)"/></xsl:with-param>
  15. <xsl:with-param name="count"><xsl:value-of select="number($count - 1)"/></xsl:with-param>
  16. <xsl:with-param name="includeVat"><xsl:value-of select="$includeVat"/></xsl:with-param>
  17. </xsl:call-template>
  18. </xsl:when>
  19. <xsl:otherwise>
  20. <xsl:choose>
  21. <xsl:when test="$includeVat = string('true')">
  22. <xsl:variable name="vatAmount" select="sum(/Invoice/OrderDetails/OrderLine/VATAmount)"/>
  23. <xsl:variable name="grandTotal" select="$loopSum + $vatAmount"/> <xsl:value-of select="/Invoice/OrderDetails/@Currency"/>
  24. <xsl:value-of select="format-number($grandTotal,'#.00')"/>
  25. </xsl:when>
  26. <xsl:otherwise>
  27. <xsl:value-of select="format-number($loopSum,'#.00')"/>
  28. </xsl:otherwise>
  29. </xsl:choose>
  30. </xsl:otherwise>
  31. </xsl:choose>
  32. </xsl:template>

Firstly we define a new template called Totals and it has three parameters, curSum to hold the current value, count to hold the item being processed (starting at the number of items found) and includeVat is a flag used for the grand total.

We define variables to get the quantity and line price of the current item, which we can do by referencing a recurring item using square brackets (similar to an array in C#, however, the first item is 1). We pass in an index that was specified in the parameter. Then we multiply quantity and value together to get the subtotal for that line and then add it to the running cost.

We also output the currency figure by accessing the XML attribute value
, using the @ symbol, in our case:

  1. <xsl:value-of select="/Invoice/OrderDetails/@Currency"/>

.

xsl:if and xsl:choose

Now comes the recursion logic. We need to determine if the item just processed is the last item or not. Since our method passes in the current item, starting at the item count, and we work down, as long as the current item is greater than 1 we need to recurse.

This is represented as a simple if... else statement, however xsl:if does not have an xsl:else, but we can fake it by using xsl:choose which is the equivalent of a switch case statement.

An if... else statement in C# looks like this:

  1. if ((count -1) > 1)
  2. {
  3. // recurse
  4. } else {
  5. // output
  6. }

and re-written for xsl:choose it looks like this:

  1. <xsl:choose>
  2. <xsl:when test="number($count - 1) & gt; 0">
  3. <!-- recurse -->
  4. </xsl:when>
  5. <xsl:otherwise>
  6. <!-- output -->
  7. </xsl:otherwise>
  8. </xsl:choose>

Note: The test statement must have the html short code & gt; (no space between & and gt;) instead of > and & lt; instead of <, otherwise it will not work. This is true for the if statement as well. You can also use =, != (not equals), functions such as mod, position() and last(), as well as logic operators such as and and or. Tests can be any valid expression that results in a Boolean (true/false) result.

If our test $count -1 is greater than 0 then we need to recurse to process the next item so we use a xsl:call-template to call Totals again, passing in the current running total and the item number minus one.

When we reach a stage where the item number of 1 is being processed we don't recurse anymore and can output the value. We use another xsl:choose statement to work out if we need to include VAT figures for the grand totals or just output the subtotal.

xsl:call-template

Now we have created a template, we need to call it using xsl:call-template in order to display the subtotal and the VAT total.

  1. <tr class="subTotals">
  2. <td></td>
  3. <td></td>
  4. <td></td>
  5. <td></td>
  6. <td>Sub Total:</td>
  7. <td>
  8. <xsl:variable name="count" select="count(/Invoice/OrderDetails/OrderLine)"/>
  9. <xsl:call-template name="Totals">
  10. <xsl:with-param name="curSum"><xsl:value-of select="number(0)"/></xsl:with-param>
  11. <xsl:with-param name="count"><xsl:value-of select="number($count)"/></xsl:with-param>
  12. <xsl:with-param name="includeVat"><xsl:value-of select="string('false')"/></xsl:with-param>
  13. </xsl:call-template>
  14. </td>
  15. <td><xsl:value-of select="sum(/Invoice/OrderDetails/OrderLine/VATAmount)"/></td>
  16. </tr>

We declare a variable called count, which is assigned the number of items called OrderLine in the OrderDetails section of the XML. We then do a xsl:call-template on the Totals template, and pass in the variables. The current running total is 0 as we haven't got one yet, and we pass the number of items to process and false because the subtotal does not contain VAT figures.

The next row does nearly the same thing but passes in true because the grand total includes the subtotal and the VAT total.

The final XSL stylesheet is shown below, and you can download the XSL stylesheet and the XML document at the end of the page.

  1. <?xml version="1.0" encoding="ISO-8859-1"?>
  2. <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  3. <xsl:template match="/">
  4. <style type="text/css">
  5. p {margin:0; padding:0}
  6. .headerRow {background-color:navy; color:white}
  7. .subTotals td {border-top:1px solid black}
  8. </style>
  9.  
  10. <table width="100%">
  11. <tr>
  12. <td colspan="3">Payable To</td>
  13. <td colspan="4" rowspan="2"><h1><xsl:value-of select="/Invoice/InvoiceHeader/Sender/CompanyName"/></h1></td>
  14. </tr>
  15. <tr>
  16. <td colspan="3">
  17. <p><xsl:value-of select="/Invoice/InvoiceHeader/Customer/Name"/></p>
  18. <p><xsl:value-of select="/Invoice/InvoiceHeader/Customer/AddressLine1"/></p>
  19. <p><xsl:value-of select="/Invoice/InvoiceHeader/Customer/AddressLine2"/></p>
  20. <p><xsl:value-of select="/Invoice/InvoiceHeader/Customer/AddressLine3"/></p>
  21. <p><xsl:value-of select="/Invoice/InvoiceHeader/Customer/PostalCode"/></p>
  22. <p><xsl:value-of select="/Invoice/InvoiceHeader/Customer/Country"/></p>
  23. </td>
  24. </tr>
  25. <tr>
  26. <td><h2>Invoice</h2></td>
  27. <td></td>
  28. <td></td>
  29. <td>Number:</td>
  30. <td><xsl:value-of select="/Invoice/InvoiceHeader/InvoiceNumber"/></td>
  31. <td>Invoice To:</td>
  32. <td><xsl:value-of select="/Invoice/InvoiceHeader/Customer/Name"/></td>
  33. </tr>
  34. <tr>
  35. <td></td>
  36. <td></td>
  37. <td></td>
  38. <td>Date</td>
  39. <td><xsl:value-of select="/Invoice/InvoiceHeader/InvoiceDate"/></td>
  40. <td>From:</td>
  41. <td>
  42. <p><xsl:value-of select="/Invoice/InvoiceHeader/Sender/CompanyName"/></p>
  43. </td>
  44. </tr>
  45. <tr>
  46. <td></td>
  47. <td></td>
  48. <td></td>
  49. <td></td>
  50. <td></td>
  51. <td></td>
  52. <td>
  53. <p><xsl:value-of select="/Invoice/InvoiceHeader/Sender/RegisteredAddress1"/></p>
  54. <p><xsl:value-of select="/Invoice/InvoiceHeader/Sender/RegisteredAddress2"/></p>
  55. <p><xsl:value-of select="/Invoice/InvoiceHeader/Sender/RegisteredAddress3"/></p>
  56. <p><xsl:value-of select="/Invoice/InvoiceHeader/Sender/PostalCode"/></p>
  57. <p><xsl:value-of select="/Invoice/InvoiceHeader/Sender/Country"/></p>
  58. </td>
  59. </tr>
  60. <tr>
  61. <td></td>
  62. <td></td>
  63. <td></td>
  64. <td></td>
  65. <td></td>
  66. <td>Tel</td>
  67. <td><xsl:value-of select="/Invoice/InvoiceHeader/Sender/TelephoneNumber"/></td>
  68. </tr>
  69. <tr>
  70. <td colspan="7"></td>
  71. </tr>
  72. <tr class="headerRow">
  73. <td>Order Line Number</td>
  74. <td>Product Code</td>
  75. <td>Description</td>
  76. <td>Quantity</td>
  77. <td>Price (ex. vat)</td>
  78. <td>Line Total</td>
  79. <td>VAT Amount</td>
  80. </tr>
  81. <xsl:for-each select="/Invoice/OrderDetails/OrderLine">
  82. <tr>
  83. <td><xsl:number value="position()"/></td>
  84. <td><xsl:value-of select="ProductCode"/></td>
  85. <td><xsl:value-of select="ProductDescription"/></td>
  86. <td><xsl:value-of select="OrderQuantity"/></td>
  87. <td><xsl:value-of select="Price"/></td>
  88. <td><xsl:value-of select="OrderQuantity * Price"/></td>
  89. <td><xsl:value-of select="VATAmount"/></td>
  90. </tr>
  91. </xsl:for-each>
  92. <tr class="subTotals">
  93. <td></td>
  94. <td></td>
  95. <td></td>
  96. <td></td>
  97. <td>Sub Total:</td>
  98. <td>
  99. <xsl:variable name="count" select="count(/Invoice/OrderDetails/OrderLine)"/>
  100. <xsl:call-template name="Totals">
  101. <xsl:with-param name="curSum"><xsl:value-of select="number(0)"/></xsl:with-param>
  102. <xsl:with-param name="count"><xsl:value-of select="number($count)"/></xsl:with-param>
  103. <xsl:with-param name="includeVat"><xsl:value-of select="string('false')"/></xsl:with-param>
  104. </xsl:call-template>
  105. </td>
  106. <td><xsl:value-of select="sum(/Invoice/OrderDetails/OrderLine/VATAmount)"/> <xsl:value-of select="/Invoice/OrderDetails/@Currency"/></td>
  107. </tr>
  108. <tr>
  109. <td></td>
  110. <td></td>
  111. <td></td>
  112. <td></td>
  113. <td>Grand Total:</td>
  114. <td>
  115. <xsl:call-template name="Totals">
  116. <xsl:with-param name="curSum"><xsl:value-of select="number(0)"/></xsl:with-param>
  117. <xsl:with-param name="count"><xsl:value-of select="number($count)"/></xsl:with-param>
  118. <xsl:with-param name="includeVat"><xsl:value-of select="string('true')"/></xsl:with-param>
  119. </xsl:call-template>
  120. </td>
  121. <td></td>
  122. </tr>
  123. </table>
  124. </xsl:template>
  125.  
  126. <xsl:template name="Totals">
  127. <xsl:param name="curSum"/>
  128. <xsl:param name="count"/>
  129. <xsl:param name="includeVat"/>
  130.  
  131. <xsl:variable name="qty" select="number(/Invoice/OrderDetails/OrderLine[number($count)]/OrderQuantity)"/>
  132. <xsl:variable name="value" select="number(/Invoice/OrderDetails/OrderLine[number($count)]/Price)"/>
  133. <xsl:variable name="sum" select="number($qty * $value)"/>
  134. <xsl:variable name="loopSum" select="number($curSum + $sum)"/>
  135.  
  136. <xsl:choose>
  137. <xsl:when test="number($count - 1) & gt; 0">
  138. <xsl:call-template name="Totals">
  139. <xsl:with-param name="curSum"><xsl:value-of select="number($loopSum)"/></xsl:with-param>
  140. <xsl:with-param name="count"><xsl:value-of select="number($count - 1)"/></xsl:with-param>
  141. <xsl:with-param name="includeVat"><xsl:value-of select="$includeVat"/></xsl:with-param>
  142. </xsl:call-template>
  143. </xsl:when>
  144. <xsl:otherwise>
  145. <xsl:choose>
  146. <xsl:when test="$includeVat = string('true')">
  147. <xsl:variable name="vatAmount" select="sum(/Invoice/OrderDetails/OrderLine/VATAmount)"/>
  148. <xsl:variable name="grandTotal" select="$loopSum + $vatAmount"/>
  149. <xsl:value-of select="format-number($grandTotal,'#.00')"/> <xsl:value-of select="/Invoice/OrderDetails/@Currency"/>
  150. </xsl:when>
  151. <xsl:otherwise>
  152. <xsl:value-of select="format-number($loopSum,'#.00')"/> <xsl:value-of select="/Invoice/OrderDetails/@Currency"/>
  153. </xsl:otherwise>
  154. </xsl:choose>
  155. </xsl:otherwise>
  156. </xsl:choose>
  157. </xsl:template>
  158.  
  159. </xsl:stylesheet>

This example is meant only as a demonstration of how XSL stylesheets work and is not intended to be used for live systems.

Further Reading
Comments
  1. hleen
    hleen

    In the XSL file, the second call for the totals you have to include the count parameter, otherwise you will not see a grand-total:

Leave a Reply

Your email address will not be published.