common lisp html dsl in one function
the function⌗
(defvar *html-format* :string)
(defun ->html (args)
(unless (equalp *html-format* :string)
(return-from ->html args))
(let* ((tag (format nil "~(~a~)" (elt args 0)))
(content "")
(attr ""))
(loop for i from 1 below (length args) do
(let ((x (elt args i))
(y (ignore-errors (elt args (1+ i)))))
(cond
((keywordp x)
(when (stringp y) (incf i))
(setf attr (format nil "~a ~a" attr (if (stringp y) (format nil "~(~a~)='~a'" x y) x))))
((consp x)
(setf content (format nil "~a~%~a" content (->html x))))
((stringp x)
(setf content (format nil "~a~a" content x))))))
(if (equalp content "")
(format nil "<~a~a />" tag attr)
(format nil "<~a~a>~a</~a>" tag attr content tag))))
why⌗
I tried to use cl-who
and have stumbled on some issues. Namely that I found it annoying to compose conditional elements because cl-who
uses macros not functions.
This also means component-like architechure would require using macros not function which I find unnecessary.
For reference, here’s how a component-like structure would like in ->html
:
(defun ->title(content)
`((title ,content)
(meta :name "title" :content ,content)
(meta :property "og:title" :content ,content)))
(defun ->description (content)
`((meta :name "description" :content ,content)
(meta :property "og:description" :content ,content)))
(defun ->page (title desc args)
(declare (string title desc))
(->html `(html
(head
(meta :charset "utf-8")
,@(->title title)
,@(->description desc)
(meta :name "viewport" :content "width=device-width, initial-scale=1")
(body
,args)))))
(defun ->/ ()
(->page "My Index Page" "My description for my Index page."
(h1 "Hello World")))
how ->html
works⌗
First, it checks if the global variable *html-format*
is set to :string
. If it isn’t, it returns the arguments as they are without processing.
This is particularily useful if the user wants to debug their HTML output, so they would:
;; print the HTML as list before processing
(let ((*html-format* :list))
(print (->/)))
Second, it loops over the elements in the list provided. It skips the first element and starts processing.
If the item is a :keyword
, it peeks for the next element. If it exists and it is a string, it forms an HTML attribute and skips the next element. If it doesn’t exist or it isn’t a string, it writes an empty attribute.
For example:
(h1 :disabled)
;; becomes
;; <h1 disabled>
(h1 :disabled "no")
;; <h1 disabled="no">
If the item is a "string"
, it writes it to the content string.
For example:
(h1 "sdf")
;; becomes
;; <h1>sdf</h1>
If the item is a (list)
, it calls ->html
and writes it to the content string.
(h1 (p "sdf"))
;; becomes
;; <h1><p>sdf</p></h1>
Lastly, it composes the elements together. The tag, attribute and content. If there is no content, the elements is closed by itself like so:
(h1 :disabled "true")
;; becomes
;; <h1 disabled="true" />
performance⌗
This function could be more performant.
conditional rendering⌗
(->html
`(div :class "container"
,(if t
`(p "This will render")
`(p "This will not render"))))
;;; "<div class='container'>
;;; <p>This will render</p></div>"
looping over items⌗
(->html
`(div :class "container"
,@(loop for x from 1 to 10 when (oddp x) collect
`(p ,(format nil "Item ~a" x)))))
;;; "<div class='container'>
;;; <p>Item 1</p>
;;; <p>Item 2</p>
;;; <p>Item 3</p>
;;; <p>Item 4</p>
;;; <p>Item 5</p>
;;; <p>Item 6</p>
;;; <p>Item 7</p>
;;; <p>Item 8</p>
;;; <p>Item 9</p>
;;; <p>Item 10</p></div>"
looping over items conditionally⌗
(->html
`(div :class "container"
,@(loop for x from 1 to 10 when (oddp x) collect
`(p ,(format nil "Item ~a" x)))))
;;; "<div class='container'>
;;; <p>Item 1</p>
;;; <p>Item 3</p>
;;; <p>Item 5</p>
;;; <p>Item 7</p>
;;; <p>Item 9</p></div>"
other things⌗
Since the function uses Common Lisp lists, that allows it to inherit all the amazing features that Common Lisp lists have. append
, mapcar
, quote
, quasiquote
, unquote
, sort
.
license⌗
public domain.