From 868b2544d861778f6baf87a599960ac85e7a4f16 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Mon, 14 Sep 2026 22:39:58 +0200 Subject: [PATCH 1/3] Omit nil and false attributes, render aria-* and enumerated booleans as strings --- src/borkdude/html.cljc | 31 ++++++++++++++++------------ test/borkdude/html_test.cljc | 39 ++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/src/borkdude/html.cljc b/src/borkdude/html.cljc index ae41c26..d70de3c 100644 --- a/src/borkdude/html.cljc +++ b/src/borkdude/html.cljc @@ -32,20 +32,28 @@ (str (name k) ": " (name v) ";")) m))) +(defn- string-boolean-attr? [k] + (or (str/starts-with? k "aria-") + (contains? #{"contenteditable" "draggable" "spellcheck"} k))) + (defn ->attrs "Implementation, do not use" ([opts m] (let [xml? (= :xml (:mode opts))] - (str/join " " - (map (fn [[k v]] - (if (and (true? v) (not xml?)) - (name k) - (str (name k) + (apply str + (keep (fn [[k v]] + (let [k (name k)] + (cond + (nil? v) nil + (and (boolean? v) (not xml?) (not (string-boolean-attr? k))) + (when v (str " " k)) + :else + (str " " k "=" (cond (string? v) (pr-str (escape-html v)) (keyword? v) (pr-str (name v)) - (and (map? v) (= "style" (name k))) (pr-str (escape-html (->css v))) - :else (pr-str (escape-html (str v))))))) - m)))) + (and (map? v) (= "style" k)) (pr-str (escape-html (->css v))) + :else (pr-str (escape-html (str v)))))))) + m)))) ([opts m base-map] (let [m (merge base-map m)] (->attrs opts m)))) @@ -126,10 +134,7 @@ omit-tag? (= "<>" tag) unsafe? (= "$" tag) attrs (if ?attrs - (let [a (compile-attrs opts ?attrs)] - (if (string? a) - (str " " a) - a)) + (compile-attrs opts ?attrs) "")] (if unsafe? `(->Html (str ~(first children))) @@ -137,7 +142,7 @@ nil (if (string? attrs) [(str "<" tag attrs ">")] - ["<" tag " " attrs ">"])) + ["<" tag attrs ">"])) ~@(map #(list (if xml? `xml `html) %) children) diff --git a/test/borkdude/html_test.cljc b/test/borkdude/html_test.cljc index b93bdca..c3a2110 100644 --- a/test/borkdude/html_test.cljc +++ b/test/borkdude/html_test.cljc @@ -81,6 +81,45 @@ "" (xml [:input {:checked true}]) + "" + (html [:input {:disabled false}]) + + "" + (html [:input {:disabled nil}]) + + "" + (let [d false] + (html [:input {:type "text" :disabled d}])) + + "" + (xml [:input {:disabled false}]) + + "" + (xml [:input {:disabled nil}]) + + "" + (html [:button {:aria-expanded true}]) + + "" + (let [b false] + (html [:button {:aria-expanded b}])) + + "" + (html [:span {:aria-label nil}]) + + "
" + (html [:div {:spellcheck false}]) + + "
" + (html [:div {:draggable true}]) + + "
" + (html [:div {}]) + + "
" + (let [m {:disabled false}] + (html [:div {:& m}])) + "" (html [:a {:href (str "http://" "dude")}]) From 0932fedd325827806e15a8d9f77c8a2a94c97be4 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Mon, 14 Sep 2026 22:49:13 +0200 Subject: [PATCH 2/3] Render data-* booleans as strings, add changelog entries --- CHANGELOG.md | 3 +++ src/borkdude/html.cljc | 1 + test/borkdude/html_test.cljc | 10 ++++++++++ 3 files changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e97b8ee..d954e8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ Unreleased changes are available via `io.github.borkdude/html {:git/sha "..."}` ## Unreleased - Fix shorthand classes being dropped when the attribute map has no `:class`: `[:div.card {:id "a"}]` rendered `
`. The same path interpolated a symbol's _name_ when `:class` was bound to an expression, so `(let [x "big"] [:div.card {:class x}])` rendered `class="card x"`. +- Omit attributes whose value is `nil`, or `false` in html mode: `[:input {:disabled false}]` rendered `disabled="false"`, which disables the input. This also applies to non-boolean attributes: `[:option {:value false}]` now renders ``. Matches hiccup ([#19](https://github.com/borkdude/html/pull/19), [@telekid](https://github.com/telekid)) +- Render `true` and `false` as `"true"` and `"false"` for `aria-*`, `data-*`, `contenteditable`, `draggable` and `spellcheck` +- Fix `[:div {}]` rendering `
` ## 0.2.6 diff --git a/src/borkdude/html.cljc b/src/borkdude/html.cljc index d70de3c..2da6968 100644 --- a/src/borkdude/html.cljc +++ b/src/borkdude/html.cljc @@ -34,6 +34,7 @@ (defn- string-boolean-attr? [k] (or (str/starts-with? k "aria-") + (str/starts-with? k "data-") (contains? #{"contenteditable" "draggable" "spellcheck"} k))) (defn ->attrs diff --git a/test/borkdude/html_test.cljc b/test/borkdude/html_test.cljc index c3a2110..5db5a6d 100644 --- a/test/borkdude/html_test.cljc +++ b/test/borkdude/html_test.cljc @@ -113,6 +113,16 @@ "
" (html [:div {:draggable true}]) + "
" + (html [:div {:data-active false}]) + + "
" + (let [a true] + (html [:div {:data-active a}])) + + "" + (html [:option {:value false}]) + "
" (html [:div {}]) From a2cda78338d6076d55ced1cc66b44ea7242833dc Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Mon, 14 Sep 2026 23:00:23 +0200 Subject: [PATCH 3/3] Match string boolean attributes in any case, add focusable --- CHANGELOG.md | 2 +- src/borkdude/html.cljc | 7 ++++--- test/borkdude/html_test.cljc | 6 ++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d954e8b..c6537d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ Unreleased changes are available via `io.github.borkdude/html {:git/sha "..."}` - Fix shorthand classes being dropped when the attribute map has no `:class`: `[:div.card {:id "a"}]` rendered `
`. The same path interpolated a symbol's _name_ when `:class` was bound to an expression, so `(let [x "big"] [:div.card {:class x}])` rendered `class="card x"`. - Omit attributes whose value is `nil`, or `false` in html mode: `[:input {:disabled false}]` rendered `disabled="false"`, which disables the input. This also applies to non-boolean attributes: `[:option {:value false}]` now renders ``. Matches hiccup ([#19](https://github.com/borkdude/html/pull/19), [@telekid](https://github.com/telekid)) -- Render `true` and `false` as `"true"` and `"false"` for `aria-*`, `data-*`, `contenteditable`, `draggable` and `spellcheck` +- Render `true` and `false` as `"true"` and `"false"` for `aria-*`, `data-*`, `contenteditable`, `draggable`, `focusable` and `spellcheck`. Names match in any case, so `:spellCheck` works as well - Fix `[:div {}]` rendering `
` ## 0.2.6 diff --git a/src/borkdude/html.cljc b/src/borkdude/html.cljc index 2da6968..3009219 100644 --- a/src/borkdude/html.cljc +++ b/src/borkdude/html.cljc @@ -33,9 +33,10 @@ m))) (defn- string-boolean-attr? [k] - (or (str/starts-with? k "aria-") - (str/starts-with? k "data-") - (contains? #{"contenteditable" "draggable" "spellcheck"} k))) + (let [k (str/lower-case k)] + (or (str/starts-with? k "aria-") + (str/starts-with? k "data-") + (contains? #{"contenteditable" "draggable" "focusable" "spellcheck"} k)))) (defn ->attrs "Implementation, do not use" diff --git a/test/borkdude/html_test.cljc b/test/borkdude/html_test.cljc index 5db5a6d..27c33b6 100644 --- a/test/borkdude/html_test.cljc +++ b/test/borkdude/html_test.cljc @@ -113,6 +113,12 @@ "
" (html [:div {:draggable true}]) + "
" + (html [:div {:spellCheck false}]) + + "" + (html [:svg {:focusable false}]) + "
" (html [:div {:data-active false}])