Skip to content

Instantly share code, notes, and snippets.

@ralt
Last active September 21, 2020 17:41
Show Gist options
  • Save ralt/6b37529f5d0e0302d92164d4b66e7d78 to your computer and use it in GitHub Desktop.
Save ralt/6b37529f5d0e0302d92164d4b66e7d78 to your computer and use it in GitHub Desktop.
horse-html: extension to Parenscript

My main gripe with who-ps-html is that it generates a string, which means that you don't have a real DOM object to play with; you must wait to render that object before being able to do things on it.

horse-html fixes that by generating real DOM elements in JavaScript, and returning those.

The absolute best feature of horse-html is that the closures also magically work. If you define an onclick on an element, the JavaScript will use the closures generated wherever your code is defining that onclick attribute.

PS: the code can definitely be improved. I suck. But for the little use cases I have, it works. Feedback definitely welcome. I'm notably not a fan of the nested functions, but couldn't figure out a better way.

(defmacro defreplacement (to-replace replacement)
`(setf (gethash ,to-replace *replacements*) ,replacement))
(defmacro horse-html (form)
(generate-dom-js form))
(eval-when (:compile-toplevel :load-toplevel :execute)
(defvar *replacements* (make-hash-table :test #'equal))
(defreplacement "class" "className")
(defun generate-dom-js (form &optional (toplevel t))
(cond ((and toplevel (keywordp (first form)))
`(let* ((tag-name ,(string-downcase (symbol-name (first form))))
(element (ps:chain document (create-element tag-name))))
,(cond ((stringp (first (rest form)))
`(setf (ps:@ element inner-text) ,(first (rest form))))
((keywordp (first (rest form)))
`(funcall (lambda () ,@(generate-dom-js (rest form) nil))))
(t
`(ps:chain
element
(append-child
(funcall (lambda () ,(generate-dom-js (first (rest form)))))))))
element))
((and (not toplevel) (listp (first form)))
(list
`(ps:chain element
(append-child
(funcall (lambda () ,(generate-dom-js (first form))))))))
((and (not toplevel) (keywordp (first form)))
;; attributes
(progn
(append
(list `(setf
(ps:@
element
,(let ((attr-name (string-downcase (symbol-name (first form)))))
(or (gethash attr-name *replacements*)
attr-name)))
,(second form)))
(list `(funcall (lambda () ,@(generate-dom-js (rest (rest form)) nil)))))))
((and (not toplevel) (stringp (first form)))
(list `(setf (ps:@ element inner-text) ,(first form))))
(t form))))
(ps:import-macros-from-lisp 'horse-html)
;; Usage example in parenscript:
(horse-html (:div :class "foo" :onclick (lambda () (chain console (log "clicked"))) (:span "oops")))
;; Generates:
(function () {
var tagName = 'div';
var element = document.createElement(tagName);
(function () {
element['className'] = 'foo';
return (function () {
element['onclick'] = function () {
return console.log('clicked');
};
return (function () {
return element.appendChild((function () {
var tagName81 = 'span';
var element82 = document.createElement(tagName81);
element82.innerText = 'oops';
return element82;
})());
})();
})();
})();
return element;
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment