hsx/src/elements.lisp

305 lines
11 KiB
Common Lisp
Raw Normal View History

2024-02-09 01:37:34 +09:00
(uiop:define-package #:piccolo/elements
(:use #:cl)
(:local-nicknames (#:util #:piccolo/util))
(:local-nicknames (#:asc #:assoc-utils))
(:local-nicknames (#:lol #:let-over-lambda))
(:local-nicknames (#:alx #:alexandria))
(:export
;;; builtin HTML elements
;;; all html5 elements, e.g. div, nav, media, export in code except
;;; <time> and <map> conflicts with cl symbol, are defined and
;;; exported as |time|, |map|
#:html
;;; fragment lets you group elements without a wrapper element.
#:<>
;;; user defined elements
#:define-element
#:*expand-user-element*
;; for reference tag name, attributes and children elements in user
;; element definition
#:tag
#:children
#:attrs
;;; attribute accessing utilility
#:attrs-alist
#:make-attrs
#:copy-attrs
#:attr
#:delete-attr
;;; element slots
#:element-tag
#:element-attrs
#:element-children
#:user-element-expand-to
;;; the h macro for avoiding import all builtin html element functions
#:h
#:element-string
#:elem-str))
(in-package #:piccolo/elements)
2018-06-24 13:08:30 -04:00
(defclass element ()
((tag :initarg :tag
:accessor element-tag)
(attrs :initarg :attrs
:accessor element-attrs)
(children :initarg :children
:accessor element-children)))
(defclass builtin-element (element) ())
(defclass builtin-element-with-prefix (builtin-element)
((prefix :initarg :prefix
:accessor element-prefix)))
(defclass user-element (element)
((expand-to :initarg :expander
:accessor user-element-expander)))
(defclass fragment (element)
())
2018-06-28 10:09:21 -04:00
(defun make-builtin-element (&key tag attrs children)
2024-02-05 02:45:48 +09:00
(make-instance 'builtin-element
:tag tag
:attrs attrs
2024-02-09 01:37:34 +09:00
:children (util:escape-children children)))
2018-06-28 10:09:21 -04:00
(defun make-builtin-element-with-prefix (&key tag attrs children prefix)
2024-02-05 02:45:48 +09:00
(make-instance 'builtin-element-with-prefix
:tag tag
:attrs attrs
:prefix prefix
2024-02-09 01:37:34 +09:00
:children (util:escape-children children)))
2018-06-24 13:08:30 -04:00
2024-02-05 02:45:48 +09:00
(defun make-user-element (&key tag attrs children expander)
(make-instance 'user-element
:tag tag
:attrs attrs
:expander expander
2024-02-09 01:37:34 +09:00
:children (util:escape-children children)))
2018-06-28 10:09:21 -04:00
(defmethod user-element-expand-to ((element user-element))
(funcall (user-element-expander element)
(element-tag element)
(element-attrs element)
(element-children element)))
(defun make-fragment (&key children)
(make-instance 'fragment
:tag 'fragment
:attrs (make-attrs :alist nil)
2024-02-09 01:37:34 +09:00
:children (util:escape-children children)))
2018-06-28 10:09:21 -04:00
(defstruct (attrs (:constructor %make-attrs))
alist)
(defun make-attrs (&key alist)
2024-02-09 01:37:34 +09:00
(if util:*escape-html*
(%make-attrs :alist (util:escape-attrs-alist alist))
2018-06-28 10:09:21 -04:00
(%make-attrs :alist alist)))
2018-06-24 13:08:30 -04:00
2018-06-27 13:00:53 -04:00
(defmethod (setf attr) (value (attrs attrs) key)
2024-02-09 01:37:34 +09:00
(setf (asc:aget (attrs-alist attrs) key) value))
2018-06-27 10:01:18 -04:00
2018-06-27 13:00:53 -04:00
(defmethod delete-attr ((attrs attrs) key)
2024-02-09 01:37:34 +09:00
(asc:delete-from-alistf (attrs-alist attrs) key))
2018-06-27 10:01:18 -04:00
2018-06-27 13:00:53 -04:00
(defmethod attr ((attrs attrs) key)
2024-02-09 01:37:34 +09:00
(asc:aget (attrs-alist attrs) key))
2018-06-27 10:01:18 -04:00
2018-06-27 13:00:53 -04:00
(defmethod (setf attr) (value (element element) key)
(setf (attr (element-attrs element) key) value))
(defmethod delete-attr ((element element) key)
(delete-attr (element-attrs element) key))
(defmethod attr ((element element) key)
(attr (element-attrs element) key))
2018-06-27 10:01:18 -04:00
2018-06-27 09:32:41 -04:00
(defvar *builtin-elements* (make-hash-table))
2024-02-09 01:37:34 +09:00
(defun split-attrs-and-children (attrs-and-children)
(cond
((attrs-p (first attrs-and-children))
(values (first attrs-and-children) (lol:flatten (rest attrs-and-children))))
((asc:alistp (first attrs-and-children))
(values (make-attrs :alist (first attrs-and-children))
(lol:flatten (rest attrs-and-children))))
((listp (first attrs-and-children)) ;plist
(values (make-attrs :alist (util:plist-alist (first attrs-and-children)))
(lol:flatten (rest attrs-and-children))))
((hash-table-p (first attrs-and-children))
(values (make-attrs :alist (asc:hash-alist (first attrs-and-children)))
(lol:flatten (rest attrs-and-children))))
((keywordp (first attrs-and-children)) ;inline-plist
(loop for thing on attrs-and-children by #'cddr
for (k v) = thing
when (and (keywordp k) v)
collect (cons k v) into attrs
when (not (keywordp k))
return (values (make-attrs :alist attrs) (lol:flatten thing))
finally (return (values (make-attrs :alist attrs) nil))))
(t
(values (make-attrs :alist nil) (lol:flatten attrs-and-children)))))
2024-02-04 00:46:53 +09:00
(defun %html (&rest attrs-and-children)
(multiple-value-bind (attrs children)
(split-attrs-and-children attrs-and-children)
2024-02-05 02:45:48 +09:00
(make-builtin-element-with-prefix :tag "html"
:attrs attrs
:children children
:prefix "<!DOCTYPE html>")))
2024-02-04 00:46:53 +09:00
(defmacro html (&body attrs-and-children)
`(%html ,@attrs-and-children))
2018-06-27 09:32:41 -04:00
(setf (gethash :html *builtin-elements*) t)
2018-06-24 13:08:30 -04:00
(defmacro define-builtin-element (element-name)
2024-02-09 01:37:34 +09:00
(let ((%element-name (alx:symbolicate '% element-name)))
`(progn
(defun ,%element-name (&rest attrs-and-children)
(multiple-value-bind (attrs children)
(split-attrs-and-children attrs-and-children)
2024-02-09 01:37:34 +09:00
(make-builtin-element :tag (string-downcase (lol:mkstr ',element-name))
2024-02-05 02:45:48 +09:00
:attrs attrs
:children children)))
(defmacro ,element-name (&body attrs-and-children)
`(,',%element-name ,@attrs-and-children)))))
2018-06-24 13:08:30 -04:00
(defmacro define-and-export-builtin-elements (&rest element-names)
`(progn
,@(mapcan (lambda (e)
(list `(define-builtin-element ,e)
2024-02-09 01:37:34 +09:00
`(setf (gethash (alx:make-keyword ',e) *builtin-elements*) t)
2018-06-24 13:08:30 -04:00
`(export ',e)))
element-names)))
2018-06-24 20:42:55 -04:00
(define-and-export-builtin-elements
2018-06-24 13:08:30 -04:00
a abbr address area article aside audio b base bdi bdo blockquote
2024-02-05 02:45:48 +09:00
body br button canvas caption cite code col colgroup data datalist
dd del details dfn dialog div dl dt em embed fieldset figcaption
figure footer form h1 h2 h3 h4 h5 h6 head header hr i iframe
img input ins kbd label legend li link main |map| mark meta meter nav
noscript object ol optgroup option output p param picture pre progress
q rp rt ruby s samp script section select small source span strong
style sub summary sup svg table tbody td template textarea tfoot th
thead |time| title tr track u ul var video wbr)
2018-06-24 13:08:30 -04:00
2024-02-09 19:10:41 +09:00
(defparameter *boolean-attrs*
'(allowfullscreen async autofocus autoplay checked controls default defer
disabled formnovalidate inert ismap itemscope loop multiple muted nomodule
novalidate open playsinline readonly required reversed selected))
2018-06-24 13:08:30 -04:00
(defmethod print-object ((attrs attrs) stream)
(if (attrs-alist attrs)
2024-02-09 19:10:41 +09:00
(let ((alist (attrs-alist attrs)))
(dolist (pair alist)
(let ((key (car pair))
(value (cdr pair)))
(if (member key *boolean-attrs* :test #'string=)
(when value
(format stream " ~a" (string-downcase key)))
(format stream " ~a=~s" (string-downcase key) value)))))
2018-06-24 13:08:30 -04:00
(format stream "")))
2024-02-05 01:47:26 +09:00
(defparameter *self-closing-tags*
'(area base br col embed hr img input keygen
link meta param source track wbr))
(defun self-closing-p (tag)
(member (make-symbol (string-upcase tag))
*self-closing-tags*
:test #'string=))
2023-12-21 12:40:05 -05:00
2018-06-24 13:08:30 -04:00
(defmethod print-object ((element element) stream)
(if (element-children element)
(format stream (if (rest (element-children element))
"~@<<~a~a>~2I~:@_~<~@{~a~^~:@_~}~:>~0I~:@_</~a>~:>"
"~@<<~a~a>~2I~:_~<~a~:>~0I~:_</~a>~:>")
(element-tag element)
(element-attrs element)
(element-children element)
(element-tag element))
2023-12-21 12:40:05 -05:00
(format stream (if (self-closing-p (element-tag element))
"<~a~a>"
"<~a~a></~a>")
(element-tag element)
(element-attrs element)
(element-tag element))))
2018-06-24 13:08:30 -04:00
(defmethod print-object ((element builtin-element-with-prefix) stream)
2018-06-27 09:32:41 -04:00
(format stream "~a~%" (element-prefix element))
(call-next-method))
2024-02-09 01:37:34 +09:00
(lol:defmacro! define-element (name (&rest args) &body body)
(let ((%name (alx:symbolicate '% name)))
2024-02-03 22:37:58 +09:00
`(progn
(defun ,%name (&rest attrs-and-children)
2024-02-03 22:37:58 +09:00
(multiple-value-bind (,g!attrs ,g!children)
(split-attrs-and-children attrs-and-children)
(make-user-element
:tag (string-downcase ',name)
:attrs ,g!attrs
:children ,g!children
:expander (lambda (tag attrs ,g!exp-children)
(declare (ignorable tag attrs ,g!exp-children))
(let ((children (and ,g!exp-children
(make-fragment :children ,g!exp-children))))
(declare (ignorable children))
(let ,(mapcar (lambda (arg)
2024-02-09 01:37:34 +09:00
(list arg `(attr attrs (alx:make-keyword ',arg))))
args)
(progn ,@body)))))))
2024-02-03 22:37:58 +09:00
(defmacro ,name (&body attrs-and-children)
`(,',%name ,@attrs-and-children)))))
2018-06-27 09:32:41 -04:00
(defvar *expand-user-element* t)
(defmethod print-object ((element user-element) stream)
(if *expand-user-element*
(print-object (user-element-expand-to element) stream)
2018-06-27 09:32:41 -04:00
(call-next-method)))
(defun %<> (&rest children)
(make-fragment :children children))
(defmacro <> (&body children)
`(%<> ,@children))
(defmethod print-object ((element fragment) stream)
(if (element-children element)
(format stream (if (rest (element-children element))
"~<~@{~a~^~:@_~}~:>"
"~<~a~:>")
(element-children element))))
2024-02-09 01:37:34 +09:00
(defun html-element-p (node)
(and (symbolp node)
(not (keywordp node))
(gethash (alx:make-keyword node) *builtin-elements*)))
2018-06-27 09:32:41 -04:00
(defmacro h (&body body)
`(progn
2024-02-09 01:37:34 +09:00
,@(util:modify-first-leaves
2018-06-27 09:32:41 -04:00
body
2024-02-09 01:37:34 +09:00
(lambda (node)
(declare (ignorable node))
(or (html-element-p node) (string= node '<>)))
(lambda (node)
(declare (ignorable node))
(find-symbol (string-upcase node) :piccolo)))))
2018-06-30 21:57:33 -04:00
(defmethod element-string ((element element))
(with-output-to-string (s)
(write element :stream s)))
(defmethod elem-str ((element element))
(with-output-to-string (s)
(write element :stream s :pretty nil)))