diff --git a/CHANGELOG.md b/CHANGELOG.md index 106b4cf..704e79e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,23 @@ ## Changed +- 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) ## Added @@ -23,4 +40,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..39480a6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # hiccup -[](https://circleci.com/gh/lambdaisland/hiccup) [](https://cljdoc.org/d/com.lambdaisland/hiccup) [](https://clojars.org/com.lambdaisland/hiccup) +[](https://circleci.com/gh/lambdaisland/hiccup) [](https://cljdoc.org/d/com.lambdaisland/hiccup) [](https://clojars.org/com.lambdaisland/hiccup) Enlive-backed Hiccup implementation (clj-only) @@ -13,9 +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"]` - -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 diff --git a/src/lambdaisland/hiccup.clj b/src/lambdaisland/hiccup.clj index 185d52f..239f261 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,43 @@ (defn- attr-map? [node-spec] (and (map? node-spec) (not (keyword? (:tag node-spec))))) +(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- 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 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 + attr-str))) + +(defn convert-attribute [attr] + (->> attr + convert-attribute-reagent-logic + convert-attribute-react-logic)) + (defn- nodify [node-spec {:keys [newlines?] :as opts}] (cond (string? node-spec) node-spec @@ -43,6 +101,12 @@ (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]] + [(convert-attribute k) v])) + (into {})))) node (if id (assoc-in node [:attrs :id] id) node) node (if (seq classes) (update-in node @@ -51,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 35df25f..0ac30fd 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,14 +22,44 @@ (is (= (hiccup/render [:div {:style {:color "blue"}} [:p]] {: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