From 4725b26e4b9029c929dd57b933140b5319037395 Mon Sep 17 00:00:00 2001 From: Rafal Dittwald Date: Mon, 8 May 2023 12:45:38 -0400 Subject: [PATCH 1/7] Convert attributes to camelCase, with some exceptions --- CHANGELOG.md | 6 ++++- README.md | 3 ++- src/lambdaisland/hiccup.clj | 38 +++++++++++++++++++++++++++++++ test/lambdaisland/hiccup_test.clj | 11 ++++++++- 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 106b4cf..1b95ef9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ ## Changed +# 0.0.!! (2023-05-08 / ???) + +- Convert attributes to camelCase, except data-*, aria-* and certain html and svg attributes that are expected to be kebab-case + # 0.0.15 (2023-03-20 / c0a2d53) ## Added @@ -23,4 +27,4 @@ - Initial implementation - fragment support - component support (fn? in first vector position) -- unsafe-html support \ No newline at end of file +- unsafe-html support diff --git a/README.md b/README.md index a546b72..e454389 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # hiccup -[![CircleCI](https://circleci.com/gh/lambdaisland/hiccup.svg?style=svg)](https://circleci.com/gh/lambdaisland/hiccup) [![cljdoc badge](https://cljdoc.org/badge/com.lambdaisland/hiccup)](https://cljdoc.org/d/com.lambdaisland/hiccup) [![Clojars Project](https://img.shields.io/clojars/v/com.lambdaisland/hiccup.svg)](https://clojars.org/com.lambdaisland/hiccup) +[![CircleCI](https://circleci.com/gh/lambdaisland/hiccup.svg?style=svg)](https://circleci.com/gh/lambdaisland/hiccup) [![cljdoc badge](https://cljdoc.org/badge/com.lambdaisland/hiccup)](https://cljdoc.org/d/com.lambdaisland/hiccup) [![Clojars Project](https://img.shields.io/clojars/v/com.lambdaisland/hiccup.svg)](https://clojars.org/com.lambdaisland/hiccup) Enlive-backed Hiccup implementation (clj-only) @@ -13,6 +13,7 @@ Enlive-backed Hiccup implementation (clj-only) - Components (`[my-fn ...]`) - Style maps (`[:div {:style {:color "blue"}}]`) - Insert pre-rendered HTML with `[::hiccup/unsafe-html "your html"]` +- Convert attributes to camelCase, except those HTML + SVG expects to remain kebab-case (`data-*, aria-*, accept-charset...`) This makes it behave closer to how Hiccup works in Reagent, reducing cognitive overhead when doing cross-platform development. diff --git a/src/lambdaisland/hiccup.clj b/src/lambdaisland/hiccup.clj index 185d52f..35dedab 100644 --- a/src/lambdaisland/hiccup.clj +++ b/src/lambdaisland/hiccup.clj @@ -8,6 +8,27 @@ [garden.compiler :as gc] [clojure.string :as str])) +(def kebab-case-tags + ;; from https://github.com/preactjs/preact-compat/issues/222 + #{;; html + :accept-charset :http-equiv + ;; svg + :accent-height :alignment-baseline :arabic-form :baseline-shift :cap-height + :clip-path :clip-rule :color-interpolation :color-interpolation-filters + :color-profile :color-rendering :fill-opacity :fill-rule :flood-color + :flood-opacity :font-family :font-size :font-size-adjust :font-stretch + :font-style :font-variant :font-weight :glyph-name + :glyph-orientation-horizontal :glyph-orientation-vertical :horiz-adv-x + :horiz-origin-x :marker-end :marker-mid :marker-start :overline-position + :overline-thickness :panose-1 :paint-order :stop-color :stop-opacity + :strikethrough-position :strikethrough-thickness :stroke-dasharray + :stroke-dashoffset :stroke-linecap :stroke-linejoin :stroke-miterlimit + :stroke-opacity :stroke-width :text-anchor :text-decoration :text-rendering + :underline-position :underline-thickness :unicode-bidi :unicode-range + :units-per-em :v-alphabetic :v-hanging :v-ideographic :v-mathematical + :vert-adv-y :vert-origin-x :vert-origin-y :word-spacing :writing-mode + :x-height}) + (def block-level-tag? #{:head :body :meta :title :script :svg :iframe :style :link :address :article :aside :blockquote :details @@ -17,6 +38,14 @@ (defn- attr-map? [node-spec] (and (map? node-spec) (not (keyword? (:tag node-spec))))) +(defn- keep-kebab-case? [k] + (or (contains? kebab-case-tags k) + (str/starts-with? (name k) "data-") + (str/starts-with? (name k) "aria-"))) + +(defn- camel-case [k] + (keyword (str/replace (name k) #"-(\w)" (fn [[_ match]] (str/capitalize match))))) + (defn- nodify [node-spec {:keys [newlines?] :as opts}] (cond (string? node-spec) node-spec @@ -43,6 +72,15 @@ (into {} (filter val m)) {}) :content (enlive/flatmap #(nodify % opts) (if (attr-map? m) ms more))} + node (update node :attrs + (fn [attrs] + (->> attrs + (map (fn [[k v]] + [(if (keep-kebab-case? k) + k + (camel-case k)) + v])) + (into {})))) node (if id (assoc-in node [:attrs :id] id) node) node (if (seq classes) (update-in node diff --git a/test/lambdaisland/hiccup_test.clj b/test/lambdaisland/hiccup_test.clj index 35df25f..a760eb9 100644 --- a/test/lambdaisland/hiccup_test.clj +++ b/test/lambdaisland/hiccup_test.clj @@ -32,4 +32,13 @@ "

"))) (testing "autoescaping" (is (= (hiccup/render [:div "

"] {:doctype? false}) - "
<p></p>
")))) + "
<p></p>
"))) + (testing "camelCases attributes" + (is (= (hiccup/render [:div {:foo-bar "baz"}] {:doctype? false}) + "
"))) + (testing "keeps data-* attributes as kebab-case" + (is (= (hiccup/render [:div {:data-foo "bar"}] {:doctype? false}) + "
"))) + (testing "keeps certain http and svg attributes as kebab-case" + (is (= (hiccup/render [:div {:font-family "Arial"}] {:doctype? false}) + "
")))) From e7b6bfc77d3eff492be1dcd7af59c75ac197599f Mon Sep 17 00:00:00 2001 From: Rafal Dittwald Date: Mon, 8 May 2023 13:22:46 -0400 Subject: [PATCH 2/7] Never convert string attributes --- CHANGELOG.md | 2 +- README.md | 2 +- src/lambdaisland/hiccup.clj | 7 ++++--- test/lambdaisland/hiccup_test.clj | 15 +++++++++------ 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b95ef9..8106ecb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ # 0.0.!! (2023-05-08 / ???) -- Convert attributes to camelCase, except data-*, aria-* and certain html and svg attributes that are expected to be kebab-case +- Convert attributes to camelCase, except strings, data-*, aria-* and certain html and svg attributes that are expected to be kebab-case # 0.0.15 (2023-03-20 / c0a2d53) diff --git a/README.md b/README.md index e454389..b69c397 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Enlive-backed Hiccup implementation (clj-only) - Components (`[my-fn ...]`) - Style maps (`[:div {:style {:color "blue"}}]`) - Insert pre-rendered HTML with `[::hiccup/unsafe-html "your html"]` -- Convert attributes to camelCase, except those HTML + SVG expects to remain kebab-case (`data-*, aria-*, accept-charset...`) +- Convert attributes to camelCase, except `"strings"` and those HTML + SVG expects to remain kebab-case (`data-*, aria-*, accept-charset...`) This makes it behave closer to how Hiccup works in Reagent, reducing cognitive overhead when doing cross-platform development. diff --git a/src/lambdaisland/hiccup.clj b/src/lambdaisland/hiccup.clj index 35dedab..ebf3170 100644 --- a/src/lambdaisland/hiccup.clj +++ b/src/lambdaisland/hiccup.clj @@ -76,9 +76,10 @@ (fn [attrs] (->> attrs (map (fn [[k v]] - [(if (keep-kebab-case? k) - k - (camel-case k)) + [(cond + (string? k) k + (keep-kebab-case? k) k + :else (camel-case k)) v])) (into {})))) node (if id (assoc-in node [:attrs :id] id) node) diff --git a/test/lambdaisland/hiccup_test.clj b/test/lambdaisland/hiccup_test.clj index a760eb9..f45a612 100644 --- a/test/lambdaisland/hiccup_test.clj +++ b/test/lambdaisland/hiccup_test.clj @@ -1,5 +1,5 @@ -(ns lambdaisland.hiccup-test +(ns lambdaisland.hiccup-test (:require [clojure.test :refer [deftest testing is]] [lambdaisland.hiccup :as hiccup])) @@ -7,11 +7,11 @@ [:p contents]) (defn test-fragment-component [contents] - [:<> + [:<> [:p contents] [:p contents]]) -(deftest render-test +(deftest render-test (testing "simple tag" (is (= (hiccup/render [:p] {:doctype? false}) "

"))) @@ -22,10 +22,10 @@ (is (= (hiccup/render [:div {:style {:color "blue"}} [:p]] {:doctype? false}) "

"))) (testing "simple component" - (is (= (hiccup/render [my-test-component "hello"] {:doctype? false}) + (is (= (hiccup/render [my-test-component "hello"] {:doctype? false}) "

hello

"))) (testing "simple component with fragment" - (is (= (hiccup/render [:div [test-fragment-component "hello"]] {:doctype? false}) + (is (= (hiccup/render [:div [test-fragment-component "hello"]] {:doctype? false}) "

hello

hello

"))) (testing "pre-rendered HTML" (is (= (hiccup/render [::hiccup/unsafe-html "

"] {:doctype? false}) @@ -41,4 +41,7 @@ "
"))) (testing "keeps certain http and svg attributes as kebab-case" (is (= (hiccup/render [:div {:font-family "Arial"}] {:doctype? false}) - "
")))) + "
"))) + (testing "keeps string attributes as is" + (is (= (hiccup/render [:div {"foo-bar" "baz"}] {:doctype? false}) + "
")))) From 55e453d1ea34e2da2339e927db535e70a39edef0 Mon Sep 17 00:00:00 2001 From: Rafal Dittwald Date: Mon, 8 May 2023 22:24:16 -0400 Subject: [PATCH 3/7] Remove unnecessary CHANGELOG metadata --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8106ecb..2049052 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,6 @@ ## Changed -# 0.0.!! (2023-05-08 / ???) - - Convert attributes to camelCase, except strings, data-*, aria-* and certain html and svg attributes that are expected to be kebab-case # 0.0.15 (2023-03-20 / c0a2d53) From f739a5097cd9d722557e185d0ea82c491b20af50 Mon Sep 17 00:00:00 2001 From: Rafal Dittwald Date: Mon, 8 May 2023 22:24:42 -0400 Subject: [PATCH 4/7] Add :camelCase test --- test/lambdaisland/hiccup_test.clj | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/lambdaisland/hiccup_test.clj b/test/lambdaisland/hiccup_test.clj index f45a612..d41e3f4 100644 --- a/test/lambdaisland/hiccup_test.clj +++ b/test/lambdaisland/hiccup_test.clj @@ -44,4 +44,7 @@ "
"))) (testing "keeps string attributes as is" (is (= (hiccup/render [:div {"foo-bar" "baz"}] {:doctype? false}) - "
")))) + "
"))) + (testing "camelCase attributes remain as is" + (is (= (hiccup/render [:div {:camelCase "foo"}] {:doctype? false}) + "
")))) From 29d657126f5d0757c67ababd72d5ac6ba54c72f7 Mon Sep 17 00:00:00 2001 From: Rafal Dittwald Date: Tue, 30 May 2023 17:27:59 -0400 Subject: [PATCH 5/7] Handle other attribute case situations --- src/lambdaisland/hiccup.clj | 93 ++++++++++++++++++++----------- test/lambdaisland/hiccup_test.clj | 45 ++++++++++----- 2 files changed, 89 insertions(+), 49 deletions(-) diff --git a/src/lambdaisland/hiccup.clj b/src/lambdaisland/hiccup.clj index ebf3170..de58ab2 100644 --- a/src/lambdaisland/hiccup.clj +++ b/src/lambdaisland/hiccup.clj @@ -11,23 +11,23 @@ (def kebab-case-tags ;; from https://github.com/preactjs/preact-compat/issues/222 #{;; html - :accept-charset :http-equiv + "accept-charset" "http-equiv" ;; svg - :accent-height :alignment-baseline :arabic-form :baseline-shift :cap-height - :clip-path :clip-rule :color-interpolation :color-interpolation-filters - :color-profile :color-rendering :fill-opacity :fill-rule :flood-color - :flood-opacity :font-family :font-size :font-size-adjust :font-stretch - :font-style :font-variant :font-weight :glyph-name - :glyph-orientation-horizontal :glyph-orientation-vertical :horiz-adv-x - :horiz-origin-x :marker-end :marker-mid :marker-start :overline-position - :overline-thickness :panose-1 :paint-order :stop-color :stop-opacity - :strikethrough-position :strikethrough-thickness :stroke-dasharray - :stroke-dashoffset :stroke-linecap :stroke-linejoin :stroke-miterlimit - :stroke-opacity :stroke-width :text-anchor :text-decoration :text-rendering - :underline-position :underline-thickness :unicode-bidi :unicode-range - :units-per-em :v-alphabetic :v-hanging :v-ideographic :v-mathematical - :vert-adv-y :vert-origin-x :vert-origin-y :word-spacing :writing-mode - :x-height}) + "accent-height" "alignment-baseline" "arabic-form" "baseline-shift" "cap-height" + "clip-path" "clip-rule" "color-interpolation" "color-interpolation-filters" + "color-profile" "color-rendering" "fill-opacity" "fill-rule" "flood-color" + "flood-opacity" "font-family" "font-size" "font-size-adjust" "font-stretch" + "font-style" "font-variant" "font-weight" "glyph-name" + "glyph-orientation-horizontal" "glyph-orientation-vertical" "horiz-adv-x" + "horiz-origin-x" "marker-end" "marker-mid" "marker-start" "overline-position" + "overline-thickness" "panose-1" "paint-order" "stop-color" "stop-opacity" + "strikethrough-position" "strikethrough-thickness" "stroke-dasharray" + "stroke-dashoffset" "stroke-linecap" "stroke-linejoin" "stroke-miterlimit" + "stroke-opacity" "stroke-width" "text-anchor" "text-decoration" "text-rendering" + "underline-position" "underline-thickness" "unicode-bidi" "unicode-range" + "units-per-em" "v-alphabetic" "v-hanging" "v-ideographic" "v-mathematical" + "vert-adv-y" "vert-origin-x" "vert-origin-y" "word-spacing" "writing-mode" + "x-height"}) (def block-level-tag? #{:head :body :meta :title :script :svg :iframe :style @@ -38,13 +38,42 @@ (defn- attr-map? [node-spec] (and (map? node-spec) (not (keyword? (:tag node-spec))))) -(defn- keep-kebab-case? [k] - (or (contains? kebab-case-tags k) - (str/starts-with? (name k) "data-") - (str/starts-with? (name k) "aria-"))) +(defn- kebab-in-html? [attr-str] + (or (contains? kebab-case-tags attr-str) + (str/starts-with? attr-str "data-") + (str/starts-with? attr-str "aria-") + (str/starts-with? attr-str "hx-"))) -(defn- camel-case [k] - (keyword (str/replace (name k) #"-(\w)" (fn [[_ match]] (str/capitalize match))))) +(defn- kebab->camel [s] + (str/replace s #"-(\w)" (fn [[_ match]] (str/capitalize match)))) + +(defn- camel->kebab [s] + (str/replace s #"([A-Z])" (fn [[_ match]] (str "-" (str/lower-case match))))) + +(defn convert-attribute-reagent-logic + [attr] + (cond + (string? attr) + attr + (keyword? attr) + (if (kebab-in-html? (name attr)) + (name attr) + (kebab->camel (name attr))))) + +(defn convert-attribute-react-logic + [attr-str] + (let [kebab-str (camel->kebab attr-str)] + ;; not use kebab-in-html? here b/c + ;; React does not convert ariaFoo to aria-foo + ;; but does convert fontStretch to font-stretch + (if (kebab-case-tags kebab-str) + kebab-str + attr-str))) + +(defn convert-attribute [attr] + (->> attr + convert-attribute-reagent-logic + convert-attribute-react-logic)) (defn- nodify [node-spec {:keys [newlines?] :as opts}] (cond @@ -76,11 +105,7 @@ (fn [attrs] (->> attrs (map (fn [[k v]] - [(cond - (string? k) k - (keep-kebab-case? k) k - :else (camel-case k)) - v])) + [(convert-attribute k) v])) (into {})))) node (if id (assoc-in node [:attrs :id] id) node) node (if (seq classes) @@ -90,13 +115,13 @@ (concat classes (if (string? kls) [kls] kls)))) node)] (cond-> node - (map? (get-in node [:attrs :style])) - (update-in [:attrs :style] (fn [style] - (-> (gc/compile-css [:& style]) - (str/replace #"^\s*\{|\}\s*$" "") - str/trim))) - (sequential? (get-in node [:attrs :class])) - (update-in [:attrs :class] #(str/join " " %)) + (map? (get-in node [:attrs "style"])) + (update-in [:attrs "style"] (fn [style] + (-> (gc/compile-css [:& style]) + (str/replace #"^\s*\{|\}\s*$" "") + str/trim))) + (sequential? (get-in node [:attrs "class"])) + (update-in [:attrs "class"] #(str/join " " %)) (and newlines? (block-level-tag? tag)) (->> (list "\n")))) diff --git a/test/lambdaisland/hiccup_test.clj b/test/lambdaisland/hiccup_test.clj index d41e3f4..0ac30fd 100644 --- a/test/lambdaisland/hiccup_test.clj +++ b/test/lambdaisland/hiccup_test.clj @@ -33,18 +33,33 @@ (testing "autoescaping" (is (= (hiccup/render [:div "

"] {:doctype? false}) "
<p></p>
"))) - (testing "camelCases attributes" - (is (= (hiccup/render [:div {:foo-bar "baz"}] {:doctype? false}) - "
"))) - (testing "keeps data-* attributes as kebab-case" - (is (= (hiccup/render [:div {:data-foo "bar"}] {:doctype? false}) - "
"))) - (testing "keeps certain http and svg attributes as kebab-case" - (is (= (hiccup/render [:div {:font-family "Arial"}] {:doctype? false}) - "
"))) - (testing "keeps string attributes as is" - (is (= (hiccup/render [:div {"foo-bar" "baz"}] {:doctype? false}) - "
"))) - (testing "camelCase attributes remain as is" - (is (= (hiccup/render [:div {:camelCase "foo"}] {:doctype? false}) - "
")))) + + (testing "attribute conversion" + ;; convert kebab-case and camelCase attributes + ;; based on behaviour of using Reagent + React + ;; except, don't force lowercase (the * below) + (doseq [[input expected] + {"tabIndex" "tabIndex" ;; * + "dataA" "dataA" + "fontStyle" "font-style" + + "tab-index" "tab-index" + "data-b" "data-b" + "font-variant" "font-variant" + + :tabIndex "tabIndex" ;; * + :dataD "dataD" + :fontStretch "font-stretch" + + :tab-index "tabIndex" ;; * + :data-c "data-c" + :font-weight "font-weight" + + :hx-foo "hx-foo" + "hx-foo" "hx-foo"}] + (testing (str (pr-str input) "->" (pr-str expected)) + (is (= expected + (hiccup/convert-attribute input))) + (is (= (str "
") + (hiccup/render [:div {input "baz"}] + {:doctype? false}))))))) From 1e0c36327018c72c58b4e8a7411d3f0d9946e554 Mon Sep 17 00:00:00 2001 From: Rafal Dittwald Date: Tue, 30 May 2023 17:34:45 -0400 Subject: [PATCH 6/7] Improve comment --- src/lambdaisland/hiccup.clj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lambdaisland/hiccup.clj b/src/lambdaisland/hiccup.clj index de58ab2..239f261 100644 --- a/src/lambdaisland/hiccup.clj +++ b/src/lambdaisland/hiccup.clj @@ -63,8 +63,8 @@ (defn convert-attribute-react-logic [attr-str] (let [kebab-str (camel->kebab attr-str)] - ;; not use kebab-in-html? here b/c - ;; React does not convert ariaFoo to aria-foo + ;; not using kebab-in-html? here because + ;; React does not convert dataFoo to data-foo ;; but does convert fontStretch to font-stretch (if (kebab-case-tags kebab-str) kebab-str From 51cf7e1050991f5561a7ea84d5e2fc90d56b8329 Mon Sep 17 00:00:00 2001 From: Rafal Dittwald Date: Tue, 13 Jun 2023 18:42:13 -0400 Subject: [PATCH 7/7] Update attribute conversion explanation in README and CHANGELOG --- CHANGELOG.md | 17 ++++++++++++++++- README.md | 5 +---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2049052..704e79e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,22 @@ ## Changed -- Convert attributes to camelCase, except strings, data-*, aria-* and certain html and svg attributes that are expected to be kebab-case +- Convert multi-word attributes to match Reagent + React's behaviour. + + data-, aria-, and hx- attributes remain kebab-case. + html and svg attributes that are kebab-case in those specs, are converted to kebab-case + + BREAKING in some cases: + + Previously: + :tab-index -> "tab-index" + "fontStyle" -> "fontStyle" + :fontStyle -> "fontStyle" + + Now: + :tab-index -> "tabIndex" + "fontStyle" -> "font-style" + :fontStyle -> "font-style" # 0.0.15 (2023-03-20 / c0a2d53) diff --git a/README.md b/README.md index b69c397..39480a6 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,7 @@ Enlive-backed Hiccup implementation (clj-only) - Components (`[my-fn ...]`) - Style maps (`[:div {:style {:color "blue"}}]`) - Insert pre-rendered HTML with `[::hiccup/unsafe-html "your html"]` -- Convert attributes to camelCase, except `"strings"` and those HTML + SVG expects to remain kebab-case (`data-*, aria-*, accept-charset...`) - -This makes it behave closer to how Hiccup works in Reagent, reducing cognitive -overhead when doing cross-platform development. +- Convert multi-word attributes to the appropriate case, matching Reagent + React's behaviour (`:tab-index -> "tabIndex"`; `"fontStyle" -> "font-style"`); you should be able to use `:kebab-case` keywords and get what you expect ## Installation