{"id":15010378,"url":"https://github.com/onionpancakes/chassis","last_synced_at":"2025-04-04T18:05:08.765Z","repository":{"id":222027192,"uuid":"748959038","full_name":"onionpancakes/chassis","owner":"onionpancakes","description":"Fast HTML5 serialization for Clojure","archived":false,"fork":false,"pushed_at":"2025-02-16T07:14:13.000Z","size":890,"stargazers_count":111,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-28T17:07:45.103Z","etag":null,"topics":["clojure","hiccup","html","serialization","template"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/onionpancakes.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-01-27T06:42:17.000Z","updated_at":"2025-03-03T12:22:30.000Z","dependencies_parsed_at":"2024-02-16T22:57:45.539Z","dependency_job_id":"1569461b-1f02-4ad6-ae1d-44036c62a626","html_url":"https://github.com/onionpancakes/chassis","commit_stats":null,"previous_names":["onionpancakes/chassis"],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/onionpancakes%2Fchassis","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/onionpancakes%2Fchassis/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/onionpancakes%2Fchassis/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/onionpancakes%2Fchassis/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/onionpancakes","download_url":"https://codeload.github.com/onionpancakes/chassis/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247226213,"owners_count":20904465,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","hiccup","html","serialization","template"],"created_at":"2024-09-24T19:33:47.477Z","updated_at":"2025-04-04T18:05:08.743Z","avatar_url":"https://github.com/onionpancakes.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Chassis\n\nFast HTML5 serialization for Clojure.\n\nRenders [Hiccup](https://github.com/weavejester/hiccup/) style HTML vectors to strings.\n\nHighly optimized runtime serialization without macros. Even faster serialization when combined with compiling macros.\n\n* See [Compiling Elements](#compiling-elements).\n* See [Performance](#performance).\n\n# Status\n\n[![Run tests](https://github.com/onionpancakes/chassis/actions/workflows/run_tests.yml/badge.svg)](https://github.com/onionpancakes/chassis/actions/workflows/run_tests.yml)\n\nProduction released.\n\n# Deps\n\nAdd one of these deployments to `deps.edn`.\n\n### GitHub\n\n```clojure\ndev.onionpancakes/chassis {:git/url \"https://github.com/onionpancakes/chassis\"\n                           :git/tag \"v1.0.365\" :git/sha \"3e98fdc\"}\n```\n\n### Clojars\n\n```clojure\ndev.onionpancakes/chassis {:mvn/version \"1.0.365\"}\n```\n\n# Example\n\n### Runtime HTML Serialization\n\n```clojure\n(require '[dev.onionpancakes.chassis.core :as c])\n\n(defn my-post\n  [post]\n  [:div {:id (:id post)}\n   [:h2.title (:title post)]\n   [:p.content (:content post)]])\n\n(defn my-blog\n  [data]\n  [c/doctype-html5 ; Raw string for \u003c!DOCTYPE html\u003e\n   [:html\n    [:head\n     [:link {:href \"/css/styles.css\" :rel \"stylesheet\"}]\n     [:title \"My Blog\"]]\n    [:body\n     [:h1 \"My Blog\"]\n      (for [p (:posts data)]\n        (my-post p))]]])\n\n(let [data {:posts [{:id \"1\" :title \"foo\" :content \"bar\"}]}]\n  (c/html (my-blog data)))\n\n;; \"\u003c!DOCTYPE html\u003e\u003chtml\u003e\u003chead\u003e\u003clink href=\\\"/css/styles.css\\\" rel=\\\"stylesheet\\\"\u003e\u003ctitle\u003eMy Blog\u003c/title\u003e\u003c/head\u003e\u003cbody\u003e\u003ch1\u003eMy Blog\u003c/h1\u003e\u003cdiv id=\\\"1\\\"\u003e\u003ch2 class=\\\"title\\\"\u003efoo\u003c/h2\u003e\u003cp class=\\\"content\\\"\u003ebar\u003c/p\u003e\u003c/div\u003e\u003c/body\u003e\u003c/html\u003e\"\n```\n\n### Compiled HTML Serialization\n\n```clojure\n(require '[dev.onionpancakes.chassis.core :as c])\n(require '[dev.onionpancakes.chassis.compiler :as cc])\n\n(defn my-post-compiled\n  [post]\n  (cc/compile\n    [:div {:id (:id post)}\n     [:h2.title (:title post)]\n     [:p.content (:content post)]]))\n\n(defn my-blog-compiled\n  [data]\n  (cc/compile\n    [c/doctype-html5 ; Raw string for \u003c!DOCTYPE html\u003e\n     [:html\n      [:head\n       [:link {:href \"/css/styles.css\" :rel \"stylesheet\"}]\n       [:title \"My Blog\"]]\n      [:body\n       [:h1 \"My Blog\"]\n        (for [p (:posts data)]\n          (my-post-compiled p))]]]))\n\n(let [data {:posts [{:id \"1\" :title \"foo\" :content \"bar\"}]}]\n  (c/html (my-blog-compiled data)))\n\n;; \"\u003c!DOCTYPE html\u003e\u003chtml\u003e\u003chead\u003e\u003clink href=\\\"/css/styles.css\\\" rel=\\\"stylesheet\\\"\u003e\u003ctitle\u003eMy Blog\u003c/title\u003e\u003c/head\u003e\u003cbody\u003e\u003ch1\u003eMy Blog\u003c/h1\u003e\u003cdiv id=\\\"1\\\"\u003e\u003ch2 class=\\\"title\\\"\u003efoo\u003c/h2\u003e\u003cp class=\\\"content\\\"\u003ebar\u003c/p\u003e\u003c/div\u003e\u003c/body\u003e\u003c/html\u003e\"\n```\n\n# Usage\n\nRequire the namespace.\n\n```clojure\n(require '[dev.onionpancakes.chassis.core :as c])\n```\n\n## Elements\n\nUse `c/html` function to generate HTML strings from vectors.\n\nVectors 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.\n\n```clojure\n(c/html [:div \"foo\"])\n\n;; \"\u003cdiv\u003efoo\u003c/div\u003e\"\n```\n\nMaps in the second position are treated as attributes. Use **global keywords** to name attribute keys.\n\n```clojure\n(c/html [:div {:id \"my-id\"} \"foo\"])\n\n;; \"\u003cdiv id=\\\"my-id\\\"\u003efoo\u003c/div\u003e\"\n```\n\n```clojure\n;; Strings also accepted, but discouraged.\n;; Use when keywords cannot encode the desired attribute name.\n(c/html [:div {\"id\" \"my-id\"} \"foo\"])\n\n;; \"\u003cdiv id=\\\"my-id\\\"\u003efoo\u003c/div\u003e\"\n```\n\nThe 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](#non-element-vectors) are logically flattened with the rest of the content.\n\n```clojure\n(c/html [:div {:id \"my-id\"}\n         \"foo\"\n         (for [i (range 3)] i)\n         \"bar\"])\n\n;; \"\u003cdiv id=\\\"my-id\\\"\u003efoo012bar\u003c/div\u003e\"\n```\n\n## Id and Class Sugar\n\nLike Hiccup, id and class attributes can be specified along with the tag name using css style `#` and `.` syntax.\n\n```clojure\n(c/html [:div#my-id.my-class \"foo\"])\n\n;; \"\u003cdiv id=\\\"my-id\\\" class=\\\"my-class\\\"\u003efoo\u003c/div\u003e\"\n```\n\n```clojure\n;; Multiple '.' classes concatenates\n(c/html [:div.my-class-1.my-class-2 \"foo\"])\n\n;; \"\u003cdiv class=\\\"my-class-1 my-class-2\\\"\u003efoo\u003c/div\u003e\"\n```\n\n```clojure\n;; '.' classes concatenates with :class keyword\n(c/html [:div.my-class-1 {:class \"my-class-2\"} \"foo\"])\n\n;; \"\u003cdiv class=\\\"my-class-1 my-class-2\\\"\u003efoo\u003c/div\u003e\"\n```\n\n\n```clojure\n;; First '#' determines the id.\n;; Extra '#' are uninterpreted.\n(c/html [:div## \"foo\"])\n\n;; \"\u003cdiv id=\\\"#\\\"\u003efoo\u003c/div\u003e\"\n\n(c/html [:div#my-id.my-class-1#not-id \"foo\"])\n\n;; \"\u003cdiv id=\\\"my-id\\\" class=\\\"my-class-1#not-id\\\"\u003efoo\u003c/div\u003e\"\n```\n\nHowever there are differences from Hiccup.\n\n```clojure\n;; '#' id takes precedence over :id keyword\n(c/html [:div#my-id {:id \"not-my-id\"} \"foo\"])\n\n;; \"\u003cdiv id=\\\"my-id\\\"\u003efoo\u003c/div\u003e\"\n```\n\n```clojure\n;; '#' id can be place anywhere\n(c/html [:div.my-class-1#my-id \"foo\"])\n\n;; \"\u003cdiv id=\\\"my-id\\\" class=\\\"my-class-1\\\"\u003efoo\u003c/div\u003e\"\n```\n\n```clojure\n;; '#' id can be place in-between, but don't do this.\n;; It will be slightly slower.\n(c/html [:div.my-class-1#my-id.my-class-2 \"foo\"])\n\n;; \"\u003cdiv id=\\\"my-id\\\" class=\\\"my-class-1 my-class-2\\\"\u003efoo\u003c/div\u003e\"\n```\n\n## Boolean Attributes\n\nUse `true`/`false` to toggle boolean attributes.\n\n```clojure\n(c/html [:button {:disabled true} \"Submit\"])\n\n;; \"\u003cbutton disabled\u003eSubmit\u003c/button\u003e\"\n\n(c/html [:button {:disabled false} \"Submit\"])\n\n;; \"\u003cbutton\u003eSubmit\u003c/button\u003e\"\n```\n\n## Composite Attribute Values\n\nCollections of attribute values are concatenated as spaced strings.\n\n```clojure\n(c/html [:div {:class [\"foo\" \"bar\"]}])\n\n;; \"\u003cdiv class=\\\"foo bar\\\"\u003e\u003c/div\u003e\"\n\n(c/html [:div {:class #{:foo :bar}}])\n\n;; \"\u003cdiv class=\\\"bar foo\\\"\u003e\u003c/div\u003e\"\n```\n\nMaps of attribute values are concatenated as style strings.\n\n```clojure\n(c/html [:div {:style {:color  :red\n                       :border \"1px solid black\"}}])\n\n;; \"\u003cdiv style=\\\"color: red; border: 1px solid black;\\\"\u003e\u003c/div\u003e\"\n```\n\nAttribute collections and maps arbitrarily nest.\n\n```clojure\n(c/html [:div {:style {:color  :red\n                       :border [:1px :solid :black]}}])\n\n;; \"\u003cdiv style=\\\"color: red; border: 1px solid black;\\\"\u003e\u003c/div\u003e\"\n```\n\n## Write to Appendable\n\nAvoid intermediate allocation by writing directly to [`java.lang.Appendable`](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Appendable.html) using the `c/write-html` function.\n\nHowever, [`java.lang.StringBuilder`](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/StringBuilder.html) 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.\n\n```clojure\n(let [out (get-appendable-from-somewhere)]\n  (c/write-html out [:div \"foo\"]))\n```\n\n## Escapes\n\nText and attribute values are escaped by default.\n\n```clojure\n(c/html [:div \"\u0026 \u003c \u003e\"])\n\n;; \"\u003cdiv\u003e\u0026amp; \u0026lt; \u0026gt;\u003c/div\u003e\"\n\n(c/html [:div {:foo \"\u0026 \u003c \u003e \\\" '\"}])\n\n;; \"\u003cdiv foo=\\\"\u0026amp; \u0026lt; \u0026gt; \u0026quot; \u0026apos;\\\"\u003e\u003c/div\u003e\"\n```\n\nEscaping can be disabled locally by wrapping string values with `c/raw`.\n\n```clojure\n(c/html [:div (c/raw \"\u003cp\u003efoo\u003c/p\u003e\")])\n\n;; \"\u003cdiv\u003e\u003cp\u003efoo\u003c/p\u003e\u003c/div\u003e\"\n```\n\nEscaping can be disabled globally by altering vars. Change `c/escape-text-fragment` and `c/escape-attribute-value-fragment` to\n`identity` function to allow fragment values to pass through unescaped.\n\nThen use `c/escape-text` and `c/escape-attribute-value` to escape locally.\n\n```clojure\n(alter-var-root #'c/escape-text-fragment (constantly identity))\n(alter-var-root #'c/escape-attribute-value-fragment (constantly identity))\n\n(c/html [:div \"\u003cp\u003efoo\u003c/p\u003e\"])\n\n;; \"\u003cdiv\u003e\u003cp\u003efoo\u003c/p\u003e\u003c/div\u003e\"\n\n(c/html [:div (c/escape-text \"foo \u0026 bar\")])\n\n;; \"\u003cdiv\u003efoo \u0026amp; bar\u003c/div\u003e\"\n```\n\n### Vetted Unescaped Types\n\nFor performance, `java.lang.Number` and `java.util.UUID` are not escaped by default.\n\n### Tags and Attribute Keys Are Not Escaped!\n\nElement tags and attribute keys are not escaped. Be careful when placing dangerous text in these positions.\n\n```clojure\n;; uhoh\n(c/html [:\u003c\u003e \"This is bad!\"])\n\n;; \"\u003c\u003c\u003e\u003eThis is bad!\u003c/\u003c\u003e\u003e\"\n\n(c/html [:div {:\u003c\u003e \"This is bad!\"}])\n\n;; \"\u003cdiv \u003c\u003e=\\\"This is bad!\\\"\u003e\u003c/div\u003e\"\n```\n\n## Non-Element Vectors\n\nOnly 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.\n\n```clojure\n;; Not elements\n(c/html [0 1 2])                  ; =\u003e \"012\"\n(c/html [\"foo\" \"bar\"])            ; =\u003e \"foobar\"\n(c/html ^::c/content [:foo :bar]) ; =\u003e \"foobar\"\n\n;; Use this to generate fragments of elements\n(c/html [[:div \"foo\"]\n         [:div \"bar\"]]) ; \"\u003cdiv\u003efoo\u003c/div\u003e\u003cdiv\u003ebar\u003c/div\u003e\"\n```\n\n## Non-Attribute Keys\n\nOnly **global keywords** and **strings** are interpreted as attribute keys. Everything else is ignored.\n\n```clojure\n(c/html [:div {:foo/bar \"not here!\"}])\n\n;; \"\u003cdiv\u003e\u003c/div\u003e\"\n```\n\n## Alias Elements\n\nAlias elements are user defined elements. They resolve to other elements through the `c/resolve-alias` multimethod. They must begin with **namespaced keywords**.\n\nDefine alias elements by extending `c/resolve-alias` multimethod on a namespaced keyword. It accepts the following 3 arguments of types:\n\n1. Tag keyword. Used for the dispatch.\n2. Attributes map or nil if attrs is absent.\n3. Content vector, possibly empty if no content.\n\nWhen implementing aliases, consider the following points:\n\n* Because namespaced keywords are ignored as attributes, they can be used as arguments for alias elements.\n\n* 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.\n* The content vector has metadata `{::c/content true}` to avoid being interpreted as an element.\n\n```clojure\n;; Capitalized name optional, just to make it distinctive.\n(defmethod c/resolve-alias ::Layout\n  [_ {:layout/keys [title] :as attrs} content]\n  [:div.layout attrs ; Merge attributes\n   [:h1 title]\n   [:main content]\n   [:footer \"Some footer message.\"]])\n\n(c/html [::Layout#blog.dark {:layout/title \"My title!\"}\n         [:p \"My content!\"]])\n\n;; \"\u003cdiv id=\\\"blog\\\" class=\\\"layout dark\\\"\u003e\u003ch1\u003eMy title!\u003c/h1\u003e\u003cmain\u003e\u003cp\u003eMy content!\u003c/p\u003e\u003c/main\u003e\u003cfooter\u003eSome footer message.\u003c/footer\u003e\u003c/div\u003e\"\n```\n\n## Stateful Values\n\nInstances of `clojure.lang.IDeref` and `clojure.lang.Fn` are automatically dereferenced at serialization. Functions are invoked on their zero argument arity.\n\nWhether or not if this is a good idea is left to the user.\n\n```clojure\n(defn current-year []\n  (.getValue (java.time.Year/now)))\n\n(c/html [:footer \"My Company Inc \" current-year])\n\n;; \"\u003cfooter\u003eMy Company Inc 2024\u003c/footer\u003e\"\n```\n\n```clojure\n(def delayed-thing\n  (delay \"delayed\"))\n\n(c/html [:div {:foo delayed-thing}])\n\n;; \"\u003cdiv foo=\\\"delayed\\\"\u003e\u003c/div\u003e\"\n```\n\nThey can even deference into other elements.\n\n```clojure\n(defn get-children []\n  [:p \"Child element\"])\n\n(c/html [:div.parent get-children])\n\n;; \"\u003cdiv class=\\\"parent\\\"\u003e\u003cp\u003eChild element\u003c/p\u003e\u003c/div\u003e\"\n```\n\n## Token and HTML Serializers\n\nUse `c/token-serializer` and `c/html-serializer` to access individual tokens and fragment instances. The underlying type implements `clojure.lang.IReduceInit` and is intended to be used in a reduce.\n\n```clojure\n(-\u003e\u003e (c/token-serializer [:div \"foo\"])\n     (eduction (map type))\n     (vec))\n\n;; [dev.onionpancakes.chassis.core.OpeningTag\n;;  java.lang.String\n;;  dev.onionpancakes.chassis.core.ClosingTag]\n```\n\n```clojure\n(-\u003e\u003e (c/html-serializer [:div \"foo\"])\n     (vec))\n\n;; [\"\u003cdiv\u003e\" \"foo\" \"\u003c/div\u003e\"]\n```\n\n## RawString Constants\n\n### DOCTYPE\n\nUse `c/doctype-html5`, a `RawString` wrapping `\u003c!DOCTYPE html\u003e`. Because it is a `RawString`, it is safe to wrap in a vector to concatenate with the rest of the HTML document.\n\n```clojure\n(c/html [c/doctype-html5 [:html \"...\"]])\n\n;; \"\u003c!DOCTYPE html\u003e\u003chtml\u003e...\u003c/html\u003e\"\n```\n\n### \u0026amp;nbsp;\n\nUse the `c/nbsp` constant.\n\n```clojure\n(c/html [:div \"foo\" c/nbsp \"bar\"])\n\n;; \"\u003cdiv\u003efoo\u0026nbsp;bar\u003c/div\u003e\"\n```\n\n# Compiling Elements\n\nRequire the namespace.\n\n```clojure\n(require '[dev.onionpancakes.chassis.compiler :as cc])\n```\n\n## Compile Examples\n\nSlap a `cc/compile` wherever speed is needed! Then call `c/html` like normal to generate HTML.\n\n```clojure\n;; In defs\n(def global-element\n  (cc/compile [:div \"foo\"]))\n\n;; In defns\n(defn fn-element\n  [arg]\n  (cc/compile [:div \"foo\" arg \"bar\"]))\n\n;; In aliases\n(defmethod c/resolve-alias ::MyElement\n  [_ attrs content]\n  (cc/compile\n    [:div\n     [:p attrs content]]))\n\n;; In fn args\n(fn-element (cc/compile [:p \"some content\"]))\n\n;; Then call c/html like normal to generate HTML.\n(c/html (fn-element 123))\n\n;; \"\u003cdiv\u003efoo123bar\u003c/div\u003e\"\n```\n\n## Compile Usage\n\nChassis provides compiling macros `cc/compile` and `cc/compile*`. They take **one** argument, the root HTML tree, and they return compiled versions of the HTML tree. Use them to compile elements and pass their results to `c/html`.\n\n```clojure\n(defn my-element []\n  (cc/compile\n    [:div [:p \"foobar\"]]))\n\n(c/html (my-element))\n\n;; \"\u003cdiv\u003e\u003cp\u003efoobar\u003c/p\u003e\u003c/div\u003e\"\n```\n\nCompiling **flattens** and **compacts** the HTML tree, making subsequent calls to `c/html` much faster.\n\n```clojure\n(macroexpand-1 '(cc/compile [:div [:p \"foobar\"]]))\n\n;; Results in:\n#object[dev.onionpancakes.chassis.core.RawString 0x11c2d9a2 \"\u003cdiv\u003e\u003cp\u003efoobar\u003c/p\u003e\u003c/div\u003e\"]\n\n(let [body (identity \"some-dynamic-content\")]\n  (pprint\n    (macroexpand-1\n      '(cc/compile\n        [:div.deeply\n          [:div.nested\n            [:div.thing\n              [:p \"before\" body \"after\"]]]]))))\n\n;; Results in:\n[#object[dev.onionpancakes.chassis.core.RawString 0x66fd28ce \"\u003cdiv class=\\\"deeply\\\"\u003e\u003cdiv class=\\\"nested\\\"\u003e\u003cdiv class=\\\"thing\\\"\u003e\u003cp\u003ebefore\"]\n body\n #object[dev.onionpancakes.chassis.core.RawString 0xe9c5af6 \"after\u003c/p\u003e\u003c/div\u003e\u003c/div\u003e\u003c/div\u003e\"]]\n```\n\nUse `cc/compile` for most purposes. For performance, the returned value may or may not be a vector. This is so that compiling small fragments of fully compacted HTML (like `\u003chr\u003e`) is as efficient as possible when iterated over by `c/html`.\n\n```clojure\n;; \u003chr\u003e is not wrapped as a 1-sized vector\n(cc/compile [:hr])\n\n;; #object[dev.onionpancakes.chassis.core.RawString 0x6ba58490 \"\u003chr\u003e\"]\n\n;; The end result is the same either way,\n;; but the runtime serialization is faster this way.\n(-\u003e\u003e (range 10)\n     (interpose (cc/compile [:hr]))\n     (c/html))\n\n;; \"0\u003chr\u003e1\u003chr\u003e2\u003chr\u003e3\u003chr\u003e4\u003chr\u003e5\u003chr\u003e6\u003chr\u003e7\u003chr\u003e8\u003chr\u003e9\"\n```\n\nUse `cc/compile*` to ensure the return value is a vector. Otherwise, it is the same as `cc/compile`.\n\n```clojure\n;; \u003chr\u003e is wrapped as a 1-sized vector\n(cc/compile* [:hr])\n\n;; [#object[dev.onionpancakes.chassis.core.RawString 0x24f1caeb \"\u003chr\u003e\"]]\n```\n\n### Compiled Elements Must Have Literal Tags\n\nA small but subtle difference between `cc/compile` and `c/html` is that `cc/compile` assumes elements are **literal** vectors with **literal** keyword tags. Vectors without literal tags, after [var resolution](#var-resolved-constants), are assumed to be content.\n\n```clojure\n;; Basically don't do this.\n(let [footag :div]\n  (c/html (cc/compile [footag \"It's foobarred.\"])))\n\n;; \"divIt's foobarred.\"\n\n;; Works at runtime.\n(let [footag :div]\n  (c/html [footag \"It's foobarred.\"]))\n\n;; \"\u003cdiv\u003eIt's foobarred.\u003c/div\u003e\"\n```\n\n## Ambiguous Attributes Produce Speed Bumps\n\nAmbiguous objects in the second position forces the compiler to emit checks which examine the potential attributes map at runtime.\n\n```clojure\n(let [data {:body \"foo\"}]\n  (pprint (clojure.walk/macroexpand-all\n     ;; Compiler can't see what (:body data) returns.\n    '(cc/compile [:div (:body data)]))))\n\n;; Results in:\n[(let*\n  [attrs13712 (:body data)]\n  (if ;; Check if 2nd item is attrs map at runtime.\n   (dev.onionpancakes.chassis.core/attrs? attrs13712)\n   (dev.onionpancakes.chassis.core/-\u003eOpeningTag\n    nil\n    :div\n    nil\n    nil\n    attrs13712)\n   [#object[dev.onionpancakes.chassis.core.RawString 0x1cc8f6fb \"\u003cdiv\u003e\"]\n    attrs13712]))\n #object[dev.onionpancakes.chassis.core.RawString 0x6753cbe6 \"\u003c/div\u003e\"]]\n```\n\n### Resolving Ambiguity - Force Attributes Absence\n\nUse `nil` in second position to force compile the element without attributes.\n\n```clojure\n(let [data {:body \"foo\"}]\n  (pprint (macroexpand-1\n    '(cc/compile [:div nil (:body data)]))))\n\n;; Results in:\n[#object[dev.onionpancakes.chassis.core.RawString 0x6e42ae2e \"\u003cdiv\u003e\"]\n (:body data)\n #object[dev.onionpancakes.chassis.core.RawString 0x588c9f7d \"\u003c/div\u003e\"]]\n```\n\n### Resolving Ambiguity - Force Attributes Presence\n\nType hint the second position with either `java.util.Map` or `clojure.lang.IPersistentMap` to force compile elements with attributes.\n\n```clojure\n(let [data {:attrs {:foo \"bar\"}\n            :body  \"foo\"}]\n  (pprint (macroexpand-1\n    '(cc/compile [:div ^java.util.Map (:attrs data) (:body data)]))))\n\n;; Results in:\n[(dev.onionpancakes.chassis.core/-\u003eOpeningTag\n  nil\n  :div\n  nil\n  nil\n  (:attrs data))\n (:body data)\n #object[dev.onionpancakes.chassis.core.RawString 0x6314faa \"\u003c/div\u003e\"]]\n```\n\nType hinting the argument or bindings also works.\n* Note: It doesn't show up correctly in a `macroexpand`, but it does works normally. This is because `cc/compile` examines the type hints from macro implied arg `\u0026env`, and `macroexpand` for some reason doesn't capture `\u0026env`.\n\n```clojure\n;; Should work!\n(defmethod c/resolve-alias ::CompileWithAttrs\n  [_ ^java.util.Map attrs content]\n  (cc/compile [:div attrs content]))\n\n(let [^java.util.Map attrs {:foo \"bar\"}]\n  (cc/compile [:div attrs \"foobar\"]))\n```\n\n### Vetted Attributes Core Functions\n\nCertain functions in `clojure.core` which returns maps are considered as attributes when called in the second position. Type hinting these invocations is not necessary. They include:\n\n* `array-map`\n* `hash-map`\n* `sorted-map`\n* `sorted-map-by`\n* `assoc`\n* `assoc-in`\n* `merge`\n* `select-keys`\n* `update-keys`\n* `update-vals`\n\n```clojure\n;; Useful in aliases when merging attrs.\n(defmethod c/resolve-alias ::AliasWithAttrsMerge\n  [_ attrs content]\n  (cc/compile\n    [:div (merge {:foo \"bar\"} attrs)\n      content]))\n```\n\n### Warn on Ambiguous Attributes\n\nCall `(cc/set-warn-on-ambig-attrs!)` to turn on warnings when compiling elements with ambiguous attributes. It will add a tap which prints out warning messages to `*err*` whenever ambiguous attributes are compiled.\n\nCall `(cc/unset-warn-on-ambig-attrs!)` to disable.\n\n## Compilation Barriers\n\n### Function Calls\n\nFunctions calls, and generally any list values, block compilation traversal. Call `cc/compile` again to compile forms within.\n\n```clojure\n(defn comp-blocked\n  []\n  [:p \"blocked\"])\n\n(cc/compile [:div \"foo\" (comp-blocked) \"bar\"])\n\n;; Results in:\n[#object[dev.onionpancakes.chassis.core.RawString 0x67574bda \"\u003cdiv\u003efoo\"] \n [:p \"blocked\"]\n #object[dev.onionpancakes.chassis.core.RawString 0x565edf06 \"bar\u003c/div\u003e\"]]\n```\n\n### Alias Elements\n\nAlias elements are implemented as `c/resolve-alias` (via `c/resolve-alias-with-meta`) function calls. As a result, they also block compilation. However, the arguments passed to `c/resolve-alias` will be compiled.\n\n```clojure\n(defmethod c/resolve-alias ::CompileMyAlias\n  [_ attrs content]\n  [:div attrs content])\n\n(pprint\n (clojure.walk/macroexpand-all\n  '(cc/compile\n    [::CompileMyAlias {:foo \"bar\"}\n     [:p \"content 1\"]\n     [:p \"content 2\"]])))\n\n;; Results in:\n(dev.onionpancakes.chassis.core/resolve-alias-with-meta\n nil\n :user/CompileMyAlias\n {:foo \"bar\"}\n [#object[dev.onionpancakes.chassis.core.RawString 0x34e3a7d6 \"\u003cp\u003econtent 1\u003c/p\u003e\u003cp\u003econtent 2\u003c/p\u003e\"]])\n```\n\n### Macro Calls\n\nMacros are expanded during compilation. Like function calls, those which expand into lists block compilation.\n\n```clojure\n(pprint\n (cc/compile\n  [:ol\n   (for [i (range 4)]\n     [:li i])]))\n\n;; Results in:\n[[#object[dev.onionpancakes.chassis.core.OpeningTag 0x6e462cc4 \"\u003col\u003e\"]\n  ([:li 0] [:li 1] [:li 2] [:li 3])]\n #object[dev.onionpancakes.chassis.core.RawString 0x27b55932 \"\u003c/ol\u003e\"]]\n\n;; Manually call compile in the inner form to reach inside.\n(pprint\n (cc/compile\n  [:ol\n   (for [i (range 4)]\n     (cc/compile [:li i]))]))\n```\n\nMacros which expand into non-lists can participate in compilation. Therefore, it is possible to use macros to abstract element components in a compile friendly way.\n\nWhether or not if this is a good idea is left to the user.\n\n```clojure\n(defmacro NonBlockingElement\n  [content]\n  [:p nil content])\n\n(cc/compile [:div (NonBlockingElement \"not-blocked\")])\n\n;; Results in:\n#object[dev.onionpancakes.chassis.core.RawString 0x31b2d0a8 \"\u003cdiv\u003e\u003cp\u003enot-blocked\u003c/p\u003e\u003c/div\u003e\"]\n```\n\n## Var Resolved Constants\n\nSymbols referring to **vars** containing **constant values** are **resolved** to those values during compilation traversal, thereby allowing those constant values to participate in compilation. Constant types include `String`, `Long`, `IPersistentCollection` of constants, and `RawString` such as `c/doctype-html5` and `c/nbsp`. Use `cc/constant?` to check if values are constants.\n\n```clojure\n;; Fully compacted!\n;; Even with a symbol splitting content in the middle.\n(cc/compile [:div \"foo\" c/nbsp \"bar\"])\n\n;; Results in:\n#object[dev.onionpancakes.chassis.core.RawString 0x7fb21735 \"\u003cdiv\u003efoo\u0026nbsp;bar\u003c/div\u003e\"]\n```\n\n## Runtime Compilation\n\nChassis provides two analogous compile functions, `cc/compile-node` and `cc/compile-node*`, for compiling HTML tree at runtime. They are useful for compiling static HTML pages or components.\n\nBecause compiling happens at runtime, lists, function calls, and alias elements are no longer compilation barriers and ambiguous attributes are not possible.\n\nRuntime compilation is similar to generating HTML with `c/html` but with key differences:\n\n* The return values are `c/raw` strings, allowing the result to be embedded in other HTML components without the HTML tags being escaped.\n* Stateful values, such as functions and derefs, are not realized.\n\n```clojure\n(defn current-time []\n  (java.time.LocalTime/now))\n\n(defmethod c/resolve-alias ::CurrentTime\n  [_ _ _]\n  [:p \"Current time is: \" current-time])\n\n(def static-page\n  (cc/compile-node\n    [::CurrentTime]))\n\n;; Results in:\n[#object[dev.onionpancakes.chassis.core.RawString 0x7a702aaf \"\u003cp\u003eCurrent time is: \"]\n ;; Notice current-time function is not yet called.\n #object[user$current_time 0x584d9dc4 \"user$current_time@584d9dc4\"]\n #object[dev.onionpancakes.chassis.core.RawString 0x1c59c510 \"\u003c/p\u003e\"]]\n\n;; Stateful values realized on call to c/html\n(c/html static-page)\n\n;; \"\u003cp\u003eCurrent time is: 13:48:14.228299269\u003c/p\u003e\"\n```\n\n# Performance\n\nAt this time, benchmarks shows Chassis to be 2x faster (and often more!) when compared to other Clojure HTML templating libraries on equivalent benchmark examples.\n\nSee bench results in the resource folder.\n\n```clojure\n$ clj -M:dev\nClojure 1.11.1\n\n;; Chassis\n\nuser=\u003e (quick-bench (chassis-page data-mid))\nEvaluation count : 2712 in 6 samples of 452 calls.\n             Execution time mean : 229.730870 µs\n    Execution time std-deviation : 7.583674 µs\n   Execution time lower quantile : 221.593639 µs ( 2.5%)\n   Execution time upper quantile : 237.951723 µs (97.5%)\n                   Overhead used : 8.800684 ns\nnil\nuser=\u003e (quick-bench (chassis-page-compiled data-mid))\nEvaluation count : 4722 in 6 samples of 787 calls.\n             Execution time mean : 131.554387 µs\n    Execution time std-deviation : 4.400562 µs\n   Execution time lower quantile : 127.024648 µs ( 2.5%)\n   Execution time upper quantile : 137.206151 µs (97.5%)\n                   Overhead used : 8.800684 ns\nnil\nuser=\u003e (quick-bench (chassis-page-compiled-unambig data-mid))\nEvaluation count : 6186 in 6 samples of 1031 calls.\n             Execution time mean : 100.309952 µs\n    Execution time std-deviation : 3.392984 µs\n   Execution time lower quantile : 98.074419 µs ( 2.5%)\n   Execution time upper quantile : 105.031335 µs (97.5%)\n                   Overhead used : 8.800684 ns\nnil\n\n;; Hiccup\n\nuser=\u003e (quick-bench (hiccup-page data-mid))\nEvaluation count : 990 in 6 samples of 165 calls.\n             Execution time mean : 615.536499 µs\n    Execution time std-deviation : 15.886454 µs\n   Execution time lower quantile : 599.567903 µs ( 2.5%)\n   Execution time upper quantile : 637.703394 µs (97.5%)\n                   Overhead used : 8.800684 ns\nnil\nuser=\u003e (quick-bench (hiccup-page-compiled data-mid))\nEvaluation count : 1044 in 6 samples of 174 calls.\n             Execution time mean : 594.160734 µs\n    Execution time std-deviation : 15.249740 µs\n   Execution time lower quantile : 576.246477 µs ( 2.5%)\n   Execution time upper quantile : 611.946104 µs (97.5%)\n                   Overhead used : 8.800684 ns\nnil\nuser=\u003e (quick-bench (hiccup-page-compiled-unambig data-mid))\nEvaluation count : 2544 in 6 samples of 424 calls.\n             Execution time mean : 246.390352 µs\n    Execution time std-deviation : 6.001164 µs\n   Execution time lower quantile : 240.872342 µs ( 2.5%)\n   Execution time upper quantile : 255.422063 µs (97.5%)\n                   Overhead used : 8.800684 ns\nnil\n\n;; Selmer\n\nuser=\u003e (quick-bench (selmer-page data-mid))\nEvaluation count : 1428 in 6 samples of 238 calls.\n             Execution time mean : 455.954085 µs\n    Execution time std-deviation : 14.867158 µs\n   Execution time lower quantile : 443.374807 µs ( 2.5%)\n   Execution time upper quantile : 478.302764 µs (97.5%)\n                   Overhead used : 8.800684 ns\nnil\n\n;; Enlive\n\nuser=\u003e (quick-bench (enlive-page-item-html data-mid))\nEvaluation count : 282 in 6 samples of 47 calls.\n             Execution time mean : 2.254892 ms\n    Execution time std-deviation : 83.779038 µs\n   Execution time lower quantile : 2.156587 ms ( 2.5%)\n   Execution time upper quantile : 2.341325 ms (97.5%)\n                   Overhead used : 8.800684 ns\nnil\n```\n\n## Element Vector Allocation is Small\n\nElement vector allocation accounts for a small % of the runtime cost.\n\n```clojure\nuser=\u003e (quick-bench (page-doall data-mid))\nEvaluation count : 34752 in 6 samples of 5792 calls.\n             Execution time mean : 18.073864 µs\n    Execution time std-deviation : 623.107379 ns\n   Execution time lower quantile : 17.421242 µs ( 2.5%)\n   Execution time upper quantile : 18.715025 µs (97.5%)\n                   Overhead used : 8.800684 ns\nnil\n```\n\nThe vast proportion of the runtime cost is the iteration of HTML data structure and fragment writes.\n\n### It's All Interned\n\nKeywords and Strings are interned objects. Therefore the cost of allocating HTML vectors is mostly the cost of allocation vectors, and allocating vectors is really fast.\n\n# License\n\nReleased under the MIT License.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fonionpancakes%2Fchassis","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fonionpancakes%2Fchassis","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fonionpancakes%2Fchassis/lists"}