;; -*- lisp -*-
(in-package :it.bese.yaclml)
;;;; * YACLML - Programmatic HTML Generation
;;;; The programmatic interface is a collection of Common Lisp macros
;;;; designed to make embedding HTML in lisp code easy. It was created
;;;; with the following goals in mind:
;;;; - The code for creating HTML should look and act like regular lisp
;;;; code.
;;;; - Given what we know about HTML and the ratio of static to dynamic
;;;; text in a typical web page it's important to constant fold as much
;;;; as possible.
;;;; - Tags should be easily definable and should be able to perform
;;;; arbitrary computations at both run time and compile time.
;;;; ** Using YACLML Tag Macros
;;;; You use YACLML tags just like regular macros, any attributes are
;;;; passed in like keyword arguments. YACLML examines its args (at
;;;; compile time) and distinguishes between the keyword arguments
;;;; which become attributes and everything else, which becomes the
;;;; tag's body. Tags all have the following syntax:
;;;; ( tag-name [ :keyword value ] * . body )
;;;; ** Tag Attributes
;;;; The name of the attribute will be the result of string-downcase'ing
;;;; the symbol-name specified in the macro. Depending on the runtime value
;;;; returned executing the value specified in the macro call three things
;;;; can happen:
;;;; NIL - The attribute will be ignored
;;;; T - The attribute will be printed with (string-downcase name) as
;;;; the value.
;;;; anything else - The result of evaluating the value will be
;;;; printed (via PRINC) as the value of the
;;;; attribute.
;;;; If the need ever arises to have an HTML attribute whose value is
;;;; T or NIL it is necessary to return the string \"T\" or \"NIL\"
;;;; and not the symbol T or NIL.
;;;; ** The Tag Body
;;;; Every element of the tag body is processed in order: if it is a
;;;; form it is executed at runtime and must explicitly print to the
;;;; stream *yaclml-stream* if it needs to generate output, if it is a
;;;; string its value will be printed to *yaclml-stream* at run time.
;;;; ** Examples
;;;; ;; Assuming *yaclml-stream* is bound to *standard-output*
;;;; (<:a :href \"http://foo.com\" \"foo.com\")
;;;; =>
;;;; foo.com
;;;; (<:br)
;;;; =>
;;;;
;;;; (<:td \"whatever\")
;;;; =>
;;;;
whatever |
(defvar *tal-truename*)
(defvar *yaclml-stream* t
"The stream to which tags are printed.")
(defvar *yaclml-indent* t
"When T (must be set while compiling yaclml code) the generated
HTML is indented.")
(defvar %yaclml-indentation-depth% 0)
(defmacro with-yaclml-stream (stream &body body)
"Evaluate BODY with *yaclml-stream* bound to STREAM."
`(let ((*yaclml-stream* ,stream))
(declare (special *yaclml-stream*))
,@body))
(defmacro with-yaclml-output-to-string (&body body)
"Evaluate BODY with *yaclml-stream* bound to a string stream, return the string."
(with-unique-names (output)
`(with-output-to-string (,output)
(with-yaclml-stream ,output
,@body))))
(defvar %yaclml-code% nil
"The list of currently collected code this yaclml macro should
expand into.")
(defvar *expanders* (make-hash-table :test 'eql)
"Hash table mapping expanders to the expander function.")
(defvar *expander-macros* (make-hash-table :test 'eql)
"Hash table mapping expander macros to theri macre functions.")
(defun yaclml-constant-p (thing)
"Returns T if THING is, as far as yaclml is concerned, a run time
constant."
(or (stringp thing)
(characterp thing)
(numberp thing)
(keywordp thing)))
(defun emit-princ (&rest items)
"Emit to the current yaclml-code a form which will, at runtime,
princ ITEM. If (yaclml-constant-p ITEM) is true the princ will
be done at compile time."
(dolist (item items %yaclml-code%)
(push (cond
((stringp item)
item)
((keywordp item)
(string-downcase (princ-to-string item)))
((yaclml-constant-p item)
(princ-to-string item))
(t `(princ ,item *yaclml-stream*)))
%yaclml-code%)))
(defun emit-html (&rest items)
"Like EMIT-PRINC but escapes html chars in item."
(dolist (item items %yaclml-code%)
(if (yaclml-constant-p item)
(push (escape-as-html (princ-to-string item)) %yaclml-code%)
(push `(emit-attribute-value ,item) %yaclml-code%))))
(defun emit-code (&rest forms)
"Emit to the current yaclml-code CODE. This means that whatever
CODE is it will be run, and it's result will be ignored, at
runtime."
(setf %yaclml-code% (nconc forms %yaclml-code%)))
(defmacro emit-attribute (name value)
(rebinding (value)
`(case ,value
((t)
(princ #\Space *yaclml-stream*)
(princ ,name *yaclml-stream*)
(princ "=\"" *yaclml-stream*)
(princ ,name *yaclml-stream*)
(princ #\" *yaclml-stream*))
((nil) nil)
(t
(princ #\Space *yaclml-stream*)
(princ ,name *yaclml-stream*)
(princ "=\"" *yaclml-stream*)
(emit-attribute-value ,value)
(princ #\" *yaclml-stream*)))))
(defun emit-princ-attribute (name value)
(unless (stringp name)
(setf name (string-downcase (princ-to-string name))))
(emit-code
(rebinding (value)
`(case ,value
((t)
(princ ,(concatenate 'string " " name "=\"" name "\"")
*yaclml-stream*))
((nil) nil)
(t
(princ ,(concatenate 'string " " name "=\"") *yaclml-stream*)
(emit-attribute-value ,value)
(princ "\"" *yaclml-stream*))))))
(defun emit-attribute-value (value)
(if (listp value)
(iter (for el in value)
(unless (first-time-p)
(princ #\Space *yaclml-stream*))
(write-as-html (princ-to-string el) :stream *yaclml-stream*))
(write-as-html (princ-to-string value) :stream *yaclml-stream*)))
(defun emit-princ-attributes (attributes)
"Assuming attributes is a list of (name1 value1 name2 value2 ...), emit
the code necessary to print them at runtime. If VALUE is a
list every element will be concatenated separated by a space to
form the final string value of the attribute.
If the value of any of the attributes is NIL it will be ignored.
If a value is the symbol T the name of the attribute will be used
as the value."
(iter (while attributes)
(for key = (pop attributes))
(if (runtime-attribute-list-reference-p key)
(emit-code `(iter (for (name value) :on ,(ralr-form key) :by #'cddr)
(unless (stringp name)
(setf name (string-downcase (string name))))
(emit-attribute name value)))
(let ((value (pop attributes)))
(cond
((eql t value)
;; according to xhtml thoses attributes which in html are
;; specified without a value should just use the attribute
;; name as the xhtml value
(emit-princ " " key "=\"" key "\""))
((eql nil value) nil)
((yaclml-constant-p value)
(progn
(emit-princ " " key "=\"")
(emit-html value)
(emit-princ "\"")))
(t
(if (and (consp value)
(eql 'cl:concatenate (first value))
(consp (cdr value))
(eql 'cl:string (second value)))
;; a call to concatenate can be dealt with specially
(progn
(emit-princ " " key "=\"")
(dolist (val (cddr value))
(emit-princ val)))
(emit-princ-attribute key value)))))))
%yaclml-code%)
(defun emit-indentation ()
(when *yaclml-indent*
(emit-princ #\Newline)
(emit-princ (make-string %yaclml-indentation-depth% :initial-element #\Space))))
(defun emit-open-tag (name &rest attributes)
"Emit the code required to print an open tag whose name is NAME and
with the attributes ATTRIBUTES. ATTRIBUTES is expected to be an even
long, setf-like list of name-value pairs defining the attributes."
(incf %yaclml-indentation-depth% 2)
(emit-princ "<")
(emit-princ name)
(mapc #'emit-princ-attributes attributes)
(emit-indentation)
(emit-princ ">"))
(defun emit-close-tag (name)
"Emit the code required to print a close tag whose name is NAME."
(decf %yaclml-indentation-depth% 2)
(emit-princ "" name)
(emit-indentation)
(emit-princ ">"))
(defun emit-empty-tag (name &rest attributes)
"Emit the code required to print an empty tag with name NAME and a
attributes ATTRIBUTES. See EMIT-OPEN-TAG for more details."
(emit-princ "<" name)
(mapc #'emit-princ-attributes attributes)
(emit-indentation)
(emit-princ "/>"))
(defun emit-body (body)
"Traverse body and emit the corresponding code. Every form in body
is analyzed according to the following rules:
cons whose car is not a known expander - code which should be included
with no further analysis.
cons whose car is a known expander - simply call the expander function
with the cdr of the cons as the arg.
yaclml-constant-p - print the constant (after escape-as-html) to
*yaclml-stream*.
cons whose car is YACLML-QUOTE - emit-body on every element of the
cdr.
"
(dolist (form body)
(emit-form form)))
(defun emit-form (form)
"Emits the code to print FORM."
(if (consp form)
(let ((op (car form)))
(cond
((gethash op *expander-macros*)
(emit-form (funcall (gethash op *expander-macros*) (cdr form))))
((gethash op *expanders*)
(funcall (gethash op *expanders*) (cdr form)))
((eql 'yaclml-quote op)
(dolist (b (cdr form))
(emit-form b)))
((eql 'cl:progn op)
(dolist (b (cdr form))
(emit-form b)))
(t (emit-code form))))
(if (yaclml-constant-p form)
(emit-princ (escape-as-html (princ-to-string form)))
(emit-code form))))
(defmacro deftag (name attributes &body body)
"Define a new tag.
ATTRIBUTES should be an attribute-spec (see parse-attributes and
attribute-bind).
BODY is simply the body of the expander lambda.
Within the BODY the functions EMIT-CODE, EMIT-PRINC and EMIT-HTML can
be used to generate code. EMIT-CODE should be passed lisp code which
will be executed at runtime."
(with-unique-names (contents)
`(progn
(setf (gethash ',name *expanders*)
(lambda (,contents)
(handler-bind ((tag-related-error (lambda (c)
(setf (tag c) ,contents))))
(attribute-bind ,attributes ,contents
,@body))))
(defmacro ,name (&rest contents)
(let ((%yaclml-code% nil)
(%yaclml-indentation-depth% 0))
;; build tag's body
(funcall (gethash ',name *expanders*) contents)
(setf %yaclml-code% (nreverse %yaclml-code%))
;; now that we've generated the code we can fold the
;; strings in yaclml-code and princ them, leaving any other
;; forms as they are.
`(progn ,@(mapcar (lambda (form)
(if (stringp form)
`(write-string ,form *yaclml-stream*)
form))
(fold-strings %yaclml-code%))
(values)))))))
(defmacro deftag-macro (name attributes &body body)
"Define a new YACLML tag macro.
Tag macros, like regular macros, expand into other YACLML tag
forms which are recursivly processed."
(let ((contents (gensym))
(doc-string (if (stringp (first body))
(pop body)
nil)))
`(progn
(setf (gethash ',name *expander-macros*)
(lambda (,contents)
(handler-bind ((unrecognized-attribute (lambda (c)
(setf (tag c) ,contents))))
(attribute-bind ,attributes ,contents
,@body))))
(defmacro ,name (&rest ,contents) ,doc-string
(funcall (gethash ',name *expander-macros*) ,contents))
',name)))
(defmacro def-simple-xtag (name)
"Convience macro for defing tags which accept any kind of attribute
and just wrap the body in an xml tag."
`(deftag ,name (&allow-other-attributes other-attributes &body body)
(if body
(progn
(emit-open-tag ,(string-downcase (string name)) other-attributes)
(emit-body body)
(emit-close-tag ,(string-downcase (string name))))
(emit-empty-tag ,(string-downcase (string name)) other-attributes))))
(defmacro wrap-in-tag ((tag-name &rest tag-attributes) &body body)
(with-unique-names (tname)
`(let ((,tname ,(string-downcase (string tag-name))))
(emit-open-tag ,tname ,tag-attributes)
(prog1
(progn ,@body)
(emit-close-tag ,tname)))))
(defvar *xml-reader-open-char* #\<)
(defvar *xml-reader-close-char* #\>)
(defmacro enable-xml-syntax ()
"Enable xml reader syntax for the file being compiled or loaded.
You may consider using (enable-bracket-reader):
{with-xml-syntax
}
Syntax examples:
<(progn 33) :bar 42 (@ \"cAMeL\" \"eLitE-<>\") \"body-<>\" >
==>
<33 bar=\"42\" cAMeL=\"eLitE-<>\"
>body-<>33
>
<\"foo\" :bar 42>
==>
"
'(eval-when (:compile-toplevel :execute)
(setf *readtable* (copy-readtable *readtable*))
(%enable-xml-syntax)))
(defun %enable-xml-syntax ()
(set-macro-character *xml-reader-open-char* #'xml-reader-open nil *readtable*))
(defun xml-reader-open (s char)
"Emit XML elements into *yaclml-stream*, use keyword parameters
for attributes and rest parameters for nested XML elements or
normal lisp code. See enable-xml-syntax for more details."
;;; (attila) this code here makes sure not to use two unread-char calls
;;; as it's unspecified by the standard. and therefore the ugliness here...
;;; we create a fake package, read the token in there, delete the package
;;; and analize what was read.
(let ((fake-package (make-package (gensym)))
(symbol nil)
(symbol-name nil)
(next-char nil)
;; simple-version means that the xml tag name is a symbol, e.g. s t)))
;;(format t "The delimited list is ~S~%" result) ; TODO debug code
result))
(head (if simple-version
(subseq symbol-name 1) ; drop the "<" from the symbol-name
(car list)))
(tag-name (if (or (stringp head) (consp head))
head
(string-downcase (princ-to-string head))))
(%yaclml-code% nil)
(%yaclml-indentation-depth% 0))
(attribute-bind (&allow-other-attributes other-attributes &body body)
(if simple-version
list
(cdr list))
(if body
(let* ((open-code)
(body-code)
(close-code)
(rebind-tag-name-p (consp tag-name))
(original-tag-name tag-name))
(when rebind-tag-name-p
(setf tag-name (gensym "TAG-NAME")))
(let ((%yaclml-code% '()))
(emit-open-tag tag-name other-attributes)
(setf open-code %yaclml-code%))
(let ((%yaclml-code% '()))
(emit-body body)
(setf body-code %yaclml-code%))
(let ((%yaclml-code% '()))
(emit-close-tag tag-name)
(setf close-code %yaclml-code%))
(if rebind-tag-name-p
(emit-code `(let ((,tag-name ,original-tag-name))
,@(emitter open-code)
,@(emitter body-code)
,@(emitter close-code)))
(emit-code `(progn
,@(emitter open-code)
,@(emitter body-code)
,@(emitter close-code)))))
(emit-empty-tag tag-name other-attributes)))
`(progn
,@(emitter %yaclml-code%)
(values)))))))
(defun with-xml-syntax ()
(lambda (handler)
(%enable-xml-syntax)
`(progn ,@(funcall handler))))
;; Copyright (c) 2002-2005, Edward Marco Baringer
;; All rights reserved.
;;
;; Redistribution and use in source and binary forms, with or without
;; modification, are permitted provided that the following conditions are
;; met:
;;
;; - Redistributions of source code must retain the above copyright
;; notice, this list of conditions and the following disclaimer.
;;
;; - Redistributions in binary form must reproduce the above copyright
;; notice, this list of conditions and the following disclaimer in the
;; documentation and/or other materials provided with the distribution.
;;
;; - Neither the name of Edward Marco Baringer, nor BESE, nor the names
;; of its contributors may be used to endorse or promote products
;; derived from this software without specific prior written permission.
;;
;; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
;; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
;; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
;; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
;; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
;; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
;; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
;; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
;; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
;; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
;; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.