Chassis
Fast HTML5 templating in Clojure
Install / Use
/learn @onionpancakes/ChassisREADME
Chassis
Fast HTML5 templating in Clojure.
Renders Hiccup style HTML vectors to strings.
Highly optimized runtime serialization without macros. Even faster serialization when combined with compiling macros.
- See Compiling Elements.
- See Performance.
Status
Production released.
Deps
Add one of these deployments to deps.edn.
GitHub
dev.onionpancakes/chassis {:git/url "https://github.com/onionpancakes/chassis"
:git/tag "v1.0.365" :git/sha "3e98fdc"}
Clojars
dev.onionpancakes/chassis {:mvn/version "1.0.365"}
Example
Runtime HTML Serialization
(require '[dev.onionpancakes.chassis.core :as c])
(defn my-post
[post]
[:div {:id (:id post)}
[:h2.title (:title post)]
[:p.content (:content post)]])
(defn my-blog
[data]
[c/doctype-html5 ; Raw string for <!DOCTYPE html>
[:html
[:head
[:link {:href "/css/styles.css" :rel "stylesheet"}]
[:title "My Blog"]]
[:body
[:h1 "My Blog"]
(for [p (:posts data)]
(my-post p))]]])
(let [data {:posts [{:id "1" :title "foo" :content "bar"}]}]
(c/html (my-blog data)))
;; "<!DOCTYPE html><html><head><link href=\"/css/styles.css\" rel=\"stylesheet\"><title>My Blog</title></head><body><h1>My Blog</h1><div id=\"1\"><h2 class=\"title\">foo</h2><p class=\"content\">bar</p></div></body></html>"
Compiled HTML Serialization
(require '[dev.onionpancakes.chassis.core :as c])
(require '[dev.onionpancakes.chassis.compiler :as cc])
(defn my-post-compiled
[post]
(cc/compile
[:div {:id (:id post)}
[:h2.title (:title post)]
[:p.content (:content post)]]))
(defn my-blog-compiled
[data]
(cc/compile
[c/doctype-html5 ; Raw string for <!DOCTYPE html>
[:html
[:head
[:link {:href "/css/styles.css" :rel "stylesheet"}]
[:title "My Blog"]]
[:body
[:h1 "My Blog"]
(for [p (:posts data)]
(my-post-compiled p))]]]))
(let [data {:posts [{:id "1" :title "foo" :content "bar"}]}]
(c/html (my-blog-compiled data)))
;; "<!DOCTYPE html><html><head><link href=\"/css/styles.css\" rel=\"stylesheet\"><title>My Blog</title></head><body><h1>My Blog</h1><div id=\"1\"><h2 class=\"title\">foo</h2><p class=\"content\">bar</p></div></body></html>"
Usage
Require the namespace.
(require '[dev.onionpancakes.chassis.core :as c])
Elements
Use c/html function to generate HTML strings from vectors.
Vectors with global keywords in the head position are treated as normal HTML elements. The keyword's name is used as the element's tag name.
(c/html [:div "foo"])
;; "<div>foo</div>"
Maps in the second position are treated as attributes. Use global keywords to name attribute keys.
(c/html [:div {:id "my-id"} "foo"])
;; "<div id=\"my-id\">foo</div>"
;; Strings also accepted, but discouraged.
;; Use when keywords cannot encode the desired attribute name.
(c/html [:div {"id" "my-id"} "foo"])
;; "<div id=\"my-id\">foo</div>"
The rest of the vector is treated as the element's content. They may be of any type including other elements. Sequences, eductions, and non-element vectors are logically flattened with the rest of the content.
(c/html [:div {:id "my-id"}
"foo"
(for [i (range 3)] i)
"bar"])
;; "<div id=\"my-id\">foo012bar</div>"
Id and Class Sugar
Like Hiccup, id and class attributes can be specified along with the tag name using css style # and . syntax.
(c/html [:div#my-id.my-class "foo"])
;; "<div id=\"my-id\" class=\"my-class\">foo</div>"
;; Multiple '.' classes concatenates
(c/html [:div.my-class-1.my-class-2 "foo"])
;; "<div class=\"my-class-1 my-class-2\">foo</div>"
;; '.' classes concatenates with :class keyword
(c/html [:div.my-class-1 {:class "my-class-2"} "foo"])
;; "<div class=\"my-class-1 my-class-2\">foo</div>"
;; First '#' determines the id.
;; Extra '#' are uninterpreted.
(c/html [:div## "foo"])
;; "<div id=\"#\">foo</div>"
(c/html [:div#my-id.my-class-1#not-id "foo"])
;; "<div id=\"my-id\" class=\"my-class-1#not-id\">foo</div>"
However there are differences from Hiccup.
;; '#' id takes precedence over :id keyword
(c/html [:div#my-id {:id "not-my-id"} "foo"])
;; "<div id=\"my-id\">foo</div>"
;; '#' id can be place anywhere
(c/html [:div.my-class-1#my-id "foo"])
;; "<div id=\"my-id\" class=\"my-class-1\">foo</div>"
;; '#' id can be place in-between, but don't do this.
;; It will be slightly slower.
(c/html [:div.my-class-1#my-id.my-class-2 "foo"])
;; "<div id=\"my-id\" class=\"my-class-1 my-class-2\">foo</div>"
Boolean Attributes
Use true/false to toggle boolean attributes.
(c/html [:button {:disabled true} "Submit"])
;; "<button disabled>Submit</button>"
(c/html [:button {:disabled false} "Submit"])
;; "<button>Submit</button>"
Composite Attribute Values
Collections of attribute values are concatenated as spaced strings.
(c/html [:div {:class ["foo" "bar"]}])
;; "<div class=\"foo bar\"></div>"
(c/html [:div {:class #{:foo :bar}}])
;; "<div class=\"bar foo\"></div>"
Maps of attribute values are concatenated as style strings.
(c/html [:div {:style {:color :red
:border "1px solid black"}}])
;; "<div style=\"color: red; border: 1px solid black;\"></div>"
Attribute collections and maps arbitrarily nest.
(c/html [:div {:style {:color :red
:border [:1px :solid :black]}}])
;; "<div style=\"color: red; border: 1px solid black;\"></div>"
Write to Appendable
Avoid intermediate allocation by writing directly to java.lang.Appendable using the c/write-html function.
However, java.lang.StringBuilder is highly optimized and it may be faster to write to it (and then write the string out) than to write to the Appendable directly. Performance testing is advised.
(let [out (get-appendable-from-somewhere)]
(c/write-html out [:div "foo"]))
Escapes
Text and attribute values are escaped by default.
(c/html [:div "& < >"])
;; "<div>& < ></div>"
(c/html [:div {:foo "& < > \" '"}])
;; "<div foo=\"& < > " '\"></div>"
Escaping can be disabled locally by wrapping string values with c/raw.
(c/html [:div (c/raw "<p>foo</p>")])
;; "<div><p>foo</p></div>"
Escaping can be disabled globally by altering vars. Change c/escape-text-fragment and c/escape-attribute-value-fragment to
identity function to allow fragment values to pass through unescaped.
Then use c/escape-text and c/escape-attribute-value to escape locally.
(alter-var-root #'c/escape-text-fragment (constantly identity))
(alter-var-root #'c/escape-attribute-value-fragment (constantly identity))
(c/html [:div "<p>foo</p>"])
;; "<div><p>foo</p></div>"
(c/html [:div (c/escape-text "foo & bar")])
;; "<div>foo & bar</div>"
Vetted Unescaped Types
For performance, java.lang.Number and java.util.UUID are not escaped by default.
Tags and Attribute Keys Are Not Escaped!
Element tags and attribute keys are not escaped. Be careful when placing dangerous text in these positions.
;; uhoh
(c/html [:<> "This is bad!"])
;; "<<>>This is bad!</<>>"
(c/html [:div {:<> "This is bad!"}])
;; "<div <>=\"This is bad!\"></div>"
Non-Element Vectors
Only vectors beginning with keywords are interpreted as elements. Vectors can set their metadata {::c/content true} to avoid being interpreted as elements, even if they begin with keywords.
;; Not elements
(c/html [0 1 2]) ; => "012"
(c/html ["foo" "bar"]) ; => "foobar"
(c/html ^::c/content [:foo :bar]) ; => "foobar"
;; Use this to generate fragments of elements
(c/html [[:div "foo"]
[:div "bar"]]) ; "<div>foo</div><div>bar</div>"
Non-Attribute Keys
Only global keywords and strings are interpreted as attribute keys. Everything else is ignored.
(c/html [:div {:foo/bar "not here!"}])
;; "<div></div>"
Alias Elements
Alias elements are user defined elements. They resolve to other elements through the c/resolve-alias multimethod. They must begin with namespaced keywords.
Define alias elements by extending c/resolve-alias multimethod on a namespaced keyword. It accepts the following 3 arguments of types:
- Tag keyword. Used for the dispatch.
- Attributes map or nil if attrs is absent.
- Content vector, possibly empty if no content.
When implementing aliases, consider the following points:
-
Because namespaced keywords are ignored as attributes, they can be used as arguments for alias elements.
-
The attributes map will contain
#idand.classmerged from the element tag. By placing the alias element's attribute map as the attribute map of a resolved element, the attributes transfers seamlessly between the two. -
The content vector has metadata
{::c/content true}to avoid being interpreted as an element.
;; Capitalized name optional, just to make it distinctive.
(defmethod c/resolve-alias ::Layout
[_ {:layout/keys [title] :as attrs} content]
[:div.layout attrs ; Merge attributes
[:h1 title]
[:main content]
[:footer "Some footer message."]])
(c/html [::Layout#blog.dark {:lay
Related Skills
node-connect
335.2kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
82.5kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
335.2kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
commit-push-pr
82.5kCommit, push, and open a PR
