{"id":31595271,"url":"https://github.com/escherize/huff","last_synced_at":"2025-10-06T03:59:04.626Z","repository":{"id":173058448,"uuid":"650169615","full_name":"escherize/huff","owner":"escherize","description":"Juicy hiccup in pure Clojure","archived":false,"fork":false,"pushed_at":"2025-07-13T14:43:31.000Z","size":599,"stargazers_count":117,"open_issues_count":0,"forks_count":5,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-09-20T16:24:02.186Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"epl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/escherize.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":null,"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}},"created_at":"2023-06-06T13:44:30.000Z","updated_at":"2025-09-16T16:51:49.000Z","dependencies_parsed_at":null,"dependency_job_id":"8358a5a2-4813-4d1d-87cc-69c0b174f218","html_url":"https://github.com/escherize/huff","commit_stats":null,"previous_names":["escherize/huff"],"tags_count":20,"template":false,"template_full_name":null,"purl":"pkg:github/escherize/huff","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/escherize%2Fhuff","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/escherize%2Fhuff/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/escherize%2Fhuff/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/escherize%2Fhuff/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/escherize","download_url":"https://codeload.github.com/escherize/huff/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/escherize%2Fhuff/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":278556181,"owners_count":26006081,"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","status":"online","status_checked_at":"2025-10-06T02:00:05.630Z","response_time":65,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":[],"created_at":"2025-10-06T03:58:52.971Z","updated_at":"2025-10-06T03:59:04.617Z","avatar_url":"https://github.com/escherize.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# huff\n\nHiccup in pure Clojure\n\n## Usage\n\n[![Clojars Project](https://img.shields.io/clojars/v/io.github.escherize/huff.svg)](https://clojars.org/io.github.escherize/huff)\n\n``` clojure\n(require '[huff2.core :as h])\n```\n\n## Rationale\n\nWhen it comes to hiccup libraries, there's a venn-diagram \"has ergonomic and modern affordances\" and \"works on babashka\". So [huff](https://github.com/escherize/huff) is my way of saying why not both?\n\n- [Weavejester's hiccup library](https://github.com/weavejester/hiccup) runs on babashka, but is missing some of the newer features hiccup afficianados have come to demand.\n \n- [Lambda Island's hiccup](https://github.com/lambdaisland/hiccup) also provides a modern api, but overall I'd still call it a subset of huff's features.\n \n## Features\n\n- 🏭 Use [**functions** as **components**](#use-functons-as-components) \n- 🎨 Style maps work as you'd expect `[:div {:style {:font-size 30}}]`\n- 🔀 Include classes and ids tags [in any order](#tag-parsing)\n  - `:div.a#id.b` or `:div.a.c#id` or `:#id.a.c` all work!\n- 🔒️ HTML-encoded by default\n- 👵 Runs on [babashka](https://github.com/babashka/babashka) (unlike `lambdaisland/hiccup`) \n- 🏎 Performance: 22-48% faster than hiccup/hiccup for runtime-generated HTML [without pre-compilation](https://github.com/escherize/huff/issues/8) \n- 🙂 Hiccup-style fragments to return multiple forms `(list [:li.a] [:li.b])` \n- 🙃 Reagent-style fragments to return multiple forms `[:\u003c\u003e [:li.a] [:li.b]]` \n- 🤐 Extreme shorthand syntax `[:. {:color :red}]` `\u003cdiv color=red\u003e\u003c/div\u003e` \n- 🦺 Tested against slightly modified hiccup 2 tests \n- 🪗 [Extensible grammar](#extendable-grammar)\n- 📦 [raw-strings](https://github.com/escherize/huff/issues/5)\n\n### Tag Parsing\n\nParse tags for id and class (in any order).\n\n```clojure\n(h/html [:div.hello#world \"!\"])\n;; =\u003e \u003cdiv class=\"hello\" id=\"world\"\u003e!\u003c/div\u003e\n```\n\n#### Nested tag parsing\n\n```clojure\n(println (h/html [:div.left-aligned\u003ep#user-parent\u003espan {:id \"user-name\"} \"Jason\"]))\n\n;=\u003e \u003cdiv class=\"left-aligned\"\u003e\u003cp id=\"user-parent\"\u003e\u003cspan id=\"user-name\"\u003eJason\u003c/span\u003e\u003c/p\u003e\u003c/div\u003e\n```\n\n### [reagent](https://github.com/reagent-project/reagent)-like fragment tags\n\n```clojure\n(h/html [:\u003c\u003e [:div \"d\"] [:\u003c\u003e [:\u003c\u003e [:span \"s\"]]]])\n;; =\u003e \n\u003cdiv\u003ed\u003c/div\u003e\u003cspan\u003es\u003c/span\u003e\n```\n\nThis is useful for returning multiple elements from a function:\n\n```clojure\n(defn twins [x] [:\u003c\u003e\n                 [:div.a x]\n                 [:div.b x]])\n\n(h/html [:span.parent [twins \"elements\"]])\n;;=\u003e\n\u003cspan class=\"parent\"\u003e\n  \u003cdiv class=\"a\"\u003eelements\u003c/div\u003e\n  \u003cdiv class=\"b\"\u003eelements\u003c/div\u003e\n\u003c/span\u003e\n\n```\n\nNest and combine them with lists to better convey intent to expand:\n\n``` clojure\n(h/html\n  [:ol\n   [:\u003c\u003e (for [x [1 2]]\n          [:li\u003ep.green {:id (str \"id-\" x)} x])]])\n\n;;=\u003e\n\u003col\u003e\n \u003cli\u003e\n   \u003cp id=\\\"id-1\\\" class=\\\"green\\\"\u003e1\u003c/p\u003e\n \u003c/li\u003e\n \u003cli\u003e\n   \u003cp id=\\\"id-2\\\" class=\\\"green\\\"\u003e2\u003c/p\u003e\n \u003c/li\u003e\n\u003c/ol\u003e\n\n```\n\n### Style map rendering\n\n```clojure\n(h/html [:div {:style {:border \"1px red solid\"\n                       :background-color \"#ff00ff\"}}])\n;; =\u003e \u003cdiv style=\"background-color:#ff00ff;border:1px red solid;\"\u003e\u003c/div\u003e\n\n(h/html [:. {:style {:width (h/px 3)}}])\n;;=\u003e \u003cdiv style=\\\"width:3px;\\\"\u003e\u003c/div\u003e\n```\n\n### Raw HTML tag:\n\nThis is nice if you want to e.g. render markdown in the middle of your hiccup. Note: We disable this by default and it must be manually enabled in the call to `html` or `page`,\n\n``` clojure\n\n(h/html [:hiccup/raw-html \"\u003cdiv\u003eraw\u003c/div\u003e\"])\n;; =Throws=\u003e :hiccup/raw-html is not allowed. Maybe you meant to set allow-raw to true?\n\n(h/html {:allow-raw true} [:hiccup/raw-html \"\u003cdiv\u003eraw\u003c/div\u003e\"])\n;;=\u003e \u003cdiv\u003eraw\u003c/div\u003e\n```\n\nAnother nice-to-have is to disallow raw html in un-trusted data getting passed into to the compiler, but being able to do that as a dev.\n\n``` clojure\n(h/html [:div [:hiccup/raw-html (h/raw-string \"\u003c!-- oops.\")]])\n;; =\u003e \u003cdiv\u003e\u003c!-- oops.\u003c/div\u003e\n```\n\n[More Info]\n\n### Use functons as components\n\nWrite a function that returns hiccup, and call it from the first position of a vector, like in [reagent](https://cljdoc.org/d/reagent/reagent/1.2.0/doc/tutorials/using-square-brackets-instead-of-parentheses-#using-greet-via--1).\n\n```clojure\n(defn str-info [s]\n  [:div.info\n   [:span (apply str (reverse s))]\n   [:pre.len \"Length: \" (count s)]])\n\n(h/html [str-info \"hello\"])\n;; =\u003e \n\u003cdiv class=\"info\"\u003e\n  \u003cspan\u003eolleh\u003c/span\u003e\n  \u003cpre class=\"len\"\u003eLength: 5\u003c/pre\u003e\n\u003c/div\u003e\n```\n\n## Extensible Grammar\n\nNow you can handcraft your own hiccup grammar. With this power, you can write new tags that can parse (and validate) their inputs.\n\n### Example:\n\nLet's say you _really_ need a tag to count its children, and put that into the final html.\n\n1. Add your new tag to the hiccup schema:\n``` clojure\n(def my-schema (h2e/add-schema-branch h/hiccup-schema :my/child-counter-tag))\n```\n\n2. Write the emitter function for your tag:\n``` clojure\n(defmethod h/emit :my/child-counter-tag [append! {[_ values] :value} opts]\n  (append! \"I have \" (count values) \" children.\"))\n```\n\n`append!` takes strings and will append them internally to a StringBuilder during html generation.\n\n3. Call huff2.core/html with your new schema:\n\n``` clojure\n;; call:\n(h/html (custom-fxns! my-schema)\n  [:div\u003eh1 [:my/child-counter-tag \"one\" \"two\" \"three\"]])\n  ;; =\u003e \u003cdiv\u003e\u003ch1\u003eI have 3 children.\u003c/h1\u003e\u003c/div\u003e\n```\n\nThis will be a little faster, and you should prefer it if your schema isnt dynamic.\n\n``` clojure\n;; build:\n(let [my-fxns (custom-fxns! my-schema)]\n  (def my-html (fn my-html [hic] (h/html my-fxns))))\n  \n(my-html [:div\u003eh1 [:my/child-counter-tag \"one\" \"two\" \"three\"]])\n;; =\u003e \u003cdiv\u003e\u003ch1\u003eI have 3 children.\u003c/h1\u003e\u003c/div\u003e\n```\n\nMore details in the [huff extension tests](./test/huff/extension_test.clj).\n\n## Prior Art\n\n- [hiccup](https://github.com/weavejester/hiccup)\n- [lambdaisland/hiccup](https://github.com/lambdaisland/hiccup)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fescherize%2Fhuff","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fescherize%2Fhuff","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fescherize%2Fhuff/lists"}