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.