Custom Search

Friday, February 12, 2010

bitchin web framework - templates

One of the advantages of making the templates functions is that clojure  does almost all the work. Tags basically come in two flavors, depending on whether they render newlines between the elements of their contents to make the resulting HTML more readable. So we could define a tag like so:
(defn html
([] (format "<%s />" "html"))
([attribs & args]
(if (associative? attribs)
(str "<" "html" (make-attrib-strings attribs)
(if (nil? args)
" />"
(format ">%s%s%s</%s>" \n (str-join \n args) \n "html")))
(apply html (concat [{} attribs] args)))))
Where make-attrib-strings handles the attribute map, and looks like:
(defn- make-attrib-strings [attrs]
(apply str (map #(format " %s=%s" (if (keyword? %) (name %) %)
(quoted-attribute (attrs %)))
(keys attrs))))

(defn quoted-attribute [input]
(format "\"%s\""
(apply str (replace {\& "&amp;", \" "&quot;", \> "&gt;"} (str input)))))
These two functions are fairly straightforward. quoted-attrib takes a string, and returns it with the characters that are dangerous inside of attributes - '"', '&' and '>' (the latter only for very old browsers, but it's easy to fix as well) and returns a string with them replaced by the appropriate XHTML entities. We have to process the output through str, as the result of replace is a sequence. Likewise, we pass the input through str, just in case they gave us a symbol (which we will use later). make-attrib-string is passed an associative object, and maps the keys through a function that turns it into a stringo f the form key="value", with value being quoted by quoted-attribute. It does check to see if the key is a keyword, and if so uses it's name, so that users can use :href instead of 'href. Again, we pass the results to str to turn it into a string for output.

The problem with the above approach is that it requires writing out that rather long function for every tag we want to use. Fortunately, clojure provides us with macros, so we can just write a macro to do all that for us, like so:
(defmacro make-tags
([sep name & names]
`(do
(make-tags ~sep ~name)
(make-tags ~sep ~@names)))
([sep name]
(let [name# (first (re-split #"_" (str name)))]
`(defn ~name
([] (format "<%s />" ~name#))
([attribs# & args#]
(if (associative? attribs#)
(str "<" ~name# (make-attrib-strings attribs#)
(if (nil? args#)
" />"
(format ">%s%s%s</%s>"
~sep (str-join ~sep args#) ~sep ~name#)))
(apply ~name (concat [{} attribs#] args#))))))))
The first clause is a standard clojure idiom - we want to allow multiple tag names, so we handle the case with more than one name by calling the macro with one name, and then again with the rest of the names.

The second clause is the code from the html function above, with the
string or symbol html taken out, and replaced by an
expression involving name. With one hiccup - some of the html tag names are core clojure function names, so we can't use them. Instead, we use map_ and meta_, and extract the string into the symbol name#. The # causes clojure to generate a unique symbol for this binding, so we don't have to worry about name collisions. The rest of the macro is the function from above, with ~name or ~name# (the ~ causing evaluation) replacing what was html, and ~sep.

So now we can put that all together, along with two invocations of the macro to create all the tags we normally use. And of course, the user gets the make-tags function if they want to use custom tags of some kind or another.
(ns org.mired.bitchin.html
(:use [clojure.contrib.str-utils :only (str-join re-split)]))

(defmacro make-tags
([sep name & names]
`(do
(make-tags ~sep ~name)
(make-tags ~sep ~@names)))
([sep name]
(let [name# (first (re-split #"_" (str name)))]
`(defn ~name
([] (format "<%s />" ~name#))
([attribs# & args#]
(if (associative? attribs#)
(str "<" ~name# (make-attrib-strings attribs#)
(if (nil? args#)
" />"
(format ">%s%s%s</%s>"
~sep (str-join ~sep args#) ~sep ~name#)))
(apply ~name (concat [{} attribs#] args#))))))))

(defn quoted-attribute [input]
(format "\"%s\""
(apply str (replace {\& "&amp;", \" "&quot;", \> "&gt;"} (str input)))))

(defn- make-attrib-strings [attrs]
(apply str (map #(format " %s=%s" (if (keyword? %) (name %) %)
(quoted-attribute (attrs %)))
(keys attrs))))


;; The standard html tags with newlines between element in the contents
(make-tags "\n" html head meta_ base link script style body div dl ol ul li table
tr colgroup col thead tbody tfoot form select input)

;; And now the same, with nothing between elements in the contents.
(make-tags "" title h1 h2 h3 h4 h5 h6 p pre address blockquote hr dd dt th td
caption option label textarea button a b i big small strong em q
tt a cite dfn abbr acronym code samp kbd var sub del ins font span img
br map_ area object param embed noembed)
As expected, the tests had to be debugged as well. Mostly, it was spacing inside of tags. However, the design changed just slightly - the template system doesn't automatically quote characters in content strings (but it does in attribute strings). The recursive nature of the invocations makes it hard - if not impossible - to keep track of what has and has not been quoted, and quoting a second time would be a failure. This may be a reason to change from a code representation to a vector representation, if the problem can be solved.