;;;; Walk along the tree and expand all the macros in the specified list (in-package :cl-who) (defparameter *macro-to-expand* nil "List of macro that macroexpand-tree should expand ") (defparameter *stream* nil "Dynamic variable that is binded to the html stream inside cl-who syntax macro.") (defun macroexpand-tree (tree) "Recursively expands all macro from *macro-to-expand* list in TREE." (apply-to-tree (lambda (subtree) (macroexpand-tree (macroexpand-1 subtree))) (lambda (subtree) (and (consp subtree) (member (first subtree) *macro-to-expand* :test #'eq))) tree)) (defmacro def-syntax-macro (name attrs &body body) "Syntactic sugar for defining a macro and adding its name to *macro-to-expand*." `(progn (defmacro ,name ,attrs ,@body) (pushnew ',name *macro-to-expand*))) (def-syntax-macro esc (form &rest rest) "Defines macroexpansion for ESC special form." (declare (ignore rest)) (let ((result (gensym))) `(let ((,result ,form)) (when ,form (write-string (escape-string ,result) *stream*))))) (def-syntax-macro str (form &rest rest) "Defines macroexpansion for STR special form." (declare (ignore rest)) (let ((result (gensym))) `(let ((,result ,form)) (when ,result (princ ,result *stream*))))) (def-syntax-macro fmt (form &rest rest) "Defines macroexpansion for FMT special form." `(format *stream* ,form ,@rest)) (def-syntax-macro htm (&rest rest) "Defines macroexpasion for HTM special form." (tree-to-commands rest '*stream*)) (defun tree-to-template (tree) "Transforms an HTML tree into an intermediate format - mainly a flattened list of strings. Utility function used by TREE-TO-COMMANDS." (loop for element in tree nconc (cond ((or (keywordp element) (and (listp element) (keywordp (first element))) (and (listp element) (listp (first element)) (keywordp (first (first element))))) ;; normal tag (process-tag element #'tree-to-template)) (t (if *indent* (list +newline+ (n-spaces *indent*) element) (list element)))))) (defun tree-to-commands-aux (tree stream) (declare (optimize speed space)) "Transforms the intermediate representation of an HTML tree into Lisp code to print the HTML to STREAM. Utility function used by TREE-TO-COMMANDS." (let ((in-string t) collector string-collector) (flet ((emit-string-collector () "Generate a WRITE-STRING statement for what is currently in STRING-COLLECTOR." (list 'write-string (string-list-to-string (nreverse string-collector)) stream)) (tree-to-commands-aux-internal (tree) "Same as TREE-TO-COMMANDS-AUX but with closed-over STREAM for REPLACE-HTM." (tree-to-commands-aux tree stream))) (unless (listp tree) (return-from tree-to-commands-aux tree)) (loop for element in tree do (cond ((and in-string (stringp element)) ;; this element is a string and the last one ;; also was (or this is the first element) - ;; collect into STRING-COLLECTOR (push element string-collector)) ((stringp element) ;; the last one wasn't a string so we start ;; with an empty STRING-COLLECTOR (setq string-collector (list element) in-string t)) (string-collector ;; not a string but STRING-COLLECTOR isn't ;; empty so we have to emit the collected ;; strings first (push (emit-string-collector) collector) (setq in-string nil string-collector '()) ;; collect this element but walk down the ;; subtree first (push element collector)) (t ;; not a string and empty STRING-COLLECTOR (push element collector))) finally (return (if string-collector ;; finally empty STRING-COLLECTOR if ;; there's something in it (nreverse (cons (emit-string-collector) collector)) (nreverse collector))))))) (defun tree-to-commands (tree stream &optional prologue) (declare (optimize speed space)) "Transforms an HTML tree into code to print the HTML to STREAM." ;; use TREE-TO-TEMPLATE, then TREE-TO-COMMANDS-AUX, and finally ;; replace the special symbols ESC, STR, FMT, and HTM (tree-to-commands-aux (if prologue (list* 'progn prologue +newline+ (tree-to-template (macroexpand-tree tree))) (cons 'progn (tree-to-template (macroexpand-tree tree)))) stream)) (defmacro with-html-output ((var &optional stream &key prologue ((:indent *indent*) *indent*)) &body body) "Transform the enclosed BODY consisting of HTML as s-expressions into Lisp code to write the corresponding HTML as strings to VAR - which should either hold a stream or which'll be bound to STREAM if supplied." (when (and *indent* (not (integerp *indent*))) (setq *indent* 0)) (when (eq prologue t) (setq prologue *prologue*)) `(let ((,var ,(or stream var)) (*stream* ,(or stream var))) ,(tree-to-commands body var prologue)))