5.2 A Brief XSLT Cookbook

     

As I mentioned in the introduction, the goal of this chapter is to provide just enough information to allow you to be productive as quickly as possible with XSLT and AxKit. To that end, we will complete our whirlwind tour by looking at how XSLT can be used for several tasks that web developers are commonly asked to perform.

5.2.1 Delivering Browser-Friendly HTML

5.2.1.1 Problem

Your stylesheets work fine, but older HTML browsers are choking on tags such as <br/> and <img/> .

5.2.1.2 Solution

Use the xsl:output element and set its method attribute to "html":

 <?xml version="1.0"?> <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> <xsl:output method="html" />   . . . 

5.2.1.3 Discussion

The xsl:output element offers an easy way to control the formatting of the result of a given transformation. Other valid values for the method attribute include "text", and "xml" (the default). This element offers several other useful options, including the ability to set the encoding of the result document (via the encoding attribute) and the ability to add a document type declaration to the output (using the doctype-system and doctype-public attributes).

5.2.2 Alternating Colors in HTML Table Rows

5.2.2.1 Problem

You are transforming a document that consists of a long list of line items into HTML. You want to make the background of every other row a different color so that the page is more readable.

5.2.2.2 Solution

Use an xsl:if element that tests the value of the current node's position and conditionally adds the proper attribute to the output:

 <xsl:template match="recordset/item">   <tr>     <xsl:if test="position( ) mod 2 != 1">       <xsl:attribute name="bgcolor">#eeeeee</xsl:attribute>     </xsl:if>     <xsl:apply-templates/>   </tr> </xsl:template> 

5.2.2.3 Discussion

Here, a combination of the position( ) function and the mod operator are used to test whether to add the bgcolor attribute to the surrounding table row. The expression position( ) mod 2 != 1 evaluates to true only for nodes in odd-numbered positions in the larger nodeset, so the bgcolor attribute is added only to every other row (starting with the first row).

5.2.3 Using XSLT Parameters with POST and GET Data

5.2.3.1 Problem

You want to make your stylesheets more dynamic by being able to access the POST and GET parameters that are part of a given HTTP request.

5.2.3.2 Solution

Use an xsl:param element and use the name of the desired field as the name of the parameter:

 <xsl:param name="my-url-param"/> 

5.2.3.3 Discussion

Unless explicitly configured to behave otherwise , all HTTP POST and GET parameters are available from within your stylesheets via top-level xsl:param elements. You only need to declare a top-level xsl:param whose name attribute corresponds to the name of the request field.

If you have caching turned on for documents being transformed by a stylesheet that uses a parameter extracted from the query string, be sure to add the following line to your .htaccess or other configuration file:

 AxAddPlugin Apache::AxKit::Plugin::QueryStringCache 

This plug-in makes AxKit's internal caching mechanism smarter with respect to query string parameters. More cache files are created on the disk (and therefore take more space), but it allows you to safely use the values of query string parameters inside your stylesheet without serving stale data or having to turn caching off altogether.

5.2.4 Creating a "Breadcrumb" Navigation Bar

5.2.4.1 Problem

You want to add context-sensitive navigation that allows visitors to easily climb the document hierarchy from the current folder (often called a breadcrumb bar ).

5.2.4.2 Solution

Use AxKit's AddXSLParams::Request plug-in to pass the URL path of the source XML document to your XSLT stylesheet as an xsl:param and process that string into the desired HTML hyperlinks :

 <?xml version="1.0"?> <xsl:stylesheet       xmlns:xsl="http://www.w3.org/1999/XSL/Transform"      version="1.0"> <xsl:param name="request.uri"/>   <xsl:variable name="breadcrumb-delimiter">   <xsl:text> :: </xsl:text> </xsl:variable> <xsl:template name="breadcrumb-nav"> <xsl:param name="url"/> <xsl:param name="step-url"/> <xsl:param name="links"/> <xsl:choose>   <xsl:when test="contains($url, '/')">     <xsl:variable name="step">       <xsl:choose>         <xsl:when test="starts-with($url, '/')">           <xsl:value-of select="'home'"/>         </xsl:when>         <xsl:otherwise>             <xsl:value-of select="substring-before( $url, '/')"/>         </xsl:otherwise>       </xsl:choose>     </xsl:variable>          <xsl:variable name="local-url">       <xsl:choose>         <xsl:when test="$step='home'">              <xsl:value-of select="'/'"/>         </xsl:when>         <xsl:otherwise>           <xsl:value-of select="concat( $step-url, $step, '/')"/>         </xsl:otherwise>       </xsl:choose>     </xsl:variable>          <!-- call this template again to process the rest of the url -->     <xsl:call-template name="breadcrumb-nav">       <xsl:with-param name="url">         <xsl:value-of select="substring-after( $url, '/')"/>       </xsl:with-param>       <xsl:with-param name="step-url">         <xsl:value-of select="$local-url"/>         </xsl:with-param>       <xsl:with-param name="links">         <xsl:copy-of select="$links"/>         <xsl:value-of select="$breadcrumb-delimiter"/>         <a href="{$local-url}"><xsl:value-of select="$step"/></a>       </xsl:with-param>     </xsl:call-template>          </xsl:when>   <!-- if you make it to here, all the url steps        have been extracted. cover the last case        and return the result -->            <xsl:otherwise>     <xsl:copy-of select="$links"/>     <xsl:value-of select="$breadcrumb-delimiter"/>     <a href="{concat($step-url, $url)}" >       <xsl:value-of select="$url"/>     </a>   </xsl:otherwise>         </xsl:choose> </xsl:template> </xsl:stylesheet> 

5.2.4.3 Discussion

AxKit makes all form and query parameters available to the XSLT stylesheet via top-level xsl:param elements by default. This is not the only data that can be accessed in this way. AxKit's plug-in modules can pass any simple key/value pair as a stylesheet parameter. You learn more about this in Custom Plug-ins in Chapter 8, but in this case, you do not need a no custom plug-in; you can simply use the existing Apache::AxKit::Plugin::AddXSLParams::Request plug-in to pass the URI of the content source into your stylesheet. When added to the processing chain, this verbosely named class provides the ability to select sets of commonly used request and server information (HTTP headers, cookie data server hostnames, etc.) and makes them available as XSLT parameters. We will not dig into details of this plug-in's parameter groups or their naming conventions (see the installed documentation for more information), but suffice it to say, as with form and query string data, if the name of one of this plug-in's selected fields matches the name attribute of a top-level xsl:para element in your stylesheet, then the data is made available via that parameter. To access the request URI as an XSLT parameter, you need to first add and configure the plug-in via one of the web server's configuration files:

 # Add the plug-in to the processing chain AxAddPlugin Apache::AxKit::Plugin::AddXSLParams::Request # Tell the plugin which group of data you are interested in PerlSetVar AxAddXSLParamGroups "Request-Common" 

With this in place, you can access the URI fron the current request using the following top-level parameter in your stylesheet:

 <xsl:param name="request.uri"/> 

Now that you can access the URL, you still need to process that string to generate the breadcrumb bar for the top of your pages. To do this, create a named template that recursively consumes the URL string and produces a series of HTML links separated by the text string defined by the $breadcrumb-delimiter variable.

A stylesheet such as the one in this example is a good candidate for inclusion in a reusable library of common templates. With that stylesheet imported, you can simply call the breadcrumb-nav template (passing the URI parameter as an argument) at the location in the page that you want the breadcrumb bar to appear. For example, the following inserts the link bar into its own div element at the top of the result's body element:

 <?xml version="1.0"?> <xsl:stylesheet       xmlns:xsl="http://www.w3.org/1999/XSL/Transform"      version="1.0">  <xsl:import href="/style/common/breadcrumb.xsl"/>  <xsl:template match="article"> <html>   <body>     <div class="breadcrumb">  <xsl:call-template name="breadcrumb-nav">   <xsl:with-param name="url">   <xsl:value-of select="$request.uri"/>   </xsl:with-param>   </xsl:call-template>  </div>     <!-- main content continues.. -->   </body> </html> </xsl:template> 

With the template above as a part of your stylesheet, the resulting HTML generated from this snippet may look something like this:

 <html>   <body>   <div class="breadcrumb">     <a href="/">home</a> :: <a href="/samples/">samples</a> ::      <a href="/samples/mypage.xml">mypage.xml</a>   </div>   <!-- main content continues.. -->   </body> </html> 

The breadcrumb-nav template that does the real work is fairly verbose, and truly, you may have slimmed it down a bit by using an extension function. It does, however, illustrate the functional recursive template technique introduced earlier in this chapter in "Parameters and Variables." It underscores the fact that some tasks, most notably up-translating text strings into markup, are tricky but possible.

5.2.5 Passing Markup Through Untransformed

5.2.5.1 Problem

You have certain elements in your source documents that you want to appear untransformed in the result of a stylesheet transformation.

5.2.5.2 Solution

Create a template to match the nodes you want to copy, and use xsl:copy and xsl:copy-of .

Pass through a copy of the selected nodes, including their attributes and text children without including any descendant elements:

 <xsl:template match="para">   <xsl:copy>     <xsl:copy-of select="@*text( )"/>   </xsl:copy> </xsl:template> 

Copy the selected nodes and all of their descendants as is:

 <xsl:template match="article/articleinfo">   <xsl:copy-of select="."/> </xsl:template> 

Copy the selected nodes, and pass their descendants on for further processing:

 <xsl:template match="article/articleinfo">    <xsl:copy>      <xsl:copy-of select="@*"/>      <xsl:apply-templates />    </xsl:copy> </xsl:template> 

5.2.5.3 Discussion

The significant difference between xsl:copy and xsl:copy-of is that the former creates a shallow copy of the node being processed , while the latter creates a deep copy that includes all attributes, text nodes, and descendant elements. The xsl:copy element offers finer control over exactly which parts of a node are copied, but puts the onus on the stylesheet author to explicitly descend into the nodeset's structure to extract the parts that are to be copied into the result. The xsl:copy-of makes it easy to include complex nodesets in the result but relies on the fact you do not want to process any descendant nodes contained by that nodeset.

As a general rule, use xsl:copy-of when you want to include the entire nodeset without examining its contents, and a combination of xsl:copy and xsl:copy-of with xsl:apply-templates when the nodeset may contain other elements that you want to transform.

The following passes a defined subset of HTML elements through the stylesheet untransformed, while processing any non-HTML descendants according to the other templates contained in the stylesheet:

 <xsl:template match="pibfontemblockquotespanprebrdivulolli                      imgscripthtmlheadmetaa                      tabletrtdth                      forminputselectoptiontextarea                      embed">   <xsl:copy>     <xsl:copy-of select="@*"/>     <xsl:apply-templates />   </xsl:copy> </xsl:template> 

Rather than listing all the elements by name as you do here, it's far more common to use what is popularly called an identity transformation template that copies through all nodes that do not have a more specific template that matches. This offers a way to operate on only the nodes that you need to, while passing the rest through undisturbed:

 <xsl:template match="node( )@*">     <xsl:copy>       <xsl:apply-templates select="@*node( )" />     </xsl:copy>   </xsl:template> 

The template is rather common in processing chains in which an upstream stylesheet may have generated HTML (or whatever the client expects), and the last stylesheet in the chain wants to add a common header and footer to the body element while passing the rest of the content through, as is.

5.2.6 Including External Documents

5.2.6.1 Problem

You want to include all or part of another XML document in the result of an XSLT transformation.

5.2.6.2 Solution

Use the XSLT document( ) function.

Include the entire document as is:

 <xsl:copy-of select="document('include.xml')"/> 

Process the contents of the bookinfo element in the included document with the template rules in the current stylesheet:

 <xsl:apply-templates select="document('book.xml')/bookinfo/*"/> 

5.2.6.3 Discussion

XSLT's document( ) function offers a flexible mechanism for including all or some nodes in a separate XML document in the result of a transformation. It is especially useful for including common site elements such as headers, footers, and global navigation. The result of a call to document( ) is a nodeset containing all or some content of the external file. The resulting nodeset can be assigned to an xsl:variable and reused within the stylesheet:

 <xsl:variable name="common_nav" select="document('sitenav.xml')"/> <xsl:apply-templates select="$common_nav/*" mode="rightnav"/>   . . .  <xsl:apply-templates select="$common_nav/*" mode="footerenav"/> 

You can use the document( ) function for much more than adding chunks of common boilerplate . For example, you can include details about the products in a customer's order by looking up the product details in a separate file:

 <xsl:template match="/">   <xsl:variable name="item_details"                 select="document('productdetail.xml')"/>   <xsl:for-each select="order/item">     <xsl:variable name="lookup">       <xsl:value-of select="@product_id"/>     </xsl:variable>     <item product_id="{$lookup}">       <xsl:copy-of select="$item_details/productlist/item[@product_id=$lookup]/*"/>     </item>   </xsl:for-each>    . . .  </xsl:template> 

The way the document( ) function treats relative URIs often trips the unwary. Stylesheet authors typically expect the file path to be evaluated in the context of the document being transformedit is not. It is processed in the context of the stylesheet, not the source document. When you type select="document('header.xml') ", remember that the XSLT processor looks in the same directory as the stylesheet for the file header.xml .

You can alter this default behavior, however, by using the two-argument form of document( ) . In the two-argument form, the second argument is a nodeset. The URI of the source document that originally contained that nodeset is used to set context in which any relative path contained by the first argument is evaluated.

To resolve a relative file path in the context of the source document, rather than the stylesheet, use:

 <xsl:copy-of select="document('widgets/footer.xml', /)"/> 

This includes the contents of the file footer.xml from the directory widgets in the same directory as the source document. It works because the second argument (/) represents the root of the document currently being transformed. Therefore, the relative path contained in the first argument is resolved in the context of that URI.

5.2.7 Dividing Large Datasets into Pages

5.2.7.1 Problem

You have a large XML source document containing many records. You want to display only a few at a time.

5.2.7.2 Solution

Use the XPath position( ) function to select a smaller subset of the records stored in the document.

Use position( ) in the select expression of an xsl:for-each loop:

 <xsl:template match="/"> <recordset>   <xsl:for-each select="recordset/record[position ( ) >= $start and position( )  <= $end]">     <xsl:copy-of select="."/>   </xsl:for-each> </recordset> 

5.2.7.3 Discussion

Displaying a limited set of all records contained in a large document ( pagination ) is one of the most common XML publishing tasks. Selecting a given set of records is as easy as selecting the proper element nodes whose position falls within the desired range.

Using top-level xsl:param elements that allow the range and context to be passed dynamically to the stylesheet via the query string makes navigating large documents fairly straightforward. Example 5-1 demonstrates one way to navigate a large set of records using the type of interface popularized by major web search engines.

Example 5-1. An XSLT stylesheet for paginating large records
 <?xml version="1.0"?> <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"                 version="1.0"> <xsl:output method="html" /> <xsl:param name="start">1</xsl:param> <xsl:param name="perpage">10</xsl:param> <xsl:variable name="totalitems" select="count(//item)"/> <xsl:variable name="end">   <xsl:choose>     <xsl:when test="($start + $perpage) > $totalitems">       <xsl:value-of select="$totalitems"/>     </xsl:when>     <xsl:otherwise>       <xsl:value-of select="$start + $perpage - 1"/>     </xsl:otherwise>   </xsl:choose> </xsl:variable> <!-- begin root template --> <xsl:template match="/">   <h3>     Showing records     <xsl:value-of select="$start"/> - <xsl:value-of select="$end"/> of     <xsl:value-of select="$totalitems"/>   </h3>   <table border="0" cellpadding="0" cellspacing="0">     <tr><th>Product ID</th><th>Description</th><th>Price</th></tr>     <xsl:for-each select="/order/item[position( ) >= $start and position( ) <= $end]">       <tr>         <!-- make every other row gray -->         <xsl:if test="position( ) mod 2 != 1">           <xsl:attribute name="bgcolor">eeeeee</xsl:attribute>         </xsl:if>         <td><xsl:value-of select="@product-id"/></td>         <td><xsl:value-of select="name"/></td>         <td><xsl:value-of select="price"/></td>       </tr>     </xsl:for-each>   </table>   <!-- if there are records before the block you are viewing, provide a 'prev.' link -->   <xsl:if test="$start > 1">     <a href="paginate.xml?start={$start - $perpage};perpage={$perpage}">prev.</a>   </xsl:if>   <!-- process *all* the <item> elements in the document to        build the navbar -->   <xsl:apply-templates select="/order/item"/>   <!-- if there are more records, provide a 'next' link -->   <xsl:if test="$totalitems > $end">     <a href="paginate.xml?start={$end + 1};perpage={$perpage}">next</a>   </xsl:if> </xsl:template> <!-- end root template --> <!-- the 'item' template that builds the numbered navbar links --> <xsl:template match="item">   <xsl:if test="position( ) mod $perpage = 1 or $perpage = 1">     <xsl:variable name="pagenum">       <xsl:value-of select="ceiling(position( ) div $perpage)"/>     </xsl:variable>     <a href="paginate.xml?start={position( )};perpage={$perpage}">       <xsl:value-of select="$pagenum"/>       <!-- force whitespace in between the numbered links -->       <xsl:text> </xsl:text>     </a>   </xsl:if> </xsl:template> </xsl:stylesheet> 

Remember, XSLT is not, nor does it pretend to be, a general-purpose programming language. If the XML documents you are publishing require a significant amount of processing that is beyond the reach of XSLT, or if you are bending over backwards too often to get XSLT to do what you need, consider using XPathScript to transform your documents. Similar to XSLT in many respects, XPathScript adds the ability to process the data contained in your documents using Perl and its myriad functions and modules.



XML Publishing with AxKit
XML Publishing with Axkit
ISBN: 0596002165
EAN: 2147483647
Year: 2003
Pages: 109
Authors: Kip Hampton

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net