Chapter 10. XML to HTML

That was a surprise to me—that people were prepared to painstakingly write HTML.

Tim Berners-Lee

Introduction

If I had to hazard a guess, I would say that at least 60 percent of the HTML delivered over the Internet today is at least partially generated by code. This is not because HTML is painstakingly hard to write, as Tim Berners-Lee states in the opening quotation (it is, but now we have fancy HTML editors), but because dynamically generated HTML allows you to do so much more.

There are many open and proprietary technologies for delivering HTML content from data stored in other forms. However, when the data is in XML, XSLT is one of the most important tools of which web authors should be aware.

You can use XSLT to generate HTML in three basic ways.

First, XSLT can transform XML into HTML and statically store the generated HTML on a web server or hard drive for delivery to a browser. This is also a good way to test such transformations.

Second, you can use XSLT as a server-side scripting solution in which XML extracted from flat files or databases is dynamically transformed by the web server as requested by the client browser. This solution is necessary when the underlying data changes frequently. However, sometimes a hybrid solution is used in which HTML is constructed on demand, but then cached on the server to avoid the need for subsequent transformations as long as the underlying data does not change. If you wish to use XSLT server side, you should definitely take a look at Apache Cocoon (http://cocoon.apache.org).

Third, you can use XSLT as a client-side stylesheet, provided the browser supports XSLT processing. At this time, the latest versions of Microsoft Internet Explorer (Version 6.0 or later), Netscape Navigator (6.1 or later), Mozilla, Firefox, and Apple Macintosh Safari (Tiger) have support for XSLT right out of the box. Older versions of IE require installation of MSXML 3.0 in replacement mode. In addition, XSmiles (http://www.x-smiles.org/) and the Antenna House XSL Formatter (http://www.antennahouse.com/) perform client-side XSLT processing and display the result. XSmiles can handle all sorts of results, including SVG and XSL-FO, although the HTML handling is not perfect. The Antenna House XSL Formatter handles XSL-FO. As the state of the world changes rapidly in this area, you should check the latest online documentation of your favorite browser or browser add-in.

10.1. Using XSLT as a Styling Language

Problem

You want the browser to dynamically stylize an XML document into HTML.

Solution

Only an XSLT 1.0 solution is needed. Here is an example for publishing a snippet of a DocBook document in HTML using an XSLT stylesheet. The document source is a portion of this chapter:

<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="application/xml" href="chapter.xsl"?>
<chapter label="8">
  <chapterinfo>
    <author>
      <surname>Mangano</surname>
      <firstname>Sal</firstname>
    </author>
    <copyright>
      <year>2002</year>
      <holder>O'Reilly</holder>
    </copyright>
  </chapterinfo>
  <title>XML to HTML</title>
  <epigraph>
    <para>That was a surprise to me - that people were prepared to painstakingly 
write HTML</para>
    <attribution>Tim Berners-Lee</attribution>
  </epigraph>
  <sect1>
    <title>Using XSLT as a Styling Language</title>
    <sect2>
      <title>Problem</title>
      <para>You want to use XSLT to stylize a XML document for dissemination via 
HTML.</para>
    </sect2>
    <sect2>
      <title>Solution</title>
      <para>Here we show an example for publishing a snippet of 
      a DocBook document in HTML using a XSLT stylesheet. The document source is a portion of this 
chapter.</para>
    </sect2>
    <sect2>
      <title>Discussion</title>
      <para>DocBook is an example of a document centric DTD that enables you to 
author and store document content in a presentation-neutral form that captures the 
logical structure of the content. The beauty of authoring documents (especially 
technical ones) in this form is that one can use XSLT to transform a single content 
specification into multiple delivery vehicles such as HTML, PDF, Microsoft Help 
files, and Unix man pages. Although we present this recipe in terms of DocBook, the 
techniques are applicable to other public domain document schema or documents of your 
own creation. </para>
    </sect2>
  </sect1>
</chapter>

Notice that the second line of this document includes a processing instruction, xml-stylesheet. This instructs the browser to apply the following stylesheet to the XML and render the stylesheet’s output rather than the actual XML. (Remember, this instruction works only in recent browser versions):

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="html"/>
  
  <xsl:template match="/">
    <html>
      <head>
        <xsl:apply-templates mode="head"/>
      </head>
      <!-- You may want to use styles in a CSS style element rather -->
      <!-- than hardcoding as I do here -->
      <body style="margin-left:100;margin-right:100;margin-top:50;margin-bottom:50">
        <xsl:apply-templates/> 
        <xsl:apply-templates select="chapter/chapterinfo/*" mode="copyright"/> 
      </body>
    </html>
  
  </xsl:template>
   
  <!-- Head -->
   
  <xsl:template match="chapter" mode="head">
    <xsl:apply-templates select="chapterinfo" mode="head" />
    <xsl:apply-templates select="title" mode="head" />
  </xsl:template>
   
  
  <xsl:template match="chapter/title" mode="head">
        <title><xsl:value-of select="."/></title>
  </xsl:template>
   
  <xsl:template match="author" mode="head">
        <meta name="author" content="{concat(firstname,' ', surname)}"/>
  </xsl:template>
   
  <xsl:template match="copyright" mode="head">
        <meta name="copyright" content="{concat(holder,' ',year)}"/>
  </xsl:template>
   
  <xsl:template match="text()" mode="head"/>
   
<!-- Body -->
  
  <xsl:template match="chapter">
    <div align="right" style="font-size : 48pt; font-family: Times serif; 
    font-weight : bold; padding-bottom:10; color:red" ><xsl:value-of 
    select="@label"/></div>
    <xsl:apply-templates/>
  </xsl:template>  
   
  <xsl:template match="chapter/title">
    <div align="right" style="font-size : 24pt; font-family: Times serif; padding-
bottom:150; color:red"><xsl:value-of select="."/></div>
  </xsl:template>
   
  <xsl:template match="epigraph/para">
    <div align="right" style="font-size : 10pt; font-family: Times 
    serif; font-style : italic; padding-top:4; padding-bottom:4">
    <xsl:value-of select="."/></div>
  </xsl:template>
   
  <xsl:template match="epigraph/attribution">
    <div align="right" style="font-size : 10pt; font-family: Times serif; padding-
top:4; padding-bottom:4"><xsl:value-of select="."/></div>
  </xsl:template>
  
  
  <xsl:template match="sect1">
    <h1 style="font-size : 18pt; font-family: Times serif; font-weight : bold">
      <xsl:value-of select="title"/>
    </h1>
    <xsl:apply-templates/>
  </xsl:template>
   
  <xsl:template match="sect2">
    <h2 style="font-size : 14pt; font-family: Times serif; font-weight : bold">
    <xsl:value-of select="title"/>
    </h2>
     <xsl:apply-templates/>
  </xsl:template>
  
  <xsl:template match="para">
    <p style="font-size : 12pt; font-family: Times serif">
      <xsl:value-of select="."/>
    </p>
  </xsl:template>
   
  <xsl:template match="text()"/>
   
<xsl:template match="copyright" mode="copyright">
  <div style="font-size : 10pt; font-family: Times serif; padding-top : 100">
    <xsl:text>Copyright </xsl:text>
    <xsl:value-of select="holder"/>
    <xsl:text> </xsl:text>
    <xsl:value-of select="year"/>
    <xsl:text>. All rights reserved.</xsl:text>
  </div>
</xsl:template>   
   
<xsl:template match="*" mode="copyright"/>
   
</xsl:stylesheet>

Ultimately, the browser sees the following HTML:

<html>
   <head>
      <meta name="author" content="Sal Mangano">
      <meta name="copyright" content="O'Reilly 2002">
      <title>XML to HTML</title>
   </head>
   <body style="margin-left:100;margin-right:100;margin-top:50;margin-bottom:50">
      <div align="right" style="font-size : 48pt; font-family: Times serif; font-
weight : bold; padding-bottom:10; color:red">8</div>
      <div align="right" style="font-size : 24pt; font-family: Times serif; 
padding-bottom:150; color:red">XML to HTML</div>
      <div align="right" style="font-size : 10pt; font-family: Times serif; font-
style : italic; padding-top:4; padding-bottom:4">That was a surprise to me - that 
people were prepared to painstakingly write HTML</div>
      <div align="right" style="font-size : 10pt; font-family: Times serif; 
padding-top:4; padding-bottom:4">Tim Berners-Lee</div>
      <h1 style="font-size : 18pt; font-family: Times serif; font-weight : bold">
Using XSLT as a Styling Language</h1>
      <h2 style="font-size : 14pt; font-family: Times serif; font-weight : bold">
Problem</h2>
      <p style="font-size : 12pt; font-family: Times serif">You want to use XSLT to 
stylize a XML document for dissemination via HTML.</p>
      <h2 style="font-size : 14pt; font-family: Times serif; font-weight : bold">
Solution</h2>
      <p style="font-size : 12pt; font-family: Times serif">Here we show an example 
for publishing a snippet of a DocBook document in HTML using a XSLT stylesheet. The 
document source is a portion of this chapter.
      </p>
      <h2 style="font-size : 14pt; font-family: Times serif; font-weight : bold">
Discussion</h2>
      <p style="font-size : 12pt; font-family: Times serif">DocBook is an example of 
a document-centric DTD that enables you to author and store document content in a 
presentation-neutral form that captures the logical structure of the content. The 
beauty of authoring documents (especially technical ones) in this form is that one 
can use XSLT to transform a single content specification into multiple delivery 
vehicles such as HTML, PDF, Microsoft Help files, and Unix man pages. Although we 
present this recipe in terms of DocBook, the techniques are applicable to other 
public domain document schema or documents of your own creation.</p>
      <div style="font-size : 10pt; font-family: Times serif; padding-top : 100">
Copyright O'Reilly 2002. All rights reserved.</div>
   </body>
</html>

Discussion

DocBook is a document-centric DTD that enables you to author and store document content in a presentation-neutral form that captures the content’s logical structure. The beauty of authoring documents (especially technical ones) in this form is that you can use XSLT to transform a single content specification into multiple delivery vehicles such as HTML, PDF, Microsoft Help files, and Unix manpages. Although the book presents this example in terms of DocBook, the techniques apply to other public-domain document schema or one of your own creation.

Since you only used a subset of the DocBook DTD, creating a simple monolithic stylesheet was convenient. However, an industrial-strength solution would modularize the handling of many DocBook elements by using separate stylesheets and templates that use modes.

One obvious deficiency in the solution is the way style information is hard coded within the stylesheet. Since basic support for Cascading Stylesheets (CSS) is almost universal in today’s browsers, a superior solution would delegate transformation to XSLT and styling to CSS.

The stylesheet retains its basic structure, but we replace hard-coded style attributes with class attributes that refer to a stylesheet called style.css that you add to the head element of the html:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="html"/>
  
  <xsl:template match="/">
    <html>
      <head>
        <xsl:apply-templates mode="head"/>
        <link href="style.css" rel="stylesheet" type="text/css"/>
      </head>
      <body>
        <xsl:apply-templates/> 
        <xsl:apply-templates select="chapter/chapterinfo/*" mode="copyright"/> 
      </body>
    </html>
  
  </xsl:template>
   
  <!-- Head -->
   
  <xsl:template match="chapter" mode="head">
    <xsl:apply-templates select="chapterinfo" mode="head" />
    <xsl:apply-templates select="title" mode="head" />
  </xsl:template>
   
  
  <xsl:template match="chapter/title" mode="head">
        <title><xsl:value-of select="."/></title>
  </xsl:template>
   
  <xsl:template match="author" mode="head">
        <meta name="author" content="{concat(firstname,' ', surname)}"/>
  </xsl:template>
   
  <xsl:template match="copyright" mode="head">
        <meta name="copyright" content="{concat(holder,' ',year)}"/>
  </xsl:template>
   
  <xsl:template match="text()" mode="head"/>
   
<!-- Body -->
  
  <xsl:template match="chapter">
    <div class="chapter"><xsl:value-of select="@label"/></div>
    <xsl:apply-templates/>
  </xsl:template>  
   
  <xsl:template match="chapter/title">
    <div class="title"><xsl:value-of select="."/></div>
  </xsl:template>
   
  <xsl:template match="epigraph/para">
    <div class="epigraph"><xsl:value-of select="."/></div>
  </xsl:template>
   
  <xsl:template match="epigraph/attribution">
    <div class="epigraph-attribution"><xsl:value-of select="."/></div>
  </xsl:template>
  
  
  <xsl:template match="sect1">
    <h1><xsl:value-of select="title"/></h1>
    <xsl:apply-templates/>
  </xsl:template>
   
  <xsl:template match="sect2">
    <h2><xsl:value-of select="title"/></h2>
     <xsl:apply-templates/>
  </xsl:template>
  
  <xsl:template match="para">
    <p class="para"><xsl:value-of select="."/></p>
  </xsl:template>
   
  <xsl:template match="text()"/>
   
<xsl:template match="copyright" mode="copyright">
  <div class="copyright">
    <xsl:text>Copyright </xsl:text>
    <xsl:value-of select="holder"/>
    <xsl:text> </xsl:text>
    <xsl:value-of select="year"/>
    <xsl:text>. All rights reserved.</xsl:text>
  </div>
</xsl:template>   
   
<xsl:template match="*" mode="copyright"/>
   
</xsl:stylesheet>

A CSS is a simple flat ASCII file (style.css) that follows a simple set of conventions:

body
{
   margin-left:100;
   margin-right:100;
   margin-top:50;
   margin-bottom:50;
   font-family: Times serif;
   font-size : 12pt; 
   color:black; 
}

div.chapter
{
   text-align:right;
   font-size : 48pt; 
   font-weight: bold; 
   padding-bottom:10; 
   color:red;
}

div.title
{
   font-size : 24pt; 
   font-family: Times serif; 
   padding-bottom:150; 
   color:red"
}

div.epigraph
{   
   font-style:: italic;
} 

div.epigraph, div.epigraph-attribution
{
   text-align: right; 
   font-size : 10pt; 
   padding-top: 4; 
   padding-bottom: 4;
}

h1
{
   font-size : 18pt; 
   font-weight : bold;
} 

h2
{
   font-size : 14pt; 
   font-weight : bold";
}

If you are unfamiliar with CSS, you can find a nice tutorial at http://www.w3schools.com/css/default.asp. I also recommend Cascading Style Sheets: The Definitive Guide, Second Edition by Eric Meyer, O’Reilly, 2004. In addition, there are many tools available for creating CSS. I am fond of Macromedia Dreamweaver MX, but some might find this tool a bit heavy if all you need is CSS editor.

See Also

The best source for information about DocBook is http://www.docbook.org/. Norman Walsh has developed a set of open source stylesheets to convert DocBook into various publishing formats. These stylesheets are located at http://docbook.sourceforge.net/projects/xsl/.

Recipe 16.2 demonstrates several techniques for creating more modular and extensible stylesheets.

10.2. Creating Hyperlinked Documents

Problem

You want to convert XML into hyperlinked HTML content.

Solution

A typical course of action when converting XML into HTML is to make two or more passes over the XML to create menu or index pages and content pages. The menu pages contain links to the content pages. (Only an XSLT 1.0 solution is needed here.) The following solution generates an index and summary pages for SalesBySalesPerson.xml (see Chapter 2):

<xsl:stylesheet version="1.0"   
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:saxon="http://icl.com/saxon" 
 extension-element-prefixes="saxon">
   
<xsl:output method="html"/>
   
<xsl:template match="/">
  <xsl:apply-templates select="*" mode="index"/>
  <xsl:apply-templates select="*" mode="content"/>
</xsl:template>
   
<!-- =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  = -->
<!--                Create index.html  (mode = "index")                        -->
<!-- =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  = -->
<xsl:template match="salesBySalesperson" mode="index">
  <saxon:output href="EB0596009747_2.html">     
    <html>
     <head>
       <title>Sales By Salesperson Index</title>
     </head>
    
     <body bgcolor="#FFFFFF" text="#000000">
      <h1>Sales By Salesperson</h1>
      <xsl:apply-templates mode="index"/>
     </body>
    </html>
  </saxon:output>
</xsl:template>
   
<xsl:template match="salesperson" mode="index">
  <h2>
    <a href="{concat(@name,'.html')}">
      <xsl:value-of select="@name"/>
    </a>
  </h2>
</xsl:template>
   
<!-- =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  = -->
<!--               Create @name.html  (mode = "content")                       -->
<!-- =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  = -->
   
<xsl:template match="salesperson" mode="content">
  <saxon:output href="{@name}.html">     
    <html>
     <head>
        <title><xsl:value-of select="@name"/> Sales</title>
     </head>
    
     <body bgcolor="#FFFFFF" text="#000000">
      <h1><xsl:value-of select="@name"/> Sales</h1>
      <ol>
          <xsl:apply-templates mode="content"/>
      </ol>
     </body>
    </html>
  </saxon:output>
</xsl:template>
   
<xsl:template match="product" mode="content">
    <li><xsl:value-of select="@sku"/>&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;$<xsl:value-
of select="@totalSales"/></li>
</xsl:template>
   
</xsl:stylesheet>

Notice how the modes separate the transformation of XML elements into index content versus the information content of each salesperson’s HTML page. Modes are used commonly in HTML transformations because they allow data to be mapped onto presentation in multiple ways within a single stylesheet.

As designed, this stylesheet is limited to batch processing. You can parameterize it to control which document gets created. This parameter also removes the need for the nonstandard saxon:output extension (which is also unnecessary if you use XSLT 2.0):

<xsl:stylesheet version="1.0" 
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   
<xsl:output method="html"/>
<!--Used to specify which document to output-->
               <!--INDEX : creates the index document -->
               <!--Sales Person's name : creates the page for that salesperson --> 
               <xsl:param name="which" select="'INDEX'"/>
   
<xsl:template match="/">
  <xsl:choose>
               <xsl:when test="$which='INDEX'">
               <xsl:apply-templates select="*" mode="index"/>
               </xsl:when>
               <xsl:otherwise>
               <xsl:apply-templates select="*/salesperson[@name = $which]" 
                           mode="content"/>
               </xsl:otherwise>
               </xsl:choose>
</xsl:template>
   
<!-- =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  = -->
<!--            Create index.html  (mode = "index")                 -->
<!-- =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  = -->
<xsl:template match="salesBySalesperson" mode="index">
  <!-- Removed saxon:output. The rest is the same. -->
</xsl:template>
   
<!-- ... -->
   
<xsl:template match="salesperson" mode="content">
  <!-- Removed saxon:output. The rest is the same. -->
</xsl:template>
   
<!-- ... -->
   
</xsl:stylesheet>

This technique would be slightly more robust if each salesperson used an ID rather than her name as the parameter.

Discussion

The solution does not create fancy content, but it does illustrate the basic mechanics of producing linked HTML content. To produce all web pages with a single stylesheet, you were forced to use a nonstandard XSLT 1.0 element (saxon:output). Similar extensions are available in most processors, and XSLT 2.0 has XSL:result-document.

The stylesheet produces relative links, which is what you want most of the time. However, when you need absolute links, rather than hardcoding a URL, you might consider incorporating a top-level parameter that can be set to the URL:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   
<xsl:output method="html"/>
   
<xsl:param name="URL" select="http://www.mycompany.com/"/>
   
<!-- elided ... -->
   
<xsl:template match="salesperson" mode="index">
  <h2>
    <a href="{$URL}{@name}.html'">
      <xsl:value-of select="@name"/>
    </a>
  </h2>
</xsl:template>

See Also

See Recipe 6.6 for more information on producing multiple output documents.

The content produced by these transformations is not very user friendly. Recipe 10.3 and Recipe 10.4 show how to improve the result’s aesthetics.

10.3. Creating HTML Tables

Problem

You want to map XML content onto HTML tables.

Solution

Tables are often created in two stages. First, the top-level table markup is generated, and then templates are applied to create rows and fields. The solution is a modification of part of the stylesheet produced in Recipe 10.2. The changed portion is highlighted:

<xsl:stylesheet version="1.1" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   
<xsl:output method="html"/>
   
<xsl:param name="URL"/>
   
<xsl:template match="/">
  <xsl:apply-templates select="*" mode="index"/>
  <xsl:apply-templates select="*" mode="content"/>
</xsl:template>
   
<!-- =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  = -->
<!--             Create index.html  (mode = "index")                -->
<!-- =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  = -->
<xsl:template match="salesBySalesperson" mode="index">
  <!-- Non-standard saxon xsl:document! -->
  <xsl:document href="EB0596009747_2.html">     
    <html>
     <head>
      <title>Sales by Salesperson</title>
     </head>
    
     <body bgcolor="#FFFFFF" text="#000000">
      <h1>Sales By Salesperson</h1>
      <xsl:apply-templates mode="index"/>
     </body>
    </html>
  </xsl:document>
</xsl:template>
   
<xsl:template match="salesperson" mode="index">
  <h2>
    <a href="{concat($URL,@name,'.html')}">
      <xsl:value-of select="@name"/>
    </a>
  </h2>
</xsl:template>
   
<!-- =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  = -->
<!--          Create @name.html  (mode = "content")                 -->
<!-- =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  = -->
   
<xsl:template match="salesperson" mode="content">
               <xsl:document href="{concat(@name,'.html')}">     
               <html>
               <head>
               <title><xsl:value select="@name"/></title>
               </head>
    
               <body bgcolor="#FFFFFF" text="#000000">
               <h1><xsl:value-of select="@name"/> Sales</h1>
               <table border="1" cellpadding="3">
               <tbody >
               <tr>
               <th>SKU</th>
               <th>Sales (in US $)</th>
               </tr>
               <xsl:apply-templates mode="content"/>
               </tbody>
               </table>
               <h2><a href="{concat($URL,'index.html')}">Home</a></h2>
               </body>
               </html>
               </xsl:document>
               </xsl:template>
   
               <xsl:template match="product" mode="content">
               <tr>
               <td><xsl:value-of select="@sku"/></td>
               <td><xsl:value-of select="@totalSales"/></td>
               </tr>
    
               </xsl:template>
   
</xsl:stylesheet>

Discussion

XSLT 1.0

When creating tables, you often need to group data based on specific criteria. The difficulty of a grouping problem is related to whether the input data is already grouped and whether the grouping criteria are close- or open-ended. For example, imagine that you need to group sales data by region:

<?xml version="1.0" encoding="UTF-8"?>
<sales>
  <product sku="10000" sales="90000.00" region="NE"/>
  <product sku="10000" sales="10000.00" region="NW"/>
  <product sku="10000" sales="55000.00" region="SE"/>
  <product sku="10000" sales="32000.00" region="SW"/>
  <product sku="10000" sales="95000.00" region="NC"/>
  <product sku="10000" sales="88000.00" region="SC"/>
  <product sku="20000" sales="77000.00" region="NE"/>
  <product sku="20000" sales="11100.00" region="NW"/>
  <product sku="20000" sales="33210.00" region="SE"/>
  <product sku="20000" sales="78000.00" region="SW"/>
  <product sku="20000" sales="105000.00" region="NC"/>
  <product sku="20000" sales="12300.00" region="SC"/>
  <product sku="30000" sales="1000.00" region="NE"/>
  <product sku="30000" sales="5100.00" region="NW"/>
  <product sku="30000" sales="3210.00" region="SE"/>
  <product sku="30000" sales="8000.00" region="SW"/>
  <product sku="30000" sales="5000.00" region="NC"/>
  <product sku="30000" sales="11300.00" region="SC"/>
</sales>

Here, you know in advance that there are six regions. You could solve the grouping problem by explicit selection:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="html"/>
  
<xsl:template match="sales">
  <html>
    <head>
      <title>Sales by Region</title>
    </head>
    <body>
      <h1>Sales by Region</h1>
      <table border="1" cellpadding="3">
        <tbody>
          <tr>
            <th>SKU</th>
            <th>Sales</th>
          </tr>
          <xsl:call-template name="group-region">
            <xsl:with-param name="region" select=" 'NE' "/>
            <xsl:with-param name="title" select="'North East Sales'"/>
          </xsl:call-template>
          <xsl:call-template name="group-region">
            <xsl:with-param name="region" select=" 'NW' "/>
            <xsl:with-param name="title" select="'North West Sales'"/>
          </xsl:call-template>
          <xsl:call-template name="group-region">
            <xsl:with-param name="region" select=" 'NC' "/>
            <xsl:with-param name="title" select="'North Central Sales'"/>
          </xsl:call-template>
          <xsl:call-template name="group-region">
            <xsl:with-param name="region" select=" 'SE' "/>
            <xsl:with-param name="title" select="'South East Sales'"/>
          </xsl:call-template>
          <xsl:call-template name="group-region">
            <xsl:with-param name="region" select=" 'SC' "/>
            <xsl:with-param name="title" select="'South Central Sales'"/>
          </xsl:call-template>
          <xsl:call-template name="group-region">
            <xsl:with-param name="region" select=" 'SW' "/>
            <xsl:with-param name="title" select="'South West Sales'"/>
          </xsl:call-template>
        </tbody>
      </table>
    </body>
  </html>
</xsl:template>
   
<xsl:template name="group-region">
    <xsl:param name="region"/>
    <xsl:param name="title"/>
    <xsl:variable name="products" select="product[@region = $region]" />
    <tr>
      <th colspan="2"><xsl:value-of select="$title" /></th>
    </tr>
    <xsl:apply-templates select="$products"/>
    <tr style="font-weight:bold">
      <td >Total</td>
      <td align="right">
        <xsl:value-of 
             select="format-number(sum($products/@sales), '#.00')"/>
      </td>
     </tr>
  </xsl:template>
   
  <xsl:template match="product">
    <tr>
      <td><xsl:value-of select="@sku"/></td>
      <td align="right"><xsl:value-of select="@sales"/></td>
    </tr>
  </xsl:template>
  
</xsl:stylesheet>

If you find the hard coding of group names objectionable, use a table-driven approach:

<xsl:stylesheet version="1.0" 
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:sales="sales">
  
  <sales:region code="NE" name="North East"/>
  <sales:region code="NC" name="North Central"/>
  <sales:region code="NW" name="North West"/>
  <sales:region code="SE" name="South East"/>
  <sales:region code="SC" name="South Central"/>
  <sales:region code="SW" name="South West"/>
   
 <xsl:variable name="products" select="/sales/product"/>
  
  <xsl:output method="html"/>
    
  <xsl:template match="sales">
    <html>
      <head>
        <title>Sales by Region</title>
      </head>
      <body>
        <h1>Sales by Region</h1>
        <table border="1" cellpadding="3">
          <tbody>
            <tr>
              <th>SKU</th>
              <th>Sales</th>
            </tr>
            <xsl:for-each select="document('')/*/sales:region">
              <tr >
                <th colspan="2"><xsl:value-of select="@name"/> Sales</th>
              </tr>
              <xsl:call-template name="group-region">
                <xsl:with-param name="region" select="@code"/>
              </xsl:call-template>
            </xsl:for-each>
          </tbody>
        </table>
      </body>
    </html>
  </xsl:template>
   
  <xsl:template name="group-region">
    <xsl:param name="region"/>
        <xsl:apply-templates select="$products[@region=$region]"/>
        <tr style="font-weight:bold">
          <td >Total</td>
          <td align="right"><xsl:value-of 
          select="format-number(sum($products[@region=$region]/@sales),'#.00')"/>
          </td>
        </tr>
  </xsl:template>
   
  
  <xsl:template match="product">
    <tr>
      <td><xsl:value-of select="@sku"/></td>
      <td align="right"><xsl:value-of select="@sales"/></td>
    </tr>
  </xsl:template>
  
</xsl:stylesheet>

Of course, many grouping problems are not this easy. Imagine that you are designing a stylesheet to be used by many companies or divisions within a large company that each had their own conventions for naming sales regions. You can tackle this problem in several ways, but one of the most efficient is called the Muenchian grouping technique (named after Steve Muench of Oracle who invented it), which uses a combination of keys and IDs:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
    xmlns:sales="sales">
  
  <xsl:output method="html"/>
   
 <xsl:key name="region-key" match="product" use="@region"/>
  
  <xsl:template match="sales">
    <html>
      <head>
        <title>Sales by Region</title>
      </head>
      <body>
        <h1>Sales by Region</h1>
        <table border="1" cellpadding="3">
          <tbody>
            <tr>
              <th>SKU</th>
              <th>Sales</th>
            </tr>
            <xsl:variable name="unique-regions" 
                 select="/sales
                         /product[generate-id(.) = 
                                  generate-id(key('region-key',@region))]
                         /@region"/>
                  <xsl:for-each select="$unique-regions">
              <tr >
                <th colspan="2"><xsl:value-of select="."/> Sales</th>
              </tr>
              <xsl:call-template name="group-region">
                <xsl:with-param name="region" select="."/>
              </xsl:call-template>
            </xsl:for-each>
          </tbody>
        </table>
      </body>
    </html>
  </xsl:template>
   
  <xsl:template name="group-region">
    <xsl:param name="region"/>
        <xsl:apply-templates select="key('region-key', @region)"/>
        <tr style="font-weight:bold">
          <td >Total</td>
          <td align="right"><xsl:value-of 
          select="format-number(sum(key('region-key', @region)/@sales),'#.00')"/>
          </td>
        </tr>
  </xsl:template>
  
  <xsl:template match="product">
    <tr>
      <td><xsl:value-of select="@sku"/></td>
      <td align="right"><xsl:value-of select="@sales"/></td>
    </tr>
  </xsl:template>
  
</xsl:stylesheet>

Structurally, the solution is similar to the table-driven method. The main difference is in how you determine the unique set of groups. In the table-driven case, the unique sales regions are literally encoded in sales:region elements. The Muenchian technique uses a key to define the grouping value. You know that the expression key('region-key',@region) returns a node set of all products whose region is @region. You also know that generate-id, when presented with a node-set, returns a unique ID for the first element in the node set. Thus, the expression [generate-id(.) = generate-id( key( 'region-key', @region))] will be true only for the first node within each group, in this case allowing you to obtain all unique regions that make up the group. Having this key also makes other parts of the stylesheet that refer to product by region more efficient.

XSLT 2.0

Unless you have skipped directly to this chapter, you know that the best way to tackle grouping problems in XSLT 2.0 is via xsl:for-each-group:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" 
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  
  <xsl:output method="html"/>
     
  <xsl:template match="sales">
    <html>
      <head>
        <title>Sales by Region</title>
      </head>
      <body>
        <h1>Sales by Region</h1>
        <table border="1" cellpadding="3">
          <tbody>
            <tr>
              <th>SKU</th>
              <th>Sales</th>
            </tr>
            <xsl:for-each-group select="product" group-by="@region">
                  <tr >
                  <th colspan="2"><xsl:value-of select="."/> Sales</th>
                  </tr>
                  <xsl:apply-templates select="current-group()"/>
                  <tr style="font-weight:bold">
                  <td >Total</td>
                  <td align="right"><xsl:value-of 
                  select="format-number(sum(current-group()/@sales),'#.00')"/>
                  </td>
                  </tr>
                  </xsl:for-each-group>
          </tbody>
        </table>
      </body>
    </html>
  </xsl:template>
     
  <xsl:template match="product">
    <tr>
      <td><xsl:value-of select="@sku"/></td>
      <td align="right"><xsl:value-of select="@sales"/></td>
    </tr>
  </xsl:template>
  

</xsl:stylesheet>

10.4. Creating Frames

Problem

You want to generate HTML that organizes content by using HTML frames.

Solution

As in Recipe 10.2, you will use modes to make multiple passes over the XML. First, create the frameset container document. To do so, you use two frames. The smaller left frame holds the names of the salespeople as hyperlinks for activating content in the mainframe. The main frame contains the sales figures for the salesperson selected by the user. This example provides a default main frame that is displayed when the page first comes up:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   
<xsl:output method="html"/>
   
<xsl:param name="URL"/>
   
<xsl:template match="/">
  <xsl:apply-templates select="*" mode="frameset"/>
  <xsl:apply-templates select="*" mode="salespeople_frame"/>
  <xsl:apply-templates select="*" mode="sales_frames"/>
</xsl:template>
   
<!-- =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  = -->
<!--              Create frameset container (mode ="frameset")      -->
<!-- =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  = -->
<xsl:template match="salesBySalesperson" mode="frameset">
  <!-- Non-standard saxon xsl:document! -->
  <xsl:document href="EB0596009747_2.html">     
    <html>
     <head>
      <title>Salesperson Frameset</title>
     </head>
     <frameset rows="100%" cols="25%, 75%" border="0">
       <frame name="salespeople" src="salespeople_frame.html" noresize=""/>
       <frame name="mainFrame" src="default_sales.html" noresize=""/>
     </frameset>
     <body bgcolor="#FFFFFF" text="#000000">
     </body>
    </html>
  </xsl:document>
</xsl:template>
   
<!-- =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  = -->
<!-- Create salespeople_frame.html  (mode = "salespeople_frame")    -->
<!-- =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  = -->
<xsl:template match="salesBySalesperson" mode="salespeople_frame">
  <!-- Non-standard xsl: saxon:document! -->
  <xsl:document href="salespeople_frame.html">
    <html>
     <head>
       <title>Salespeople</title>
     </head>
     <body bgcolor="#FFFFFF" text="#000000">
       <table>
        <tbody>
          <xsl:apply-templates mode="index"/>
        </tbody>
      </table>
     </body>
    </html>
  </xsl:document>
</xsl:template>
   
<xsl:template match="salesperson" mode="index">
  <tr>
    <td>
      <a href="{concat(@name,'.html')}" 
          target="mainFrame"><xsl:value-of select="@name"/></a>
    </td>
  </tr>
</xsl:template>
   
<!-- =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  = -->
<!--                  Create @name.html  (mode = "content")         -->
<!-- =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  = -->
   
<xsl:template match="salesperson" mode="sales_frames">
   
  <xsl:document href="default_sales.html">
    <html>
     <head>
       <title>Default</title>
     </head>
    
     <body bgcolor="#FFFFFF" text="#000000">
     <h1><center>Sales By Salesperson</center></h1>
      <br/>
     Click on a salesperson on the left to load his or her sales figures.
     </body>
    </html>
  </xsl:document>
   
  <xsl:document href="{concat(@name,'.html')}">
    <html>
     <head>
       <title><xsl:value-of select="@name"/></title>
     </head>
    
     <body bgcolor="#FFFFFF" text="#000000">
     <h1><center>Sales By Salesperson</center></h1>
      <h2><xsl:value-of select="@name"/></h2>
      <table border="1" cellpadding="3">
        <tbody >
          <tr>
            <th>SKU</th>
            <th>Sales (in US $)</th>
          </tr>
          <xsl:apply-templates mode="content"/>
        </tbody>
      </table>
     </body>
    </html>
  </xsl:document>
</xsl:template>
   
<xsl:template match="product" mode="content">
    <tr>
      <td><xsl:value-of select="@sku"/></td>
      <td align="right"><xsl:value-of select="@totalSales"/></td>
    </tr>
    
</xsl:template>
   
</xsl:stylesheet>

Discussion

Frames are useful for splitting a page into logical sections; however, using frames with visible or resizable borders is somewhat passé. Your stylesheet cannot be used for client-side transformation because it outputs many separate HTML files—one for the frameset, one for the left frame that lists salespeople, and a separate page for each salesperson.

The solution demonstrates an example that is dynamic and open-ended in terms of the number of separate pages that might be generated from a single XML file. However, cases in which frames can be used with client-side XSLT processing are more straightforward. Simply create a page containing a frameset where the frames hold separate XML documents, each with their own transformation:

<html>
   <head>
      <title>Frameset</title> 
   </head>
   <frameset rows="100%" cols="25%, 75%" border="0">
      <frame name="leftFrame" src="left.xml" noresize="">
      <frame name="mainFrame" src="main.xml" noresize="">
   </frameset>
   <body bgcolor="#FFFFFF" text="#000000"></body>
</html>

You must reference different XML documents for each frame because there can be only one xml-stylesheet processing instruction per file. However, you can still keep the content in one stylesheet by using an xinclude link (http://www.w3.org/TR/xinclude/) and an XSLT template to process it.

left.xml looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="application/xml" href="left.xsl"?>
<xi:include href="salesBySalesperson.xml" 
            xmlns:xi="http://www.w3.org/2001/XInclude"/>

main.xml is similar:

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="application/xml" href="main.xsl"?> 
<xi:include href="salesBySalesperson.xml"
            xmlns:xi="http://www.w3.org/2001/XInclude"/>

left.xsl and main.xsl contain a template that processes the xinclude element by using the document function:

<!-- left.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="html"/>
   
<xsl:template match="xi:include" xmlns:xi="http://www.w3.org/2001/XInclude">
  <xsl:for-each select="document(@href,.)"> 
    <xsl:apply-templates/>
  </xsl:for-each>
</xsl:template> 
   
<xsl:template match="salesBySalesperson">
  <html>
   <head>
    <title>Salespeople</title>
   </head>
   <body bgcolor="#FFFFFF" text="#000000">
     <table>
      <tbody>
        <xsl:apply-templates/>
      </tbody>
    </table>
   </body>
  </html>
</xsl:template>
   
<xsl:template match="salesperson">
  <tr>
    <td>
      <a href="{concat('main.xml#',@name)}" target="mainFrame">
      <xsl:value-of select="@name"/></a>
    </td>
  </tr>
</xsl:template>
   
</xsl:stylesheet>
   
<!-- main.xsl -->
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="html"/>
   
<xsl:template match="xi:include" xmlns:xi="http://www.w3.org/2001/XInclude">
  <xsl:for-each select="document(@href)"> 
    <xsl:apply-templates/>
  </xsl:for-each>
</xsl:template> 
   
<xsl:template match="salesBySalesperson">
    <html>
     <head>
      <title>Sales By Salesperson</title>
     </head>
    
     <body bgcolor="#FFFFFF" text="#000000">
     <xsl:apply-templates/>
     </body>
    </html>
</xsl:template>
   
<xsl:template match="salesperson">
     <h1><a name="{@name}"><center>Sales By Salesperson</center></a></h1>
      <h2><xsl:value-of select="@name"/></h2>
      <table border="1" cellpadding="3">
        <tbody >
          <tr>
            <th>SKU</th>
            <th>Sales (in US $)</th>
          </tr>
          <xsl:apply-templates />
        </tbody>
      </table>
      <div style="padding-top:1000"/>
</xsl:template>
   
<xsl:template match="product">
    <tr>
      <td><xsl:value-of select="@sku"/></td>
      <td align="right"><xsl:value-of select="@totalSales"/></td>
    </tr>
    
</xsl:template>
       
</xsl:stylesheet>

In the main.xsl stylesheet, you generate named anchors for each salesperson and separate them by a large amount of whitespace by using <div style="padding-top:1000"/>. In left.xsl, you generate links to the anchors. This generation crudely emulates behavior of the multipage example.

See Also

Jeni Tennison’s XSLT and XPath on the Edge (M&T, 2001) includes a detailed discussion of client-side XSLT processing with frames that shows how JavaScript can create more sophisticated results.

10.5. Creating Data-Driven Stylesheets

Problem

You want to generate HTML that is styled based on data content.

Solution

XSLT 1.0

XSLT attribute sets provide a nice vehicle for encapsulating the complexity of data-driven stylization. Consider how the following XML describes an investment portfolio:

<portfolio>
  <investment>
    <symbol>IBM</symbol>
    <current>72.70</current>
    <paid>65.00</paid>
    <qty>1000</qty>
  </investment>
  <investment>
    <symbol>JMAR</symbol>
    <current>1.90</current>
    <paid>5.10</paid>
    <qty>5000</qty>
  </investment>
  <investment>
    <symbol>DELL</symbol>
    <current>24.50</current>
    <paid>18.00</paid>
    <qty>100000</qty>
  </investment>
  <investment>
    <symbol>P</symbol>
    <current>57.33</current>
    <paid>63</paid>
    <qty>100</qty>
  </investment>
</portfolio>

You can display this portfolio in a table with a column showing the gain in black or the loss in red:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   
  <xsl:output method="html"/>
   
  <xsl:attribute-set name="gain-loss-font">
    <xsl:attribute name="color">
      <xsl:choose>
                  <xsl:when test="(current - paid) * qty >= 0">black</xsl:when>
                  <xsl:otherwise>red</xsl:otherwise>
                  </xsl:choose>
    </xsl:attribute>
  </xsl:attribute-set>      
   
<xsl:template match="portfolio">
    <html>
     <head>
      <title>My Portfolio</title>
     </head>
    
     <body bgcolor="#FFFFFF" text="#000000">
      <h1>Portfolio</h1>
      <table border="1" cellpadding="2">
        <tbody>
          <tr>
            <th>Symbol</th>
            <th>Current</th>
            <th>Paid</th>
            <th>Qty</th>
            <th>Gain/Loss</th>
          </tr>
          <xsl:apply-templates/>
        </tbody>
      </table>
     </body>
    </html>
</xsl:template>
   
<xsl:template match="investment">
  <tr>
    <td><xsl:value-of select="symbol"/></td>
    <td><xsl:value-of select="current"/></td>
    <td><xsl:value-of select="paid"/></td>
    <td><xsl:value-of select="qty"/></td>
    <td>
     <font xsl:use-attribute-sets="gain-loss-font">
      <xsl:value-of 
        select="format-number((current - paid) * qty, '#,##0.00')"/>
     </font>
    </td>
  </tr>
</xsl:template>     
   
</xsl:stylesheet>

If you are not concerned with backward compatibility to older browsers, this example can be made cleaner by using the HTML 4.0 style attribute instead of the font element:

  <xsl:attribute-set name="gain-loss-color">
    <xsl:attribute name="style">color:<xsl:text/>
      <xsl:choose>
        <xsl:when test="(current - paid) * qty >= 0">black</xsl:when>
        <xsl:otherwise>red</xsl:otherwise>
      </xsl:choose>
    </xsl:attribute>
  </xsl:attribute-set>      
   
...
   
<xsl:template match="investment">
  <tr>
    <td><xsl:value-of select="symbol"/></td>
    <td><xsl:value-of select="current"/></td>
    <td><xsl:value-of select="paid"/></td>
    <td><xsl:value-of select="qty"/></td>
    <td xsl:use-attribute-sets="gain-loss-color">
      <xsl:value-of 
              select="format-number((current - paid) * qty, '#,##0.00')"/>
    </td>
  </tr>
</xsl:template>

XSLT 2.0

The main improvement when using 2.0 is the brevity achieved by using XPath expressions instead of xsl:choose. For example, consider the following rewrite of a code snippet from above:

  <xsl:attribute-set name="gain-loss-font">
    <xsl:attribute name="color" 
                   select="if ((current - paid) * qty ge 0) 
                           then 'black' else 'red'"/>
  </xsl:attribute-set>

Discussion

As is usually the case with XSLT, you can approach this problem in many ways. You might consider embedding the style logic directly into the table-generation logic:

<xsl:template match="investment">
  <tr>
    <td><xsl:value-of select="symbol"/></td>
    <td><xsl:value-of select="current"/></td>
    <td><xsl:value-of select="paid"/></td>
    <td><xsl:value-of select="qty"/></td>
    <td>
      <font>
        <xsl:attribute name="color">
          <xsl:choose>
            <xsl:when test="(current - paid) * qty >= 0">black</xsl:when>
            <xsl:otherwise>red</xsl:otherwise>
          </xsl:choose>
        </xsl:attribute>
        <xsl:value-of 
          select="format-number((current - paid) * qty, '#,##0.00')"/>
      </font>
    </td>
  </tr>
</xsl:template>

Although placing the color determination logic inline might help you figure out what is happening, it complicates the table-creation logic. A more complex example might compute style attributes for many elements. Mixing the structural building aspect of the stylesheet with the stylization aspect will make each aspect harder to understand and modify.

Nevertheless, you can argue that the hardcoding element references in attributes sets detract from their reusability. However, you can usually remedy this problem by simply moving the logic of the attribute determination outside of the attribute set by using templates and modes. Consider a portfolio with varied investments whose profitability is calculated in different ways:

<portfolio>
   
  <stock>
    <symbol>IBM</symbol>
    <current>72.70</current>
    <paid>65.00</paid>
    <qty>1000</qty>
  </stock>
   
  <stock>
    <symbol>JMAR</symbol>
    <current>1.90</current>
    <paid>5.10</paid>
    <qty>5000</qty>
  </stock>
   
  <stock>
    <symbol>DELL</symbol>
    <current>24.50</current>
    <paid>18.00</paid>
    <qty>100000</qty>
  </stock>
   
  <stock>
    <symbol>P</symbol>
    <current>57.33</current>
    <paid>63.00</paid>
    <qty>100</qty>
  </stock>
  
  <property>
    <address>123 Main St. Anytown NY</address>
    <paid>100000</paid>
    <appraisal>250000</appraisal>
  </property>
   
  <property>
    <address>13 Skunks Misery Dr. Stinksville NJ</address>
    <paid>200000</paid>
    <appraisal>50000</appraisal>
  </property>
  
</portfolio>

You can avoid having to define two attribute sets that perform the same function by pushing the logic into templates:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   
  <xsl:output method="html"/>
   
  <xsl:attribute-set name="gain-loss-font">
    <xsl:attribute name="color">
      <xsl:apply-templates select="." mode="gain-loss-font-color"/>
    </xsl:attribute>
  </xsl:attribute-set>      
   
<xsl:template match="stock" mode="gain-loss-font-color">
    <xsl:choose>
      <xsl:when test="(current - paid) * qty >= 0">black</xsl:when>
      <xsl:otherwise>red</xsl:otherwise>
    </xsl:choose>
</xsl:template>
   
<xsl:template match="property" mode="gain-loss-font-color">
    <xsl:choose>
      <xsl:when test="appraisal - paid  >= 0">black</xsl:when>
      <xsl:otherwise>red</xsl:otherwise>
    </xsl:choose>
</xsl:template>
   
... 
   
</xsl:stylesheet>

You might be uncomfortable incorporating any purely stylistic attributes such as colors, fonts, and the like into your XSLT transformation. Perhaps it is not your job—but the job of a company style czar—to decide how to render gains and losses. In this case, you can simply classify the elements and defer stylizing decisions to a separately defined stylesheet:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   
  <xsl:output method="html"/>
   
  <xsl:attribute-set name="gain-loss">
               <xsl:attribute name="class">
               <xsl:apply-templates select="." mode="gain-loss"/>
               </xsl:attribute>
               </xsl:attribute-set>      
   
               <xsl:template match="stock" mode="gain-loss">
               <xsl:choose>
               <xsl:when test="(current - paid) * qty >= 0">gain</xsl:when>
               <xsl:otherwise>loss</xsl:otherwise>
               </xsl:choose>
               </xsl:template>
   
               <xsl:template match="property" mode="gain-loss">
               <xsl:choose>
               <xsl:when test="appraisal - paid  >= 0">gain</xsl:when>
               <xsl:otherwise>loss</xsl:otherwise>
               </xsl:choose>
               </xsl:template>
   
<xsl:template match="portfolio">
    <html>
     <head>
      <title>My Portfolio</title>
      <link  rel="stylesheet"  type="text/css"  href="portfolio.css"/>
     </head>
   
     ...
    
</xsl:template>
   
<xsl:template match="stock">
  <tr>
    <td><xsl:value-of select="symbol"/></td>
    <td align="right"><xsl:value-of select="current"/></td>
    <td align="right"><xsl:value-of select="paid"/></td>
    <td align="right"><xsl:value-of select="qty"/></td>
    <td align="right" xsl:use-attribute-sets="gain-loss">
      <xsl:value-of 
            select="format-number((current - paid) * qty, '#,##0.00')"/>
    </td>
  </tr>
</xsl:template>     
   
<xsl:template match="property">
  <tr>
    <td><xsl:value-of select="address"/></td>
    <td align="right"><xsl:value-of select="paid"/></td>
    <td align="right"><xsl:value-of select="appraisal"/></td>
    <td align="right" xsl:use-attribute-sets="gain-loss">
      <xsl:value-of 
             select="format-number(appraisal - paid, '#,##0.00')"/>
    </td>
  </tr>
</xsl:template>     
   
</xsl:stylesheet>

The style czar can then decide how to render <td class="gain"> and <td class="loss"> by using portfolio.css, as shown in Example 10-1.

Example 10-1. portfolio.css
td.gain
{
     color:black;
}
   
td.loss
{
     color:red;
     font-weight:700;
}

10.6. Creating a Self-Contained HTML Transformation

Problem

You want to package XML data, as well as a stylesheet for converting it to HTML, into a single file.

Solution

This recipe assumes you have a browser that supports client-side XSLT transformations (IE 6.0 or later, also IE 5.x + MSXML 3.0, Netscape Navigator 6.1 or later, Mozilla, Firefox 1.0 or later, Apple Macintosh Safari (Tiger), etc.):

<?xml version="1.0" encoding="UTF-8"?>
   
<?xml-stylesheet type="application/xml" href="selfcontained.xsl"?>
   
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:pf="http://www.ora.com/XSLTCookbook/namespaces/portfolio">
   
<portfolio xmlns="http://www.ora.com/XSLTCookbook/namespaces/portfolio">
  <investment>
    <symbol>IBM</symbol>
    <current>72.70</current>
    <paid>65.00</paid>
    <qty>1000</qty>
  </investment>
  <investment>
    <symbol>JMAR</symbol>
    <current>1.90</current>
    <paid>5.10</paid>
    <qty>5000</qty>
  </investment>
  <investment>
    <symbol>DELL</symbol>
    <current>24.50</current>
    <paid>18.00</paid>
    <qty>100000</qty>
  </investment>
  <investment>
    <symbol>P</symbol>
    <current>57.33</current>
    <paid>63</paid>
    <qty>100</qty>
  </investment>
</portfolio>
     
<xsl:output method="html" />
   
  <xsl:attribute-set name="gain-loss-font">
    <xsl:attribute name="color">
      <xsl:choose>
        <xsl:when test="(pf:current - pf:paid) * pf:qty >= 0">black</xsl:when>
        <xsl:otherwise>red</xsl:otherwise>
      </xsl:choose>
    </xsl:attribute>
  </xsl:attribute-set>      
   
<xsl:template match="xsl:stylesheet">
               <xsl:apply-templates select="pf:portfolio"/>
               </xsl:template>
   
<xsl:template match="pf:portfolio">
    <html>
     <head>
      <title>My Portfolio</title>
     </head>
    
     <body bgcolor="#FFFFFF" text="#000000">
      <h1>Portfolio</h1>
      <table border="1" cellpadding="2">
        <tbody>
          <tr>
            <th>Symbol</th>
            <th>Current</th>
            <th>Paid</th>
            <th>Qty</th>
            <th>Gain/Loss</th>
          </tr>
          <xsl:apply-templates/>
        </tbody>
      </table>
     </body>
    </html>
</xsl:template>
   
<xsl:template match="pf:investment">
  <tr>
    <td><xsl:value-of select="pf:symbol"/></td>
    <td><xsl:value-of select="pf:current"/></td>
    <td><xsl:value-of select="pf:paid"/></td>
    <td><xsl:value-of select="pf:qty"/></td>
    <td><font xsl:use-attribute-sets="gain-loss-font"><xsl:value-of select="format-
number((pf:current - pf:paid) * pf:qty, '#,##0.00')"/></font></td>
  </tr>
</xsl:template>
   
</xsl:stylesheet>

Two components in this stylesheet make it work.

The first is the xml-stylesheet processing instruction, which tells the browser that the stylesheet associated with the document it loads is the very same document. You can refer to the same document as its stylesheet with href="" rather than specifying the name of the file, which is helpful if you ever rename it.

The second is the template that matches the xsl:stylesheet element and redirects stylesheet processing to the embedded XML data. In this case, the elements are in the http://www.ora.com/XSLTCookbook/namespaces/portfolio namespace.

Discussion

This recipe is somewhat of a trick to impress your friends. Intermixing content and styling, in some ways, goes against the spirit of the technology. However, delivering just a single file can be convenient, so you should not feel guilty about using this recipe if it suits your needs.

The official way to achieve these results is to embed the stylesheet in the document rather than vice versa. See http://www.w3.org/TR/xslt#section-Embedding-Stylesheets in the XSLT 1.0 spec or http://www.w3.org/TR/xslt20/#embedded in the XSLT 2.0 spec for more details. However, IE does not yet support embedded stylesheets, so this trick gets around the problem.

You can deliver content in this form without necessarily developing the content directly in this form. The following stylesheet merges a stylesheet and an XML file into the self-contained format. The only two criteria are that the XML must be in a namespace and the stylesheet should not begin processing at the root node (/):

<!-- generate-selfcontained.xslt -->
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
    xmlns:xso="dummy">
   
<!-- Reuse the identity transform -->
<xsl:import href="../util/copy.xslt"/>
   
<!-- This stylesheet will be generating stylesheet content 
     so use xso as alias for xsl -->
<xsl:namespace-alias stylesheet-prefix="xso" result-prefix="xsl"/>
   
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
   
<xsl:strip-space elements="*"/>
<!--Not a good idea to strip space from text nodes -->
<xsl:preserve-space elements="xsl:text"/>
   
<!--The name of the file containing xml data -->
<xsl:param name="datafile"/>
<!-- The name of the resulting output file -->
<xsl:param name="outfile"/>
   
<xsl:template match="/">
  <!-- Insert the processing instruction to tell the browser that
       $outfile is the stylesheet -->
  <xsl:processing-instruction name="xml-stylesheet">
   <xsl:text>type="application/xml" href="</xsl:text>
   <xsl:value-of select="$outfile"/>"<xsl:text/>  
  </xsl:processing-instruction>
   
 <xsl:apply-templates/>  
   
</xsl:template>
   
<xsl:template match="xsl:stylesheet">
   
  <xsl:copy>
    <xsl:copy-of select="@*"/>
    
    <xsl:apply-templates/>
   
     <!-- Generate the xslt that tells the  
    <xso:template match="xsl:stylesheet">
      <xso:apply-templates select="{name(document($datafile)/*)}"/>
    </xso:template>
   
  <!-- Insert the data -->
  <xsl:copy-of select="document($datafile)"/>
  
  </xsl:copy>
   
</xsl:template>
   
</xsl:stylesheet>

You can use this stylesheet to transform another stylesheet and its data into a self-contained HTML transformation. The source should be the stylesheet, and the $datafile is provided as a parameter. You need an additional parameter, $outfile, to allow correct generation of the xml-stylesheet processing instruction.

Using Saxon on the command line, the generation might be invoked as:

saxon -o self-contained.xsl pf-portfolio.xslt generate-selfcontained.xslt 
          datafile="pf-portfolio.xml" outfile="self-contained.xsl"

Where self-contained.xsl is the name of the resulting stylesheet and pf-portfolio.xslt is the stylesheet being merged with pf-portfolio.xml.

10.7. Populating a Form

Problem

You want to merge XML data into a predesigned form before delivering it to the client.

Solution

Provided you use XHTML or otherwise create well-formed HTML, you can use XSLT to merge an HTML document with data in an XML document. Here we will merge data in an XML document with a boilerplate HTML form. For example, imagine that your company’s web designer created a form used when online customers are ready to complete a purchase. The form needs to add sales tax, based on the state in the U.S. selected by the customers, when entering their billing addresses. Since the states your company must collect tax for and the tax rates themselves can change, hardcoding these rates into the form would not be a good idea. Instead, you could have the server merge the tax data into the form dynamically by using XSLT.

The sales tax data might be stored (or extracted from a database) like this:

<salesTax>
  <state>
    <name>AL</name>
    <tax>4</tax>
  </state>
  <state>
    <name>AK</name>
    <tax>0</tax>
  </state>
  <state>
    <name>AZ</name>
    <tax>5.6</tax>
  </state>
   
...
   
  <state>
    <name>WY</name>
    <tax>4</tax>
  </state>
</salesTax>

The boilerplate HTML form might look like this:

<html>
  <head>
   <title>Check Out</title>
   <script type="text/javascript" language="JavaScript">
   <!--
   /* Initialize tax for default state */
   setTax(document.customerInfo.billState)
           
   /*Recompute tax when state changes */
     function setTax(field) 
       {
      var value = new String(field.value) 
      var commaPos = value.indexOf(",") 
      var taxObj = document.customerInfo.tax
      var tax = value.substr(commaPos + 1)
      var subtotalObj = document.customerInfo.subtotal
      taxObj.value = tax
      document.customerInfo.total.value = 
          parseFloat(subtotalObj.value) + 
          (parseFloat(subtotalObj.value) * parseFloat(tax) / 100.00)
     }
   -->
   </script>
  </head>
   
<body bgcolor="#FFFFFF" text="#000000">
<h1>Check Out</h1>
<form name="customerInfo" method="post" action="">
  <table width="70%" border="0" cellspacing="3" cellpadding="3">
    <tr> 
      <td width="7%">&#xa0;</td>
      <td width="32%"> 
        <div align="center"><b>Shipping Address</b></div>
      </td>
      <td width="20%">&#xa0;</td>
      <td width="7%">&#xa0;</td>
      <td width="34%"> 
        <div align="center"><b>Billing Address</b></div>
      </td>
    </tr>
    <tr> 
      <td width="7%">Name</td>
      <td width="32%"> 
        <input type="text" name="shipName" maxlength="40" size="50" border="0"
        align="absmiddle"/>
      </td>
      <td width="20%">&#xa0;</td>
      <td width="7%">Name</td>
      <td width="34%"> 
        <input type="text" name="billName" maxlength="40" size="50" border="0" 
        align="absmiddle"/>
      </td>
    </tr>
    <tr> 
      <td width="7%">Address</td>
      <td width="32%"> 
        <input type="text" name="shipAddr" maxlength="40" size="50" border="0" 
        align="absmiddle"/>
      </td>
      <td width="20%">&#xa0;</td>
      <td width="7%">Address</td>
      <td width="34%"> 
        <input type="text" name="billAddr" maxlength="40" size="50" border="0"
        align="absmiddle"/>
      </td>
    </tr>
    <tr> 
      <td width="7%">City</td>
      <td width="32%"> 
        <input type="text" name="shipCity" maxlength="40" size="50" border="0" 
        align="absmiddle"/>
      </td>
      <td width="20%">&#xa0;</td>
      <td width="7%">City</td>
      <td width="34%"> 
        <input type="text" name="billCity" maxlength="40" size="50" border="0"
        align="absmiddle"/>
      </td>
    </tr>
    <tr> 
      <td width="7%">State</td>
      <td width="32%"> 
        <select name="shipState" size="1" align="absmiddle">
        </select>
      </td>
      <td width="20%">&#xa0;</td>
      <td width="7%">State</td>
      <td width="34%"> 
        <select name="billState" size="1" align="absmiddle" onChange="setTax(this)">
        </select>
      </td>
    </tr>
    <tr> 
      <td width="7%">Zip</td>
      <td width="32%"> 
        <input type="text" name="shipZip" maxlength="10" size="15" border="0"
        align="absmiddle"/>
      </td>
      <td width="20%">&#xa0;</td>
      <td width="7%">Zip</td>
      <td width="34%"> 
        <input type="text" name="billZip" maxlength="10" size="15" border="0" 
        align="absmiddle"/>
      </td>
    </tr>
    <tr> 
      <td width="7%">&#xa0;</td>
      <td width="32%">&#xa0;</td>
      <td width="20%">&#xa0;</td>
      <td width="7%">&#xa0;</td>
      <td width="34%">&#xa0;</td>
    </tr>
    <tr> 
      <td width="7%">&#xa0;</td>
      <td width="32%">&#xa0;</td>
      <td width="20%">&#xa0;</td>
      <td width="7%">Subtotal</td>
      <td width="34%">
        <input type="text" name="subtotal" readonly="1" value="100.00"/>
      </td>
    </tr>
    <tr> 
      <td width="7%">&#xa0;</td>
      <td width="32%">&#xa0;</td>
      <td width="20%">&#xa0;</td>
      <td width="7%">Tax</td>
      <td width="34%">
        <input type="text" name="tax" readonly="1"/>
      </td>
    </tr>
    <tr>
      <td width="7%">&#xa0;</td>
      <td width="32%">&#xa0;</td>
      <td width="20%">&#xa0;</td>
      <td width="7%">Total</td>
      <td width="34%">
        <input type="text" name="total" readonly="1"/>
      </td>
    </tr>
  </table>
</form>
</body>
</html>

The transformation is a merge where the default action is to copy the contents from the boilerplate HTML to the output (see Recipe 6.5). When select elements are encountered for the bill-to and ship-to states, the state data is inserted as option elements. To keep the example relatively simple, I encoded the state name and tax rate into the option element’s value attribute, but you might want to use a JavaScript-based lookup.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   
 <xsl:import href="../util/copy.xslt"/>
  
 <xsl:output method="html"/>
   
 <xsl:template match="html">
    <xsl:copy>
      <xsl:apply-templates/>
    </xsl:copy>     
 </xsl:template>
  
 <xsl:template match="select[@name='shipState' or @name='billState']">
    <xsl:copy>
      <xsl:copy-of select="@*"/>
      <xsl:for-each select="document('salesTax.xml')/salesTax/state">
        <option value="{name}',',{tax}">
          <xsl:value-of select="name"/>
        </option>
      </xsl:for-each>
    </xsl:copy>
 </xsl:template> 
   
</xsl:stylesheet>

Discussion

Clearly technologies other than XSLT (ASP and JSP come to mind) might be better suited for the particular example covered in this solution. However, two aspects of the example might lead you to favor an XSLT-based solution.

First, notice that the boilerplate HTML contains standard HTML; there is no embedded server-side or client-side scripting. There is a bit of client-side scripting, but that could be inserted by the XSLT transformation. The author of the boilerplate HTML needs to know nothing about the technology or methodology that will be used to populate the form when it is loaded from the server. In fact, the web page author need not have any programming skills whatsoever. Likewise, the programmer of the transformation need not have any HTML or graphic design skills.[1] Between them, they merely need to agree on content and naming conventions. Thus, a transformation-based solution to dynamic content provides true separation of concerns that is lacking in other techniques.

Second, a transformational approach can do far more than simply inject content; it can also subtract and rearrange existing content without polluting the HTML with foreign gobbledygook.

See Also

Time and space did not permit me to cover XForms in this book, but readers interested in this topic should definitely investigate this new technology (http://www.w3.org/MarkUp/Forms/). The W3C describes XForms as follows:

The current design of Web forms doesn’t separate the purpose from the presentation of a form. XForms, in contrast, are comprised of separate sections that describe what the form does, and how the form looks. This allows for flexible presentation options, including classic XHTML forms, to be attached to an XML form definition.

Key goals of XForms:

  • Support for handheld, television, and desktop browsers, plus printers and scanners

  • Richer user interface to meet the needs of business, consumer, and device control applications

  • Decoupled data, logic, and presentation

  • Improved internationalization

  • Support for structured form data

  • Advanced forms logic

  • Multiple forms per page, and pages per form

  • Suspend and resume support

  • Seamless integration with other XML tag sets



[1] This sentence pretty much describes me.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset