(defmacro with-safe-io-syntax (&body body)
`(with-standard-io-syntax
(let ((*read-eval* nil))
,@body)))
Then the encoding and decoding functions are trivial.
(defun obj->base64 (obj)
(base64-encode (with-safe-io-syntax (write-to-string obj))))
(defun base64->obj (string)
(ignore-errors
(with-safe-io-syntax (read-from-string (base64-decode string)))))
Finally, you can use these functions to define a method on string->type
that defines the conversion for the query parameter type base64-list
.
(defmethod string->type ((type (eql 'base-64-list)) value)
(let ((obj (base64->obj value)))
(if (listp obj) obj nil)))
Boilerplate HTML
Next you need to define some HTML macros and helper functions to make it easy to give the different pages in the application a consistent look and feel. You can start with an HTML macro that defines the basic structure of a page in the application.
(define-html-macro :mp3-browser-page ((&key title (header title)) &body body)
`(:html
(:head
(:title ,title)
(:link :rel "stylesheet" :type "text/css" :href "mp3-browser.css"))
(:body
(standard-header)
(when ,header (html (:h1 :class "title" ,header)))
,@body
(standard-footer))))
You should define standard-header
and standard-footer
as separate functions for two reasons. First, during development you can redefine those functions and see the effect immediately without having to recompile functions that use the :mp3-browser-page
macro. Second, it turns out that one of the pages you'll write later won't be defined with :mp3-browser-page
but will still need the standard header and footers. They look like this:
(defparameter *r* 25)
(defun standard-header ()
(html
((:p :class "toolbar")
"[" (:a :href (link "/browse" :what "genre") "All genres") "] "
"[" (:a :href (link "/browse" :what "genre" :random *r*) "Random genres") "] "
"[" (:a :href (link "/browse" :what "artist") "All artists") "] "
"[" (:a :href (link "/browse" :what "artist" :random *r*) "Random artists") "] "
"[" (:a :href (link "/browse" :what "album") "All albums") "] "
"[" (:a :href (link "/browse" :what "album" :random *r*) "Random albums") "] "
"[" (:a :href (link "/browse" :what "song" :random *r*) "Random songs") "] "
"[" (:a :href (link "/playlist") "Playlist") "] "
"[" (:a :href (link "/all-playlists") "All playlists") "]")))
(defun standard-footer ()
(html (:hr) ((:p :class "footer") "MP3 Browser v" *major-version* "." *minor-version*)))
A couple of smaller HTML macros and helper functions automate other common patterns. The :table-row
HTML macro makes it easier to generate the HTML for a single row of a table. It uses a feature of FOO that I'll discuss in Chapter 31, an &attributes
parameter, which causes uses of the macro to be parsed just like normal s-expression HTML forms, with any attributes gathered into a list that will be bound to the &attributes
parameter. It looks like this:
(define-html-macro :table-row (&attributes attrs &rest values)
`(:tr ,@attrs ,@(loop for v in values collect `(:td ,v))))
And the link
function generates a URL back into the application to be used as the HREF
attribute with an A
element, building a query string out of a set of keyword/value pairs and making sure all special characters are properly escaped. For instance, instead of writing this:
(:a :href "browse?what=artist&genre=Rhythm+%26+Blues" "Artists")
you can write the following:
(:a :href (link "browse" :what "artist" :genre "Rhythm & Blues") "Artists")
It looks like this:
(defun link (target &rest attributes)
(html
(:attribute
(:format "~a~@[?~{~(~a~)=~a~^&~}~]" target (mapcar #'urlencode attributes)))))
To URL encode the keys and values, you use the helper function urlencode
, which is a wrapper around the function encode-form-urlencoded
, which is a nonpublic function from AllegroServe. This is—on one hand—bad form; since the name encode-form-urlencoded
isn't exported from NET.ASERVE
, it's possible that encode-form-urlencoded
may go away or get renamed out from under you. On the other hand, using this unexported symbol for the time being lets you get work done for the moment; by wrapping encode-form-urlencoded
in your own function, you isolate the crufty code to one function, which you could rewrite if you had to.
(defun urlencode (string)
(net.aserve::encode-form-urlencoded string))
Finally, you need the CSS style sheet mp3-browser.css
used by :mp3-browser-page
. Since there's nothing dynamic about it, it's probably easiest to just publish a static file with publish-file
.
(publish-file :path "/mp3-browser.css" :file filename :content-type "text/css")