Выбрать главу

HTML> (sexp->ops '((:p "Foo")))

#((:FRESHLINE) (:RAW-STRING "<p" NIL) (:RAW-STRING ">" NIL)

(:RAW-STRING "Foo" T) (:RAW-STRING "</p>" NIL) (:FRESHLINE))

The next phase, optimize-static-output, takes a vector of ops and returns a new vector containing the optimized version. The algorithm is simple—for each :raw-string op, it writes the string to a temporary string buffer. Thus, consecutive :raw-string ops will build up a single string containing the concatenation of the strings that need to be emitted. Whenever you encounter an op other than a :raw-string op, you convert the built-up string into a sequence of alternating :raw-string and :newline ops with the helper function compile-buffer and then add the next op. This function is also where you strip out the pretty printing ops if *pretty* is NIL.

(defun optimize-static-output (ops)

(let ((new-ops (make-op-buffer)))

(with-output-to-string (buf)

(flet ((add-op (op)

(compile-buffer buf new-ops)

(push-op op new-ops)))

(loop for op across ops do

(ecase (first op)

(:raw-string (write-sequence (second op) buf))

((:newline :embed-value :embed-code) (add-op op))

((:indent :unindent :freshline :toggle-indenting)

(when *pretty* (add-op op)))))

(compile-buffer buf new-ops)))

new-ops))

(defun compile-buffer (buf ops)

(loop with str = (get-output-stream-string buf)

for start = 0 then (1+ pos)

for pos = (position #\Newline str :start start)

when (< start (length str))

do (push-op `(:raw-string ,(subseq str start pos) nil) ops)

when pos do (push-op '(:newline) ops)

while pos))

The last step is to translate the ops into the corresponding Common Lisp code. This phase also pays attention to the value of *pretty*. When *pretty* is true, it generates code that invokes the backend generic functions on *html-pretty-printer*, which will be bound to an instance of html-pretty-printer. When *pretty* is NIL, it generates code that writes directly to *html-output*, the stream to which the pretty printer would send its output.

The actual function, generate-code, is trivial.

(defun generate-code (ops)

(loop for op across ops collect (apply #'op->code op)))

All the work is done by methods on the generic function op->code specializing the op argument with an EQL specializer on the name of the op.

(defgeneric op->code (op &rest operands))

(defmethod op->code ((op (eql :raw-string)) &rest operands)

(destructuring-bind (string check-for-newlines) operands

(if *pretty*

`(raw-string *html-pretty-printer* ,string ,check-for-newlines)

`(write-sequence ,string *html-output*))))

(defmethod op->code ((op (eql :newline)) &rest operands)

(if *pretty*

`(newline *html-pretty-printer*)

`(write-char #\Newline *html-output*)))

(defmethod op->code ((op (eql :freshline)) &rest operands)

(if *pretty*

`(freshline *html-pretty-printer*)

(error "Bad op when not pretty-printing: ~a" op)))

(defmethod op->code ((op (eql :indent)) &rest operands)

(if *pretty*

`(indent *html-pretty-printer*)

(error "Bad op when not pretty-printing: ~a" op)))

(defmethod op->code ((op (eql :unindent)) &rest operands)

(if *pretty*

`(unindent *html-pretty-printer*)

(error "Bad op when not pretty-printing: ~a" op)))

(defmethod op->code ((op (eql :toggle-indenting)) &rest operands)

(if *pretty*

`(toggle-indenting *html-pretty-printer*)

(error "Bad op when not pretty-printing: ~a" op)))

The two most interesting op->code methods are the ones that generate code for the :embed-value and :embed-code ops. In the :embed-value method, you can generate slightly different code depending on the value of the escapes operand since if escapes is NIL, you don't need to generate a call to escape. And when both *pretty* and escapes are NIL, you can generate code that uses PRINC to emit the value directly to the stream.

(defmethod op->code ((op (eql :embed-value)) &rest operands)

(destructuring-bind (value escapes) operands

(if *pretty*

(if escapes

`(raw-string *html-pretty-printer* (escape (princ-to-string ,value) ,escapes) t)

`(raw-string *html-pretty-printer* (princ-to-string ,value) t))

(if escapes

`(write-sequence (escape (princ-to-string ,value) ,escapes) *html-output*)

`(princ ,value *html-output*)))))

Thus, something like this:

HTML> (let ((x 10)) (html (:p x)))

<p>10</p>

NIL

works because html translates (:p x) into something like this:

(progn

(write-sequence "<p>" *html-output*)

(write-sequence (escape (princ-to-string x) "<>&") *html-output*)

(write-sequence "</p>" *html-output*))

When that code replaces the call to html in the context of the LET, you get the following:

(let ((x 10))

(progn

(write-sequence "<p>" *html-output*)

(write-sequence (escape (princ-to-string x) "<>&") *html-output*)

(write-sequence "</p>" *html-output*)))

and the reference to x in the generated code turns into a reference to the lexical variable from the LET surrounding the html form.

The :embed-code method, on the other hand, is interesting because it's so trivial. Because process passed the form to embed-code, which stashed it in the :embed-code op, all you have to do is pull it out and return it.

(defmethod op->code ((op (eql :embed-code)) &rest operands)

(first operands))

This allows code like this to work:

HTML> (html (:ul (dolist (x '(foo bar baz)) (html (:li x)))))

<ul>

<li>FOO</li>

<li>BAR</li>