You can make the distinction between the two flavors of macros implicit: when you define a FOO macro, the parameter list can include an &attributes
parameter. If it does, the macro form will be parsed like a regular cons form, and the macro function will be passed two values, a plist of attributes and a list of expressions that make up the body of the form. A macro form without an &attributes
parameter won't be parsed for attributes, and the macro function will be invoked with a single argument, a list containing the body expressions. The former is useful for what are essentially HTML templates. For example:
(define-html-macro :mytag (&attributes attrs &body body)
`((:div :class "mytag" ,@attrs) ,@body))
HTML> (html (:mytag "Foo"))
<div class='mytag'>Foo</div>
NIL
HTML> (html (:mytag :id "bar" "Foo"))
<div class='mytag' id='bar'>Foo</div>
NIL
HTML> (html ((:mytag :id "bar") "Foo"))
<div class='mytag' id='bar'>Foo</div>
NIL
The latter kind of macro is more useful for writing macros that manipulate the forms in their body. This type of macro can function as a kind of HTML control construct. As a trivial example, consider the following macro that implements an :if
construct:
(define-html-macro :if (test then else)
`(if ,test (html ,then) (html ,else)))
This macro allows you to write this:
(:p (:if (zerop (random 2)) "Heads" "Tails"))
instead of this slightly more verbose version:
(:p (if (zerop (random 2)) (html "Heads") (html "Tails")))
To determine which kind of macro you should generate, you need a function that can parse the parameter list given to define-html-macro
. This function returns two values, the name of the &attributes
parameter, or NIL
if there was none, and a list containing all the elements of args
after removing the &attributes
marker and the subsequent list element.[320]
(defun parse-html-macro-lambda-list (args)
(let ((attr-cons (member '&attributes args)))
(values
(cadr attr-cons)
(nconc (ldiff args attr-cons) (cddr attr-cons)))))
HTML> (parse-html-macro-lambda-list '(a b c))
NIL
(A B C)
HTML> (parse-html-macro-lambda-list '(&attributes attrs a b c))
ATTRS
(A B C)
HTML> (parse-html-macro-lambda-list '(a b c &attributes attrs))
ATTRS
(A B C)
The element following &attributes
in the parameter list can also be a destructuring parameter list.
HTML> (parse-html-macro-lambda-list '(&attributes (&key x y) a b c))
(&KEY X Y)
(A B C)
Now you're ready to write define-html-macro
. Depending on whether there was an &attributes
parameter specified, you need to generate one form or the other of HTML macro so the main macro simply determines which kind of HTML macro it's defining and then calls out to a helper function to generate the right kind of code.
(defmacro define-html-macro (name (&rest args) &body body)
(multiple-value-bind (attribute-var args)
(parse-html-macro-lambda-list args)
(if attribute-var
(generate-macro-with-attributes name attribute-var args body)
(generate-macro-no-attributes name args body))))
The functions that actually generate the expansion look like this:
(defun generate-macro-with-attributes (name attribute-args args body)
(with-gensyms (attributes form-body)
(if (symbolp attribute-args) (setf attribute-args `(&rest ,attribute-args)))
`(eval-when (:compile-toplevel :load-toplevel :execute)
(setf (get ',name 'html-macro-wants-attributes) t)
(setf (get ',name 'html-macro)
(lambda (,attributes ,form-body)
(destructuring-bind (,@attribute-args) ,attributes
(destructuring-bind (,@args) ,form-body
,@body)))))))
(defun generate-macro-no-attributes (name args body)
(with-gensyms (form-body)
`(eval-when (:compile-toplevel :load-toplevel :execute)
(setf (get ',name 'html-macro-wants-attributes) nil)
(setf (get ',name 'html-macro)
(lambda (,form-body)
(destructuring-bind (,@args) ,form-body ,@body)))))
The macro functions you'll define accept either one or two arguments and then use DESTRUCTURING-BIND
to take them apart and bind them to the parameters defined in the call to define-html-macro
. In both expansions you need to save the macro function in the name's property list under html-macro
and a boolean indicating whether the macro takes an &attributes
parameter under the property html-macro-wants-attributes
. You use that property in the following function, expand-macro-form
, to determine how the macro function should be invoked:
(defun expand-macro-form (form)
(if (or (consp (first form))
(get (first form) 'html-macro-wants-attributes))
(multiple-value-bind (tag attributes body) (parse-cons-form form)
(funcall (get tag 'html-macro) attributes body))
(destructuring-bind (tag &body body) form
(funcall (get tag 'html-macro) body))))
The last step is to integrate macros by adding a clause to the dispatching COND
in the top-level process
function.
(defun process (processor form)
(cond
((special-form-p form) (process-special-form processor form))
((macro-form-p form) (process processor (expand-macro-form form)))
((sexp-html-p form) (process-sexp-html processor form))
((consp form) (embed-code processor form))
(t (embed-value processor form))))
This is the final version of process
.
The Public API
Now, at long last, you're ready to implement the html
macro, the main entry point to the FOO compiler. The other parts of FOO's public API are emit-html
and with-html-output
, which I discussed in the previous chapter, and define-html-macro
, which I discussed in the previous section. The define-html-macro
macro needs to be part of the public API because FOO's users will want to write their own HTML macros. On the other hand, define-html-special-operator
isn't part of the public API because it requires too much knowledge of FOO's internals to define a new special operator. And there should be very little that can't be done using the existing language and special operators.[321]
320
Note that &attributes
is just another symbol; there's nothing intrinsically special about names that start with &
.
321
The one element of the underlying language-processing infrastructure that's not currently exposed through special operators is the indentation. If you wanted to make FOO more flexible, albeit at the cost of making its API that much more complex, you could add special operators for manipulating the underlying indenting printer. But it seems like the cost of having to explain the extra special operators would outweigh the rather small gain in expressiveness.