Amateur Topologist

Everything but topology.

Writing Heist Splices for Snap

I’ve been doing a lot of web stuff lately; so far, it’s only been very simple HTML + CSS + JS (well, CoffeeScript, but who’s counting), but eventually I might move on to other, neater things. And in the process one of the things that I found is Twitter Bootstrap; it’s a bunch of CSS that makes it ‘easy’ to make professional-looking sites. I put ‘easy’ in quotes because the learning curve is definitely steeper than that of CSS itself, because you have to nest your DOM just so in order for the given CSS to apply right. But you get nice margins, good color themes, etc.

One of the things that you get when you use Bootstrap is a very nice-looking nav bar, which is actually just a very well-themed unordered list of links.

In particular, if you set the class of one of the list items to “active”, it ‘highlights’ it:

Note the highlight on 'Scalemate generator'

Now, I’m serving this site through Snap, the Haskell webdev framework; I’m doing this because one of my subsites needs to be able to upload files, and I figured I might as well use Haskell. And Snap comes with a templating language called Heist. So, given that I’m already using templating to insert the boilerplate jQuery/Bootstrap includes and the GitHub link/authorship at the bottom, wouldn’t it make sense to template the nav links?

It turns out that it’s fairly simple to do, as illustrated in NavLinks.hs. We start with imports; nothing too interesting here. We need Heist to access the templating engine and Text.XmlHtml to actually construct the nodes.

1
2
3
4
5
6
7
8
9
10
11
12
{-# LANGUAGE OverloadedStrings #-}

module NavLinks where

import qualified Data.ByteString as B
import qualified Data.Text as T
import           Data.Text.Encoding

import           Snap

import           Text.Templating.Heist
import qualified Text.XmlHtml as X

Next we declare the type:

17
navSplice :: MonadSnap m => [(B.ByteString, T.Text)] -> Splice m

The way a splice works is this: you construct a Splice in some monad Splice m, and you can lift monad actions in the m monad into Splice m. In this case, we can be in any monad that provides us with getRequest, since we want to get the URI of the request. Since getRequest :: MonadSnap m => Request, we can be in any monad that is an instance of MonadSnap. The argument to navSplice is the list of (path, title) tuples; for example, it might look like [("/about", "About"), ("/blog", "Blog")]. The path is a ByteString because the request gives us the URI as a ByteString, and the title is Text because Text.XmlHtml uses Text. Now, onto the actual declaration:

18
19
20
21
22
23
24
25
26
navSplice links = (:[]) . X.Element "ul" [("class", "nav")] <$> do
  currentURI <- lift $ rqURI <$> getRequest
  -- add the 'active' class if the href is a prefix of the current URI
  let li path
        | path `B.isPrefixOf` (currentURI `B.append` "/") =  X.Element "li" [("class", "active")]
        | otherwise = X.Element "li" []
  return $ map (\(path, title) -> li path [buildLink path title]) links
   -- build a link to the path with the given text
    where buildLink path title = X.Element "a" [("href", decodeUtf8 path)] [X.TextNode title]

Look at the do-block first. Line 19 just gets the current URI out. Lines 21-23 are interesting; what we’re doing here is declaring a function of type li :: B.ByteString -> [X.Node] -> X.Node. The way an HTML element is constructed in Text.XmlHtml is through the Element constructor, which has type T.Text -> [(T.Text, T.Text)] -> [X.Node] -> X.Node. The first Text is the tag; the array of Text tuples is the list of attributes, and the list of Nodes is the list of children. So, here we’re partially applying the tag (always “li”) and the attributes (which is either empty or class="active"). We apply the “active” class only if the path is a prefix of the current URI; we append a “/” to the end of the current URI to make sure that trailing slashes are ignored.

Next, look at line 26; buildLink just builds a link node looking like <a href="path">title</a>. X.TextNode is, obviously, the constructor for text Nodes.

On the last line of the do block, we map over the (path, title) tuples, build up a list of <li> elements, and return that list. Finally, on line 18 we construct an unordered list element with the “nav” class. Finally, we construct a single-element list using (:[]) (which some people call the monkey function), which just constructs a list with its argument inside. My actual code uses return instead, but I decided that this would be clearer.

Adding that splice to the default list of splices is as simple as adding

pages :: [(B.ByteString, T.Text)]
pages = [ ("/chargen/", "Character generator")
        , ("/logify/", "Chatlog formatter")
        , ("/scalemate/", "Scalemate generator")
        ]

to the toplevel of Site.hs and adding

addSplices [("nav", liftHeist $ navSplice pages)]

to the app initializer. Now <nav/> can be used in the template to automatically insert the nav link list and highlight the current page!

One small annoyance of this is that the generated HTML is all in one line, although since it’s not handwritten you probably won’t care so much, especially since Firebug and Chrome’s debugging tools will automatically do the indentation for you and let you collapse/expand child nodes at will.

2 ResponsesLeave one →

  1. Simon

     /  February 12, 2012

    Your idea is cool and I’m able to incorporate into my toy program.

    When the path is “/” which usually the index page, this guard always be true thus it is always being highlighted.
    path `B.isPrefixOf` (currentURI `B.append` “/”)

    Reply
  2. I would like to buy 706992 suns please for 230863

    Reply

Leave a Reply