Skip to content

Commit 7c4925e

Browse files
authored
Merge pull request #5 from rafd/camel-case-attributes
Convert attributes to camelCase, with some exceptions
2 parents 5db0e34 + 51cf7e1 commit 7c4925e

File tree

4 files changed

+127
-18
lines changed

4 files changed

+127
-18
lines changed

CHANGELOG.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,23 @@
66

77
## Changed
88

9+
- Convert multi-word attributes to match Reagent + React's behaviour.
10+
11+
data-, aria-, and hx- attributes remain kebab-case.
12+
html and svg attributes that are kebab-case in those specs, are converted to kebab-case
13+
14+
BREAKING in some cases:
15+
16+
Previously:
17+
:tab-index -> "tab-index"
18+
"fontStyle" -> "fontStyle"
19+
:fontStyle -> "fontStyle"
20+
21+
Now:
22+
:tab-index -> "tabIndex"
23+
"fontStyle" -> "font-style"
24+
:fontStyle -> "font-style"
25+
926
# 0.0.15 (2023-03-20 / c0a2d53)
1027

1128
## Added
@@ -23,4 +40,4 @@
2340
- Initial implementation
2441
- fragment support
2542
- component support (fn? in first vector position)
26-
- unsafe-html support
43+
- unsafe-html support

README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# hiccup
22

33
<!-- badges -->
4-
[![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)
4+
[![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)
55
<!-- /badges -->
66

77
Enlive-backed Hiccup implementation (clj-only)
@@ -13,9 +13,7 @@ Enlive-backed Hiccup implementation (clj-only)
1313
- Components (`[my-fn ...]`)
1414
- Style maps (`[:div {:style {:color "blue"}}]`)
1515
- Insert pre-rendered HTML with `[::hiccup/unsafe-html "your html"]`
16-
17-
This makes it behave closer to how Hiccup works in Reagent, reducing cognitive
18-
overhead when doing cross-platform development.
16+
- 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
1917

2018
<!-- installation -->
2119
## Installation

src/lambdaisland/hiccup.clj

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,27 @@
88
[garden.compiler :as gc]
99
[clojure.string :as str]))
1010

11+
(def kebab-case-tags
12+
;; from https://github.com/preactjs/preact-compat/issues/222
13+
#{;; html
14+
"accept-charset" "http-equiv"
15+
;; svg
16+
"accent-height" "alignment-baseline" "arabic-form" "baseline-shift" "cap-height"
17+
"clip-path" "clip-rule" "color-interpolation" "color-interpolation-filters"
18+
"color-profile" "color-rendering" "fill-opacity" "fill-rule" "flood-color"
19+
"flood-opacity" "font-family" "font-size" "font-size-adjust" "font-stretch"
20+
"font-style" "font-variant" "font-weight" "glyph-name"
21+
"glyph-orientation-horizontal" "glyph-orientation-vertical" "horiz-adv-x"
22+
"horiz-origin-x" "marker-end" "marker-mid" "marker-start" "overline-position"
23+
"overline-thickness" "panose-1" "paint-order" "stop-color" "stop-opacity"
24+
"strikethrough-position" "strikethrough-thickness" "stroke-dasharray"
25+
"stroke-dashoffset" "stroke-linecap" "stroke-linejoin" "stroke-miterlimit"
26+
"stroke-opacity" "stroke-width" "text-anchor" "text-decoration" "text-rendering"
27+
"underline-position" "underline-thickness" "unicode-bidi" "unicode-range"
28+
"units-per-em" "v-alphabetic" "v-hanging" "v-ideographic" "v-mathematical"
29+
"vert-adv-y" "vert-origin-x" "vert-origin-y" "word-spacing" "writing-mode"
30+
"x-height"})
31+
1132
(def block-level-tag?
1233
#{:head :body :meta :title :script :svg :iframe :style
1334
:link :address :article :aside :blockquote :details
@@ -17,6 +38,43 @@
1738
(defn- attr-map? [node-spec]
1839
(and (map? node-spec) (not (keyword? (:tag node-spec)))))
1940

41+
(defn- kebab-in-html? [attr-str]
42+
(or (contains? kebab-case-tags attr-str)
43+
(str/starts-with? attr-str "data-")
44+
(str/starts-with? attr-str "aria-")
45+
(str/starts-with? attr-str "hx-")))
46+
47+
(defn- kebab->camel [s]
48+
(str/replace s #"-(\w)" (fn [[_ match]] (str/capitalize match))))
49+
50+
(defn- camel->kebab [s]
51+
(str/replace s #"([A-Z])" (fn [[_ match]] (str "-" (str/lower-case match)))))
52+
53+
(defn convert-attribute-reagent-logic
54+
[attr]
55+
(cond
56+
(string? attr)
57+
attr
58+
(keyword? attr)
59+
(if (kebab-in-html? (name attr))
60+
(name attr)
61+
(kebab->camel (name attr)))))
62+
63+
(defn convert-attribute-react-logic
64+
[attr-str]
65+
(let [kebab-str (camel->kebab attr-str)]
66+
;; not using kebab-in-html? here because
67+
;; React does not convert dataFoo to data-foo
68+
;; but does convert fontStretch to font-stretch
69+
(if (kebab-case-tags kebab-str)
70+
kebab-str
71+
attr-str)))
72+
73+
(defn convert-attribute [attr]
74+
(->> attr
75+
convert-attribute-reagent-logic
76+
convert-attribute-react-logic))
77+
2078
(defn- nodify [node-spec {:keys [newlines?] :as opts}]
2179
(cond
2280
(string? node-spec) node-spec
@@ -43,6 +101,12 @@
43101
(into {} (filter val m))
44102
{})
45103
:content (enlive/flatmap #(nodify % opts) (if (attr-map? m) ms more))}
104+
node (update node :attrs
105+
(fn [attrs]
106+
(->> attrs
107+
(map (fn [[k v]]
108+
[(convert-attribute k) v]))
109+
(into {}))))
46110
node (if id (assoc-in node [:attrs :id] id) node)
47111
node (if (seq classes)
48112
(update-in node
@@ -51,13 +115,13 @@
51115
(concat classes (if (string? kls) [kls] kls))))
52116
node)]
53117
(cond-> node
54-
(map? (get-in node [:attrs :style]))
55-
(update-in [:attrs :style] (fn [style]
56-
(-> (gc/compile-css [:& style])
57-
(str/replace #"^\s*\{|\}\s*$" "")
58-
str/trim)))
59-
(sequential? (get-in node [:attrs :class]))
60-
(update-in [:attrs :class] #(str/join " " %))
118+
(map? (get-in node [:attrs "style"]))
119+
(update-in [:attrs "style"] (fn [style]
120+
(-> (gc/compile-css [:& style])
121+
(str/replace #"^\s*\{|\}\s*$" "")
122+
str/trim)))
123+
(sequential? (get-in node [:attrs "class"]))
124+
(update-in [:attrs "class"] #(str/join " " %))
61125
(and newlines? (block-level-tag? tag))
62126
(->> (list "\n"))))
63127

test/lambdaisland/hiccup_test.clj

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11

2-
(ns lambdaisland.hiccup-test
2+
(ns lambdaisland.hiccup-test
33
(:require [clojure.test :refer [deftest testing is]]
44
[lambdaisland.hiccup :as hiccup]))
55

66
(defn my-test-component [contents]
77
[:p contents])
88

99
(defn test-fragment-component [contents]
10-
[:<>
10+
[:<>
1111
[:p contents]
1212
[:p contents]])
1313

14-
(deftest render-test
14+
(deftest render-test
1515
(testing "simple tag"
1616
(is (= (hiccup/render [:p] {:doctype? false})
1717
"<p></p>")))
@@ -22,14 +22,44 @@
2222
(is (= (hiccup/render [:div {:style {:color "blue"}} [:p]] {:doctype? false})
2323
"<div style=\"color: blue;\"><p></p></div>")))
2424
(testing "simple component"
25-
(is (= (hiccup/render [my-test-component "hello"] {:doctype? false})
25+
(is (= (hiccup/render [my-test-component "hello"] {:doctype? false})
2626
"<p>hello</p>")))
2727
(testing "simple component with fragment"
28-
(is (= (hiccup/render [:div [test-fragment-component "hello"]] {:doctype? false})
28+
(is (= (hiccup/render [:div [test-fragment-component "hello"]] {:doctype? false})
2929
"<div><p>hello</p><p>hello</p></div>")))
3030
(testing "pre-rendered HTML"
3131
(is (= (hiccup/render [::hiccup/unsafe-html "<body><main><article><p></p></article></main></body>"] {:doctype? false})
3232
"<body><main><article><p></p></article></main></body>")))
3333
(testing "autoescaping"
3434
(is (= (hiccup/render [:div "<p></p>"] {:doctype? false})
35-
"<div>&lt;p&gt;&lt;/p&gt;</div>"))))
35+
"<div>&lt;p&gt;&lt;/p&gt;</div>")))
36+
37+
(testing "attribute conversion"
38+
;; convert kebab-case and camelCase attributes
39+
;; based on behaviour of using Reagent + React
40+
;; except, don't force lowercase (the * below)
41+
(doseq [[input expected]
42+
{"tabIndex" "tabIndex" ;; *
43+
"dataA" "dataA"
44+
"fontStyle" "font-style"
45+
46+
"tab-index" "tab-index"
47+
"data-b" "data-b"
48+
"font-variant" "font-variant"
49+
50+
:tabIndex "tabIndex" ;; *
51+
:dataD "dataD"
52+
:fontStretch "font-stretch"
53+
54+
:tab-index "tabIndex" ;; *
55+
:data-c "data-c"
56+
:font-weight "font-weight"
57+
58+
:hx-foo "hx-foo"
59+
"hx-foo" "hx-foo"}]
60+
(testing (str (pr-str input) "->" (pr-str expected))
61+
(is (= expected
62+
(hiccup/convert-attribute input)))
63+
(is (= (str "<div " expected "=\"baz\"></div>")
64+
(hiccup/render [:div {input "baz"}]
65+
{:doctype? false})))))))

0 commit comments

Comments
 (0)