Книга: Practical Common Lisp
The Basic Evaluation Rule
The Basic Evaluation Rule
Now to connect the FOO language to the processor interface, all you need is a function that takes an object and processes it, invoking the appropriate processor functions to generate HTML. For instance, when given a simple form like this:
(:p "Foo")
this function might execute this sequence of calls on the processor:
(freshline processor)
(raw-string processor "<p" nil)
(raw-string processor ">" nil)
(raw-string processor "Foo" nil)
(raw-string processor "</p>" nil)
(freshline processor)
For now you can define a simple function that just checks whether a form is, in fact, a legal FOO form and, if it is, hands it off to the function process-sexp-html
for processing. In the next chapter, you'll add some bells and whistles to this function to allow it to handle macros and special operators. But for now it looks like this:
(defun process (processor form)
(if (sexp-html-p form)
(process-sexp-html processor form)
(error "Malformed FOO form: ~s" form)))
The function sexp-html-p
determines whether the given object is a legal FOO expression, either a self-evaluating form or a properly formatted cons.
(defun sexp-html-p (form)
(or (self-evaluating-p form) (cons-form-p form)))
Self-evaluating forms are easily handled: just convert to a string with PRINC-TO-STRING
and escape the characters in the variable *escapes*
, which, as you'll recall, is initially bound to the value of *element-escapes*
. Cons forms you pass off to process-cons-sexp-html
.
(defun process-sexp-html (processor form)
(if (self-evaluating-p form)
(raw-string processor (escape (princ-to-string form) *escapes*) t)
(process-cons-sexp-html processor form)))
The function process-cons-sexp-html
is then responsible for emitting the opening tag, any attributes, the body, and the closing tag. The main complication here is that to generate pretty HTML, you need to emit fresh lines and adjust the indentation according to the type of the element being emitted. You can categorize all the elements defined in HTML into one of three categories: block, paragraph, and inline. Block elements—such as body
and ul
—are emitted with fresh lines before and after both their opening and closing tags and with their contents indented one level. Paragraph elements—such as p, li
, and blockquote
—are emitted with a fresh line before the opening tag and after the closing tag. Inline elements are simply emitted in line. The following three parameters list the elements of each type:
(defparameter *block-elements*
'(:body :colgroup :dl :fieldset :form :head :html :map :noscript :object
:ol :optgroup :pre :script :select :style :table :tbody :tfoot :thead
:tr :ul))
(defparameter *paragraph-elements*
'(:area :base :blockquote :br :button :caption :col :dd :div :dt :h1
:h2 :h3 :h4 :h5 :h6 :hr :input :li :link :meta :option :p :param
:td :textarea :th :title))
(defparameter *inline-elements*
'(:a :abbr :acronym :address :b :bdo :big :cite :code :del :dfn :em
:i :img :ins :kbd :label :legend :q :samp :small :span :strong :sub
:sup :tt :var))
The functions block-element-p
and paragraph-element-p
test whether a given tag is a member of the corresponding list.[316]
(defun block-element-p (tag) (find tag *block-elements*))
(defun paragraph-element-p (tag) (find tag *paragraph-elements*))
Two other categorizations with their own predicates are the elements that are always empty, such as br
and hr
, and the three elements, pre
, style
, and script
, in which whitespace is supposed to be preserved. The former are handled specially when generating regular HTML (in other words, not XHTML) since they're not supposed to have a closing tag. And when emitting the three tags in which whitespace is preserved, you can temporarily turn off indentation so the pretty printer doesn't add any spaces that aren't part of the element's actual contents.
(defparameter *empty-elements*
'(:area :base :br :col :hr :img :input :link :meta :param))
(defparameter *preserve-whitespace-elements* '(:pre :script :style))
(defun empty-element-p (tag) (find tag *empty-elements*))
(defun preserve-whitespace-p (tag) (find tag *preserve-whitespace-elements*))
The last piece of information you need when generating HTML is whether you're generating XHTML since that affects how you emit empty elements.
(defparameter *xhtml* nil)
With all that information, you're ready to process a cons FOO form. You use parse-cons-form
to parse the list into three parts, the tag symbol, a possibly empty plist of attribute key/value pairs, and a possibly empty list of body forms. You then emit the opening tag, the body, and the closing tag with the helper functions emit-open-tag
, emit-element-body
, and emit-close-tag
.
(defun process-cons-sexp-html (processor form)
(when (string= *escapes* *attribute-escapes*)
(error "Can't use cons forms in attributes: ~a" form))
(multiple-value-bind (tag attributes body) (parse-cons-form form)
(emit-open-tag processor tag body attributes)
(emit-element-body processor tag body)
(emit-close-tag processor tag body)))
In emit-open-tag
you have to call freshline
when appropriate and then emit the attributes with emit-attributes
. You need to pass the element's body to emit-open-tag
so when it's emitting XHTML, it knows whether to finish the tag with />
or >
.
(defun emit-open-tag (processor tag body-p attributes)
(when (or (paragraph-element-p tag) (block-element-p tag))
(freshline processor))
(raw-string processor (format nil "<~(~a~)" tag))
(emit-attributes processor attributes)
(raw-string processor (if (and *xhtml* (not body-p)) "/>" ">")))
In emit-attributes
the attribute names aren't evaluated since they must be keyword symbols, but you should invoke the top-level process
function to evaluate the attribute values, binding *escapes*
to *attribute-escapes*
. As a convenience for specifying boolean attributes, whose value should be the name of the attribute, if the value is T
—not just any true value but actually T
—then you replace the value with the name of the attribute.[317]
(defun emit-attributes (processor attributes)
(loop for (k v) on attributes by #'cddr do
(raw-string processor (format nil " ~(~a~)='" k))
(let ((*escapes* *attribute-escapes*))
(process processor (if (eql v t) (string-downcase k) v)))
(raw-string processor "'")))
Emitting the element's body is similar to emitting the attribute values: you can loop through the body calling process
to evaluate each form. The rest of the code is dedicated to emitting fresh lines and adjusting the indentation as appropriate for the type of element.
(defun emit-element-body (processor tag body)
(when (block-element-p tag)
(freshline processor)
(indent processor))
(when (preserve-whitespace-p tag) (toggle-indenting processor))
(dolist (item body) (process processor item))
(when (preserve-whitespace-p tag) (toggle-indenting processor))
(when (block-element-p tag)
(unindent processor)
(freshline processor)))
Finally, emit-close-tag
, as you'd probably expect, emits the closing tag (unless no closing tag is necessary, such as when the body is empty and you're either emitting XHTML or the element is one of the special empty elements). Regardless of whether you actually emit a close tag, you need to emit a final fresh line for block and paragraph elements.
(defun emit-close-tag (processor tag body-p)
(unless (and (or *xhtml* (empty-element-p tag)) (not body-p))
(raw-string processor (format nil "</~(~a~)>" tag)))
(when (or (paragraph-element-p tag) (block-element-p tag))
(freshline processor)))
The function process
is the basic FOO interpreter. To make it a bit easier to use, you can define a function, emit-html
, that invokes process
, passing it an html-pretty-printer
and a form to evaluate. You can define and use a helper function, get-pretty-printer
, to get the pretty printer, which returns the current value of *html-pretty-printer*
if it's bound; otherwise, it makes a new instance of html-pretty-printer
with *html-output*
as its output stream.
(defun emit-html (sexp) (process (get-pretty-printer) sexp))
(defun get-pretty-printer ()
(or *html-pretty-printer*
(make-instance
'html-pretty-printer
:printer (make-instance 'indenting-printer :out *html-output*))))
With this function, you can emit HTML to *html-output*
. Rather than expose the variable *html-output*
as part of FOO's public API, you should define a macro, with-html-output
, that takes care of binding the stream for you. It also lets you specify whether you want pretty HTML output, defaulting to the value of the variable *pretty*
.
(defmacro with-html-output ((stream &key (pretty *pretty*)) &body body)
`(let* ((*html-output* ,stream)
(*pretty* ,pretty))
,@body))
So, if you wanted to use emit-html
to generate HTML to a file, you could write the following:
(with-open-file (out "foo.html" :direction output)
(with-html-output (out :pretty t)
(emit-html *some-foo-expression*)))
- 30. Practical: An HTML Generation Library, the Interpreter
- Controlling Evaluation
- 4.4.4 The Dispatcher
- About the author
- Chapter 7. The state machine
- Chapter 8. Saving and restoring large rule-sets
- Chapter 9. How a rule is built
- Appendix E. Other resources and links
- What NAT is used for and basic terms and expressions
- Example NAT machine in theory
- The final stage of our NAT machine
- Compiling the user-land applications