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.