Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

## 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)

## Added
Expand All @@ -23,4 +27,4 @@
- Initial implementation
- fragment support
- component support (fn? in first vector position)
- unsafe-html support
- unsafe-html support
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# hiccup

<!-- badges -->
[![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)
<!-- /badges -->

Enlive-backed Hiccup implementation (clj-only)
Expand All @@ -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 `"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.
Expand Down
39 changes: 39 additions & 0 deletions src/lambdaisland/hiccup.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -43,6 +72,16 @@
(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]]
[(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)
node (if (seq classes)
(update-in node
Expand Down
24 changes: 18 additions & 6 deletions test/lambdaisland/hiccup_test.clj
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@

(ns lambdaisland.hiccup-test
(ns lambdaisland.hiccup-test
(:require [clojure.test :refer [deftest testing is]]
[lambdaisland.hiccup :as hiccup]))

(defn my-test-component [contents]
[: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})
"<p></p>")))
Expand All @@ -22,14 +22,26 @@
(is (= (hiccup/render [:div {:style {:color "blue"}} [:p]] {:doctype? false})
"<div style=\"color: blue;\"><p></p></div>")))
(testing "simple component"
(is (= (hiccup/render [my-test-component "hello"] {:doctype? false})
(is (= (hiccup/render [my-test-component "hello"] {:doctype? false})
"<p>hello</p>")))
(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})
"<div><p>hello</p><p>hello</p></div>")))
(testing "pre-rendered HTML"
(is (= (hiccup/render [::hiccup/unsafe-html "<body><main><article><p></p></article></main></body>"] {:doctype? false})
"<body><main><article><p></p></article></main></body>")))
(testing "autoescaping"
(is (= (hiccup/render [:div "<p></p>"] {:doctype? false})
"<div>&lt;p&gt;&lt;/p&gt;</div>"))))
"<div>&lt;p&gt;&lt;/p&gt;</div>")))
(testing "camelCases attributes"
(is (= (hiccup/render [:div {:foo-bar "baz"}] {:doctype? false})
"<div fooBar=\"baz\"></div>")))
(testing "keeps data-* attributes as kebab-case"
(is (= (hiccup/render [:div {:data-foo "bar"}] {:doctype? false})
"<div data-foo=\"bar\"></div>")))
(testing "keeps certain http and svg attributes as kebab-case"
(is (= (hiccup/render [:div {:font-family "Arial"}] {:doctype? false})
"<div font-family=\"Arial\"></div>")))
(testing "keeps string attributes as is"
(is (= (hiccup/render [:div {"foo-bar" "baz"}] {:doctype? false})
"<div foo-bar=\"baz\"></div>"))))