Example using XSLT in a Hunchentoot handler to deliver an HTML page

Overview

Suppose you're writing a web application (say, using Hunchentoot), and you would like to show directory contents.

We do that in two steps, strictly separating the programmer-written Lisp code from the XSLT stylesheet that a web designer might want to tweak afterwards:

Hunchentoot setup

The example requires hunchentoot and xuriella:

(asdf:operate 'asdf:load-op :hunchentoot)
(asdf:operate 'asdf:load-op :xuriella)

Let's start hunchentoot and register a handler for the example first:

(push (tbnl:create-prefix-dispatcher "/show-directory" 'show-directory)
      tbnl:*dispatch-table*)
(tbnl:start-server :port 4242)

Utility functions

Since we might want to write many different handlers using stylesheets, we factor the APPLY-STYLESHEET call out into a convenient macro WITH-STYLESHEET. Its body is expected to provide XML, which it will send through the stylesheet and return the result as a string.

Note the use of WITH-XML-OUTPUT and STP:MAKE-BUILDER to build the intermediate XML as an in-memory document using STP.

(In real-world code, we could optimize this a little by compiling the stylesheet ahead of time using PARSE-STYLESHEET, and building a cache out of stylesheet objects in a hash table somewhere.)

(defmacro with-stylesheet ((stylesheet-pathname) &body body)
  `(invoke-with-stylesheet (lambda () ,@body) ,stylesheet-pathname))

(defun invoke-with-stylesheet (fn stylesheet-pathname)
  (xuriella:apply-stylesheet (pathname stylesheet-pathname)
                             (cxml:with-xml-output (stp:make-builder)
                               (funcall fn))))

Building the temporary XML

Now for the handler calling DIRECTORY. We want our XML to look like this:

    <directory namestring="/home/jrhacker/">
      <file>hello-world.lisp</file>
      <file>mbox</file>
      ...
    </directory>
which we can generate easily using WITH-ELEMENT and DOLIST:

(defun show-directory ()
  (with-stylesheet ("directory.xsl")
    (cxml:with-element "directory"
      (let ((directory (user-homedir-pathname)))
	(cxml:attribute "namestring" (namestring directory))
	(dolist (file (directory (merge-pathnames "*.*" directory)))
	  (cxml:with-element "file"
	    (cxml:text (enough-namestring file directory))))))))
  

An XSL stylesheet as a template

Finally, the XSL stylesheet that turns this into HTML for us. Note the xsl:version on the root element, which marks the literal result element used as a stylesheet.

Since <html> is the root element, the stylesheet processor will turn on its HTML output method automatically, and generate HTML 4 rather than XML. (Powered by Closure HTML.)

To keep the example short and simple, our HTML is not very fancy.

<html xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
      xsl:version="1.0">
  <head>
    <title>
      <xsl:value-of select="/directory/@namestring"/>
    </title>
  </head>

  <body>
    <h1>
      Index of <xsl:value-of select="/directory/@namestring"/>
    </h1>
    
    <ul>
      <xsl:for-each select="/directory/file">
	<li>
	  <xsl:value-of select="."/>
	</li>
      </xsl:for-each>
    </ul>
  </body>
</html>

Try it!

That's it. If you open http://localhost:4242/show-directory in a browser, you should see a listing of your home directory.