Learning Lisp Through Examples: XHTML Generation

Posted on August 11, 2008
Filed Under Programming, Programming Languages, Software Development |

I’ve learned a subset of Common Lisp that is large enough to start writing programs. This uncovered a small problem: how do I know I’m doing it right? Lisp is an old language, and there are ancient conventions and standards written deep in the caverns of MIT.

Fortunately for me, the internet is best used to tell people they are wrong. I’ll show you my code, and you tell me when I’m doing it wrong. Deal?

For the complete source code listing, click here. I don’t have any public version control set up to date, so it’s just a static file.

If you are a Lisp power user, this will be horribly boring. I’m just looking to see where I make mistakes.

Disclosure: I did not write unit tests for this project, and I’m aware that this constitutes “doing it wrong.” I recently discovered “lisp-unit“, and will be rectifying the situation.

I also make no effort to validate or encode user input strings.

XHTML Syntax

The “XHTML” referred to will be considered Transitional XHTML, since tags like <center> are not explicitly disallowed.

Basically, I wanted the program to convert something like this:

'(html
 (head
  (title "Greetings, Terran orb!"))
 (body
  (img "src" "./img/some_image.jp2")
  (p (** "margin" "1em") "Some paragraph")))

into this:

<html>
 <head>
  <title>
   "Greetings, Terran orb!"
  </title>
 </head>
 <body>
  <img src="./img/some_image.jp2"/>
  <p margin="1em">
   Some paragraph
  </p>
 </body>
</html>

There will be a separate function to generate the DOCTYPE declaration.

The biggest eyesore here is the (**) tags used for attributes. However, I don’t use very many attributes in my coding, so it’s easier for me to specify when I have an attribute than to use the following syntax:

(tag attributes inner-stuff)

and have my code scattered with more nil than you can shake a stick at.

A Summary of my Development

Based on memory and SVN commit messages.

1. I’m going to print some tags!

2. OK, I screwed up the first time, but I’m really going to print some tags this time!

3. Attributes are now supported.

4. Singleton elements are now supported.

5. Holy hell, “FORMAT” is complicated! All I want to do is indent! I am also adding code to specify the output stream.

6. I have realized that it is simpler to redirect *standard-output* than it is to specify the stream.

7. DOCTYPEs are supported.

8. It turns out that XHTML tags are mandated to be lowercase. Who knew?!

Walking Through My Code

Package definitions

(defpackage "XHTML"
  (:use "COMMON-LISP")
  (:export "GENERATE")
  (:export "DOCTYPE")
  (:export *singletons*))
 
(in-package XHTML)
 
(defparameter *singletons* '("IMG" "META" "LINK" "HR" "BR" "INPUT"))

The only interesting thing here are the exports. “GENERATE” is the function that will be generating the XHTML. “DOCTYPE” allows you to generate the DOCTYPE with an optional argument to specify the location of the DTD.

*singletons* is a list of singleton elements (Elements like <img /> or <meta />), and is available to the user as a helper. It will be more useful if/when I alter this to be an XML generator.

Function: GENERATE

(defun generate (lst &amp;optional (depth 0))
  "Recursively outputs a list of pseudo-xhtml as real xhtml to *standard-output*. Does not print doctype."
  (cond ((null lst) nil)
        ((stringp lst) (format t "~VT~A" depth lst))
        ((atom lst) (format t "~A " lst))
        ((singleton? lst) (print-singleton lst (+ depth 1)))
        ((listp lst) (print-tag (car lst) (cdr lst) (+ depth 1)))))

Calling (xhtml-generate my-list) will print the desired output to *standard-output*. The program recursively partitions the work, but all tag calls inevitably go through this function. Atoms are included so that the user is not punished if they slip and forget to quote their sentences (unless they consider caps-lock a punishment).

(p This is a paragraph!)

Will be output as

<p>THIS IS A PARAGRAPH!</p>.

They will still get an admonishment (from SBCL, anyways) if there are commas in the text, but this is a happy medium for compromise.

Function: DOCTYPE

(defun doctype (&amp;optional (dtd-loc "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"))
  (format t "" dtd-loc))

This is included separately so the user can use a different DOCTYPE declaration.

Function: PRINT-TAG

(defun print-tag (name lst &amp;optional (depth 0))
  "Prints a normal html tag. Parameters are the name of the element and a list of elements that are children of this tag. If the first element of the parameter `lst` is a list whose first element is the atom **, then this list is treated as an attribute list."
  (format t "~&amp;~VT&lt;~(~A~)" depth name)
  (let ((cur-lst lst)
        (head (car lst)))
    (cond ((null lst) nil)
          ((and (listp head)
                (equal (car head) '**))
           (progn
             (print-atts (cdr head))
             (setf cur-lst (cdr lst)))))
    (format t "&gt;~&amp;")
    (mapcar #'(lambda(x) (generate x (+ depth 1))) cur-lst)
    (format t "~&amp;~VT<!--~(~A~)-->~&amp;" depth name)))

Most of my time writing and rewriting this function was spent in the “FORMAT” docs, as it is far more complicated than ANSI Common Lisp ever suggested. Not only can it do any type of formatting under the sun, but it can also butter your toast and summon dead relatives.

The logic of this function is almost identical to the simpler “PRINT-SINGLETON” below. The added ugly checks to see if we have attributes to print. The first line and the last 3 lines are all that is really important with respect to the logic of the function.

Function: SINGLETON?

(defun singleton? (inpt)
  "Checks to see if 'inpt' is a singleton (e.g. <img alt="" />)"
  (cond ((null inpt) nil)
        ((not (listp inpt)) nil)
        (t (member (symbol-name (car inpt))
                   *singletons*
                   :test #'equal))))

Function: PRINT-SINGLETON

(defun print-singleton (lst &amp;optional (depth 0))
  "Handles singleton (<img alt="" />, etc.) statements. Parameter: A list whose car
is the singleton in question, and any remaining elements are considered
attributes. Optional: The indentation depth."
  (format t "~&amp;~VT&lt;~(~A~)" depth (car lst))
  (print-atts (cdr lst))
  (format t "/&gt;~&amp;"))

This is the simpler version of “PRINT-TAG” written above. This function is a lot simpler because any extra elements in the list are assumed to be attributes. We don’t have the possibility of attributes that are mixed with tags.

Function: PRINT-ATTS

(defun print-atts (lst)
  "Prints attribute lists. There must be an even number of parameters,
the odd parameters being the names and the even parameters being attributes."
  (if (null lst)
      nil
      (progn
        (format t " ~A=\"~A\"" (car lst) (nth 1 lst))
        (print-atts (cdr (cdr lst))))))

Pretty boring, as things go.

Future Directions

Generating Related Pages

I would like to generate pages that are related. They use the same CSS, refer to the same image directories, and perhaps have the same navigation elements.

Database connections

Does anybody have any suggestions for good database interfaces [such as CLSQL]?

Running as CGI under Apache

The last thing that I will need in order to make simple client-server web applications.

Popularity: 15% [?]

Comments

3 Responses to “Learning Lisp Through Examples: XHTML Generation”

  1. Chris on August 11th, 2008 4:11 am

    Hi,
    are you aware of BKNR, a Common Lisp Web Application environment?

    URL: http://bknr.net/html/home.html

  2. Toby on August 11th, 2008 8:37 am

    I’d use Hunchentoot (http://www.weitz.de/hunchentoot/) for the webserver if you only intend to use lisp, it’s a lot easier to integrate with lisp if you don’t need PHP or other apache things.

  3. Zach Beane on August 12th, 2008 8:22 am

    I recommend reviewing other packages to get an idea of typical style. Anything by Edi Weitz is good.

    What’s the intent of your (:export *singletons*) bit in defpackage? The actual effect is to export a symbol named “*SINGLETONS*”, not to export the values of a variable named *SINGLETONS*.

Leave a Reply