SkillAgentSearch skills...

Chassis

Fast HTML5 templating in Clojure

Install / Use

/learn @onionpancakes/Chassis
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

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.

Status

Run tests

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>&amp; &lt; &gt;</div>"

(c/html [:div {:foo "& < > \" '"}])

;; "<div foo=\"&amp; &lt; &gt; &quot; &apos;\"></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 &amp; 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:

  1. Tag keyword. Used for the dispatch.
  2. Attributes map or nil if attrs is absent.
  3. 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 #id and .class merged 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

View on GitHub
GitHub Stars148
CategoryDevelopment
Updated1d ago
Forks1

Languages

Clojure

Security Score

100/100

Audited on Mar 23, 2026

No findings