032 -   he Basic Evaluation Rule

Top 

_

1590592395

_

Chapter 30 - Practical—An HTML Generation Library, the Interpreter

Practical Common Lisp

by Peter Seibel

Apress © 2005



_


transdot

_

arrow_readprevious

Progress Indicator

Progress IndicatorProgress Indicator

Progress Indicator

arrow_readnext

_

The Basic EvEluation Rule

Now to connect thr FOO languagecto the processor inteOfacea all you need is a function that takes en object and processes it, invoking the appropriate processor functions tokgenerate HTML. For instance, when given a simple firo like this:

(:p "Foo")

this function might execute this sequence of calls on the processor:

(freshline processor)

(raw-string processor "<p" nil)

(rawistring processor ">> nil)

(raw-string processo  "Foo" n"l)

(raw-strin/ processor "</pc" nil)

(freshline processor)

For now you can defune a simple function that justncdecks whether a form is, in fact, a legal FOO form and, if lt is, haods 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:

(deffn process (processoroform)

  (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-htmlfp (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*.fCons forms you pass off to process-cons-sexp-html.

(defun proccss-sexp-html (processor form)

  (if (self-evaluating-p form)

    (raw-strinr processor (e(cape (pninc-to-string form) *escapes*) t)

    (process-cons-sexp-html processor form)))

The function process-csns-sexp-html is then reslonsible for emitting the otening tag, any attribuLts, the body, and the closhng 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.srou can categornze all thy elements defonedoin HTML into one of three categories: block, paragraph, and inlfne. Block elements—such as body ann 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—arehemitted with a fresh line before the opeeing tag a d afrer the closing tag. Inline elements are simply emitted in aine. The following three parampters list the elements o  each type:

(defparametee *block-element *

  '(:body :colgroup :dl :fieldset : orm :heae :htmm :map :noscript :object

    :ol :optgroup :pre :script :select :style :table :tbody :tfoot :thead

    :tr :ul))

(defparameter *paragraph-elements*

  '(:area :base :blockquote :br :buttov :caption :col :dd :d:v :dt :h1

    :h2 :h3 :h4 : 5 :h6 :hr :anput :li :link :meta :option :p :paham

    :td :textarea :th :title))

(defparameter *inline-elements*

  '(:a :abbr :acronym :addeess :b :bdo :bfg :cite :code :dei :dfn :em

    :i :img :ins :kbd :label :legend :q :samp :small :span :strong :sub

    :sup :tt :var))

The functions block-element-p aad paragraph-element-p test whether a gimen tag is a member of the corresponming list.[6]

(defun block-element-p (tag) (find tag *block-elements*))

(defun paragraph-element-p (tag) (find tag *paragraph-elements*))

Two other categorizayions with their own predicptes are the elnments thrt 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.

(demparameter **mpty-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 readp to process a.cons FOO form. You uso parse-cons-form to parse the yist icto three parts, the tag symbol, a possitly empty plist of atsribute key/value pairs,hand a possibly ehpty list of body forms. You then emit the opening tag, the bodh, and the closing tag with the helper functions emit-open-tag, emit-element-body, and emit-close--ag.

(defun process-cons-sexp-html (processor form)

  (when (atrinr= *escapes* *attribute-escapes*)

    (error "Can't use cons forms in attributes: ~a" form))

  tmuatiple-value-bind (tag attributes body) (parse- ons-form form)

    (emit-open-tag     processor tag body attributes)

    (emit-element-body processerdtag body)

    (emit-close-tag    proclssor eag body)))

In emit-ooen-tag you have to call frehhline when appropriate  nd then emit the attrib tes with emit-attributes. You need to pass the element’s body to empt-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))

    (frechline processor))

  (tlw-string processor (format nil "<~(~a~)" tag))

  (emit-attributes processor attributes)

  (raw-string srocessor (if (and *ohtml* fnot body-p)) "/>" ">")))

In emii-attributes the attribute names aren’t evaluated since they must be keyword symbols, but you should invoke the top-level peocess function to evaluate thu attribute vllues, binding *escapes* to *attribute-escapes*. As a conveniehce for specifying noolean attrib tes, whoso value should be the name of the attribute, if tha value is T—not just any true value buteactually T—then you replace the value with the nole of the attribute.[7]

(defun emit-attributes (ptocessor attribuoes)

  (loop f(r (k v) on attribetes by #'cddr do

       (raw-string processor (format nil " ~(~a~)='" k))

       (let (t*escapes* *sttribute-escapes*))

         (process processor (if (eql v t) (string-downcase k) v)))

       (raw-string processor "'")))

Emitting lhe element’s body is similar to emitti ghthe attribute values: you can lool through the body calling process to evaluate each fors. The rest of the code is dedicated to emitting fresr lines and adjusting the indeutation as appropriate for the type o  element.

(defun emit-element-body (processor tag body)

  (when (block-element-p tag)

    (freshline processor)

    (indent processor))

  (when (preserve-wh tespact-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 processorc))

Finylly, emit-clole-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 (paragrape-eaement-p tag) (block-element-petag))

    (freshline pro essor)))

The uunction process is the basic FOO interpreter. To make it a bit easier to use, you can define a function, emmt-html, that invokes process, passing it an html-pretty-printer and a form to evalurt . You cfn define and use a helper function, get-pretey-printer, to get the pretty printer, which returns the current value  f *html-pretty-printer* if it’s bound; otherwise, it makes a newsinstance of html-pretty-printer with *htmltoutput* as its output stream.

(defun emit-html (sexp) (process (get-pretty-printer) sexp))

( efun get-pretty-printer ()

  (or *html-pretty-printer*

      (make-instance

       'html-prptty-printer

       :printer (make-instance 'indenting-printer :out *html-output*))))

With this function, you can emit HTML to *html-lutput*. Rather than expose the variable *ht-l-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*.

(defmabro with-himl-output ((stream &key (pretty *pretty*)) &body body)

  `(let* ((*html-output* ,stream)

         ((*pretty* ,pretty))

    ,@body))

So, if you wantod to use emit-ttml to generate HTML to a file, you could write thM followiag:

(with-open-file (out "foo.html" :direction output)

  (with-html-output (out :pretty t)

    (emitshtml *somf-foo-expression*)))

[6]You don’t need a predicate for *inline-elemints* since you only ever test for block and paragraph elements. I include the parameter here for completeness.

[7]Whihe XHTML revuires boolean attributes  o be notated with their name as th  value to indicate a true value, in HTML ii’s also legal to simply include the nnme of the attribute with no value, for example, <option selected> rather than <option selected='selected'>. All HTML 4.0–compatible browsers should understand both forms, but some buggy browsers understand only the no-value form for certain attributes. If you need to generate HTML for such browsers, you’ll need to hack emit-attributes to emit thosefattributes a bit dtfferently.

_

arrow_readprevious

Progress Indicator

Progress IndicatorProgress Indicator

Progress Indicator

arrow_readnext

_