(in-package :kira)
(eval-always
(defvar *html-output* *standard-output*)
(defvar *html-processor* nil)
(defvar *in-attribute-p* nil))
(defun process-attributes (attributes)
(loop for prefix = '(:progn) until (endp attributes)
do (destructuring-bind (key value &rest rest) attributes
(cond ((not (eq key :if*)) (setq attributes rest))
(t (destructuring-bind (actual-key actual-value &rest rest) rest
(setq prefix `(:when ,value) key actual-key value actual-value)
(setq attributes rest))))
(check-type key (satisfies self-evaluating-p))
(let* ((no-value-p (and (symbolp value) (zerop (length (symbol-name value)))))
(value-sequence (unless no-value-p `("=\"" (:attribute ,value) #\"))))
(process-html `((,@prefix #\Space ,key ,@value-sequence)))))))
(defun parse-html-tag (tag)
(cond ((atom tag) (values tag))
(t (values (pop tag) tag))))
(defun start-tag (name attributes)
(write-char #\< *html-output*)
(princ name *html-output*)
(process-attributes attributes)
(write-char #\> *html-output*)
name)
(define-symbol-macro +empty-html-elements+
'(:br :col :hr :img :input :link :meta))
(defun end-tag (name)
(unless (member name +empty-html-elements+)
(format *html-output* "</~A>" name)
name))
(defun process-html (forms)
(loop with (collector . environment) = (or *html-processor* '(print))
for original-form in forms as form = (html-macroexpand original-form environment)
do (multiple-value-bind (form form-evaluated-p) (macro-eval form environment)
(cond ((stringp form) (write-string form *html-output*))
((characterp form) (write-char form *html-output*))
(form-evaluated-p (escape-html form *in-attribute-p*))
(t (when (symbolp form)
(setq form `(escape-html ,form ,*in-attribute-p*)))
(multiple-value-bind (name attributes)
(parse-html-tag (first form))
(typecase name
((eql :attribute)
(let ((*in-attribute-p* t))
(process-html (rest form))))
((eql :process-with)
(or (whereas ((function (cadr form)))
(funcall function (cddr form)))
(error "~S needs function." :process-with)))
((eql :progn) (process-html (rest form)))
((not keyword) (funcall collector form))
(t (start-tag name attributes)
(process-html (rest form))
(end-tag name)))))))))
(defun constant-fold-html (processor)
(let ((collector (car processor)))
(let ((string (get-output-stream-string *html-output*)))
(when (zerop (length string)) (return-from constant-fold-html))
(funcall collector `(write-string ,string *html-output*) nil))))
(defmacro html
(&rest original-forms
&environment environment)
(collecting (lisp-forms)
(let* ((collector
#'(lambda (form &optional (foldp t))
(when foldp
(constant-fold-html *html-processor*))
(collect form :into lisp-forms)))
(*html-processor* (cons collector environment))
(*html-output* (make-string-output-stream))
(*in-attribute-p* nil))
(process-html original-forms)
(constant-fold-html *html-processor*)
`(progn ,.lisp-forms))))
(defmacro grab (&rest html-output-forms)
`(with-output-to-string (*html-output*)
(html ,@html-output-forms)))
(defun escape-html (object &optional in-attribute-p &key (start 0) end)
"Equivalent to PRINC'ing an object to *HTML-OUTPUT* but escaping HTML."
(prog1 object
(typecase object
((or number local-time) (princ object *html-output*))
(t (with-lexer ((%as-string object) start end)
(loop with stream = *html-output* and safe-start
with output of-type simple-string = ""
do (when (< (setq safe-start (lexer-skip (not "<>&\""))) lexer-position)
(write-string lexer-string stream :start safe-start :end lexer-position))
(lexer-case (char (loop-finish))
(#\" (if in-attribute-p
(setq output """)
(setq output "\"")))
(#\< (setq output "<"))
(#\> (setq output ">"))
(#\& (setq output "&")))
(write-string output stream)))))))
(defmacro define-html-macro (macro-name lambda-list &body body)
(multiple-value-bind (lambda-list whole-element environment)
(normalize-lambda-list lambda-list t t)
(orf environment (gensym (string 'environment)))
(orf whole-element (gensym (string 'whole-element)))
(with-gensyms (tag content)
(let ((attributes-tail (member '&attributes lambda-list)))
(multiple-value-bind (body declarations documentation) (parse-body body nil t)
(let ((attributes (require-type (if attributes-tail (second attributes-tail)) 'symbol))
(documenting-form `(setf (documentation ',macro-name 'html-macro) ',documentation)))
(when (prog1 attributes-tail (orf attributes (gensym (string 'attributes))))
(let ((preceding-lambda-list (ldiff lambda-list attributes-tail)))
(setq lambda-list (nconc preceding-lambda-list (cddr attributes-tail)))))
`(eval-always
(prog1 (prog1 ',macro-name ,documenting-form)
(setf (get ',macro-name 'html-macro-function)
#'(lambda (,whole-element ,environment)
(declare (ignorable ,environment))
(destructuring-bind (,tag &rest ,content) ,whole-element
(let ((,attributes (nth-value 1 (parse-html-tag ,tag))))
(declare (ignorable ,attributes))
(destructuring-bind ,lambda-list ,content
,.declarations (block ,macro-name ,@body))))))))))))))
(defun html-macro-function (name)
(get name 'html-macro-function))
(defun html-macroexpand-1 (form &optional environment)
(let ((element-name (when (consp form) (parse-html-tag (car form)))))
(whereas ((macro-function (when (symbolp element-name) (html-macro-function element-name))))
(return-from html-macroexpand-1 (values (funcall macro-function form environment) t)))
(values form nil)))
(defun html-macroexpand (form &optional environment)
(loop with expanded-p and any-expanded-p = nil
do (multiple-value-setq (form expanded-p)
(html-macroexpand-1 form environment))
while expanded-p do (setq any-expanded-p t)
finally (return (values form any-expanded-p))))
(define-html-macro
html (&rest html-forms)
`(:progn ,@html-forms))
(define-html-macro :string (string &key (start 0) end &environment environment)
(multiple-value-bind (string string-evaluated-p) (macro-eval string environment)
(multiple-value-bind (start start-evaluated-p) (macro-eval start environment)
(multiple-value-bind (end end-evaluated-p) (macro-eval end environment)
(cond ((and string-evaluated-p start-evaluated-p end-evaluated-p)
(subseq string start end))
((and (eql start 0) (not end)) `(write-string ,string *html-output*))
(t `(write-string ,string *html-output* :start ,start :end ,end)))))))
(define-html-macro :princ (object)
`(escape-html ,object ,*in-attribute-p*))
(define-html-macro :prin1 (object)
`(:princ (prin1-to-string ,object)))
(define-html-macro uri-to (object . parameters)
`(escape-html (uri-to ,object ,@parameters)
,*in-attribute-p*))
(define-html-macro absolute-uri-to (object . parameters)
`(escape-html (absolute-uri-to ,object ,@parameters)
,*in-attribute-p*))
(define-html-macro link (designator &rest attributes &environment environment)
(let* ((uri (uri (macro-eval designator environment))) (path (uri-path uri)))
`((:a :href ,uri ,@attributes) ,(uri path))))
(define-html-macro :ip-address (&attributes attributes ip-address)
(let ((ip-lookup-service "https://whatismyipaddress.com/ip/"))
(unless (getf attributes :class) (setq attributes `(:class "ip" ,@attributes)))
(rebinding (ip-address) (let ((href `(:progn ,ip-lookup-service ,ip-address)))
`(html ((:a :href ,href ,@attributes) ,ip-address))))))
(define-html-macro :format (control &rest arguments)
`(escape-html (format nil ,control ,@arguments)
,*in-attribute-p*))
(define-html-macro :when (test &body body &environment env)
(let ((non-constant-expression `(when ,test (html ,@body))))
(multiple-value-bind (test test-evaluated-p) (macro-eval test env)
(if test-evaluated-p (if test `(:progn ,@body) "") non-constant-expression))))
(define-html-macro :unless (test &body body &environment env)
(let ((non-constant-expression `(unless ,test (html ,@body))))
(multiple-value-bind (test test-evaluated-p) (macro-eval test env)
(if test-evaluated-p (if test "" `(:progn ,@body)) non-constant-expression))))
(define-html-macro :let ((&rest bindings) &body body &environment environment)
(multiple-value-bind (body declarations) (parse-body body environment nil)
`(let (,@bindings) ,.declarations (html ,@body))))
(define-html-macro :let* ((&rest bindings) &body body &environment environment)
(multiple-value-bind (body declarations) (parse-body body environment nil)
`(let* (,@bindings) ,.declarations (html ,@body))))
(define-html-macro :whereas
((&rest bindings) &body body)
`(whereas ,bindings (html ,@body)))
(define-html-macro :if (test-form then &optional (else "") &environment environment)
(multiple-value-bind (test-form test-evaluated-p) (macro-eval test-form environment)
(when test-evaluated-p (return-from :if (if test-form then else)))
`(if ,test-form (html ,then) (html ,else))))
(define-html-macro :standard-form (&attributes attributes &rest form-fields)
(let ((standard-submission-p (null (get-properties attributes '(:method :enctype)))))
`((:form ,@(unless (getf attributes :action) '(:action (:princ (request-uri *request*))))
,@(when standard-submission-p '(:method :post :enctype "multipart/form-data"))
,@attributes) ,@form-fields)))
(define-html-macro :table-form (&attributes attributes . form-fields)
`((:standard-form ,@attributes) ((:table :class "form") ,@form-fields)))
(define-html-macro :html-page (&attributes attributes (&rest head) body)
`(:progn "<!DOCTYPE HTML>" ((:html ,@attributes) (:head ,@head) ,body)))
(define-symbol-macro +canonical-xml-declaration+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
(defmacro with-xml-element (tag &body content)
`(html (:process-with process-xml-element
,tag ,@content)))
(defun process-xml-element (element)
(multiple-value-bind (name attributes)
(parse-html-tag (first element))
(write-char #\< *html-output*)
(princ name *html-output*)
(process-attributes attributes)
(if (endp (rest element))
(write-string "/>" *html-output*)
(progn (write-char #\> *html-output*)
(process-html (rest element))
(write-string "</" *html-output*)
(princ name *html-output*)
(write-char #\> *html-output*)))))
(define-html-macro with-xml-element (tag &body content)
`(:process-with process-xml-element ,tag ,@content))