Книга: Practical Common Lisp

Generating HTML

Generating HTML

Although using FORMAT to emit HTML works fine for the simple pages I've discussed so far, as you start building more elaborate pages it'd be nice to have a more concise way to generate HTML. Several libraries are available for generating HTML from an s-expression representation including one, htmlgen, that's included with AllegroServe. In this chapter you'll use a library called FOO,[289] which is loosely modeled on Franz's htmlgen and whose implementation you'll look at in more detail in Chapters 30 and 31. For now, however, you just need to know how to use FOO.

Generating HTML from within Lisp is quite natural since s-expressions and HTML are essentially isomorphic. You can represent HTML elements with s-expressions by treating each element in HTML as a list "tagged" with an appropriate first element, such as a keyword symbol of the same name as the HTML tag. Thus, the HTML <p>foo</p> is represented by the s-expression (:p "foo"). Because HTML elements nest the same way lists in s-expressions do, this scheme extends to more complex HTML. For instance, this HTML:

<html>
<head>
<title>Hello</title>
</head>
<body>
<p>Hello, world!</p>
</body>
</html>

could be represented with the following s-expression:

(:html
(:head (:title "Hello"))
(:body (:p "Hello, world!")))

HTML elements with attributes complicate things a bit but not in an insurmountable way. FOO supports two ways of including attributes in a tag. One is to simply follow the first item of the list with keyword/value pairs. The first element that follows a keyword/value pair that's not itself a keyword symbol marks the beginning of the element's contents. Thus, you'd represent this HTML:

<a href="foo.html">This is a link</a>

with the following s-expression:

(:a :href "foo.html" "This is a link")

The other syntax FOO supports is to group the tag name and attributes into their own list like this:

((:a :href "foo.html") "This is link.")

FOO can use the s-expression representation of HTML in two ways. The function emit-html takes an HTML s-expression and outputs the corresponding HTML.

WEB> (emit-html '(:html (:head (:title "Hello")) (:body (:p "Hello, world!"))))
<html>
<head>
<title>Hello</title>
</head>
<body>
<p>Hello, world!</p>
</body>
</html>
T

However, emit-html isn't always the most efficient way to generate HTML because its argument must be a complete s-expression representation of the HTML to be generated. While it's easy to build such a representation, it's not always particularly efficient. For instance, suppose you wanted to make an HTML page containing a list of 10,000 random numbers. You could build the s-expression using a backquote template and then pass it to emit-html like this:

(emit-html
`(:html
(:head
(:title "Random numbers"))
(:body
(:h1 "Random numbers")
(:p ,@(loop repeat 10000 collect (random 1000) collect " ")))))

However, this has to build a tree containing a 10,000-element list before it can even start emitting HTML, and the whole s-expression will become garbage as soon as the HTML is emitted. To avoid this inefficiency, FOO also provides a macro html, which allows you to embed bits of Lisp code in the middle of an HTML s-expression.

Literal values such as strings and numbers in the input to html are interpolated into the output HTML. Likewise, symbols are treated as variable references, and code is generated to emit their value at runtime. Thus, both of these:

(html (:p "foo"))
(let ((x "foo")) (html (:p x)))

will emit the following:

<p>foo</p>

List forms that don't start with a keyword symbol are assumed to be code and are embedded in the generated code. Any values the embedded code returns will be ignored, but the code can emit more HTML by calling html itself. For instance, to emit the contents of a list in HTML, you might write this:

(html (:ul (dolist (item (list 1 2 3)) (html (:li item)))))

which will emit the following HTML:

<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>

If you want to emit the value of a list form, you must wrap it in the pseudotag :print. Thus, this expression:

(html (:p (+ 1 2)))

generates this HTML after computing and discarding the value 3:

<p></p>

To emit the 3, you must write this:

(html (:p (:print (+ 1 2))))

Or you could compute the value and store it in a variable outside the call to html like this:

(let ((x (+ 1 2))) (html (:p x)))

Thus, you can use the html macro to generate the list of random numbers like this:

(html
(:html
(:head
(:title "Random numbers"))
(:body
(:h1 "Random numbers")
(:p (loop repeat 10 do (html (:print (random 1000)) " "))))))

The macro version will be quite a bit more efficient than the emit-html version. Not only do you never have to generate an s-expression representing the whole page, also much of the work that emit-html does at runtime to interpret the s-expression will be done once, when the macro is expanded, rather than every time the code is run.

You can control where the output generated by both html and emit-html is sent with the macro with-html-output, which is part of the FOO library. Thus, you can use the with-html-output and html macros from FOO to rewrite random-number like this:

(defun random-number (request entity)
(with-http-response (request entity :content-type "text/html")
(with-http-body (request entity)
(with-html-output ((request-reply-stream request))
(html
(:html
(:head (:title "Random"))
(:body
(:p "Random number: " (:print (random 1000))))))))))

Оглавление книги


Генерация: 0.034. Запросов К БД/Cache: 0 / 0
поделиться
Вверх Вниз