Practical HTML generation in Racket servlets with txexpr

You want to generate a simple piece of HTML for your Racket web app, but the existing options don't seem to quite cut it, or behave curiously. Why is generating HTML in Racket like pulling teeth? Do we sharpen our razor and hunker down for some some custom HTML yak shaving? A common (though not the only) approach to working with HTML in the context of Racket web servlets is to use X-expressions AKA xexprs. The X in X-expression is for XML. Even if you're well aware of the differences between XML and HTML and are sensitive to the different needs that they address, from a structural point of view, the two are so similar that it would be a shame to throw out xexprs and decide that you need to roll your own HTML generator. If you're in the xexpr camp and want to know how to generate HTML, take a moment to learn about txexpr and, especially, txexpr's xexpr->html function. Chances are good that that will give you the HTML you're after.

txexpr: X-expressions for the rest of us

Matthew Butterick's txexpr library is, in my experience, a really useful tool for working with HTML in Racket. txexpr is short for tagged X-expression. I frequently use txexpr in my Racket sites and books (xexprs are the bread and butter of Pollen). (A couple of examples are included below.) I might even go so far as to say that txexpr is perhaps the best way to generate HTML for Racket, though I also use and can recommend Scribble for this purpose. (That's another topic.) The txexpr library offers a few useful functions for generating so-called tagged X-expressions (whence the t in txexpr). The main functions are txexpr and txexpr*. One gives the name of an element, a list of attributes, and the children, which are themselves xexprs. The value is an xexpr. The last point is important. Tagged X-expressions are X-expressions. To be more precise: the values returned by txexpr and txexpr* are X-expressions. You can combine txexpr with other utilities for working with xexprs. In other words, by adopting txexpr in your work, you're not entering a walled garden containing its own offshoot variety of XML. You're in the standard Racket camp.


Here's a real-life example how I use txexpr in my servlets:
(define (section #:title [title #f] . elements)
  (txexpr 'section
          (cond ((string? title)
                 (list (list 'title title)))
I use this to create a section element, possibly with a title (which later gets turned into a suitable h1, h2, h3, etc.). Here's an example of txexpr* and txexpr being mixed:
(define-tag-function (explanation-row attrs elements)
  (define field (find-elem 'field elements))
  (define type (find-elem 'type elements))
  (define explanation (find-elem 'explanation elements))
  (txexpr* 'tr
           (txexpr* 'td
                    (txexpr 'code
                             (get-elements field))
                    (txexpr* 'br)
                    (txexpr 'em
                            (get-elements type)))
           (txexpr 'td
                    (get-elements explanation))))
Here I'm building an HTML table row (tr) having a certain pre-specified format (there are exactly two columns). (In this snippet, it doesn't matter what find-elem is (it's some private function for my own use). All one needs to know is that it returns an xexpr.) Notice that txexpr* allows an arbitrary number of child node arguments (and, in an extreme case of generating an empty br element, even allows no arguments at all apart from the element name). In this example, I'm also using the get-elements to extract the children of a tagged X-expression.

“But I want HTML, not XML.”

It's true that when you're building up, breaking down, and rearranging things using the txexpr functions, you are, in fact, building up, breaking down, and rearranging X-expressions. That is, you're doing XML work. But we're not after XML; we're trying to get HTML. How, then, is txexpr going to help? For the proposes of generating HTML, the most useful function that txexpr offers is xexpr->html. Despite the name, this function resides in the txexpr package. The main thing that xexpr->html respects that xexpr->string does not is the handling of a couple of HTML-y exceptions: the script and style elements. For these, the intended use is that they are raw character data (CDATA), from the XML point of view, even though one does not explicitly wrap them in a CDATA. For XML, there's nothing special about script and style, so Racket's xexpr->string is happy to escape all the text within the script and style elements. For HTML, though, these are to be treated differently.