├── .clj-kondo └── config.edn ├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── deps.edn ├── doc └── syntax.md ├── project.clj ├── resources └── clj-kondo.exports │ └── hiccup │ └── hiccup │ ├── config.edn │ └── hiccup │ └── hooks.clj ├── src ├── hiccup │ ├── compiler.clj │ ├── core.clj │ ├── def.clj │ ├── element.clj │ ├── form.clj │ ├── middleware.clj │ ├── page.clj │ └── util.clj └── hiccup2 │ └── core.clj └── test ├── hiccup ├── compiler_test.clj ├── core_test.clj ├── def_test.clj ├── element_test.clj ├── form_test.clj ├── middleware_test.clj ├── page_test.clj └── util_test.clj └── hiccup2 ├── core_test.clj └── optimizations_test.clj /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:config-paths ["../resources/clj-kondo.exports/hiccup/hiccup"] 2 | :ignore [:deprecated-var :deprecated-namespace]} 3 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout 8 | uses: actions/checkout@v3 9 | 10 | - name: Prepare java 11 | uses: actions/setup-java@v3 12 | with: 13 | distribution: 'zulu' 14 | java-version: '8' 15 | 16 | - name: Install clojure tools 17 | uses: DeLaGuardo/setup-clojure@10.1 18 | with: 19 | lein: 2.9.10 20 | 21 | - name: Cache clojure dependencies 22 | uses: actions/cache@v3 23 | with: 24 | path: ~/.m2/repository 25 | key: cljdeps-${{ hashFiles('project.clj') }} 26 | restore-keys: cljdeps- 27 | 28 | - name: Run tests 29 | run: lein test-all 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /codox 4 | /classes 5 | /checkouts 6 | pom.xml 7 | pom.xml.asc 8 | *.jar 9 | *.class 10 | .lein-* 11 | .nrepl-port 12 | .clj-kondo/* 13 | !.clj-kondo/config.edn 14 | /.cpcache 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0.0-RC5 (2025-05-21) 2 | 3 | * Fixed emitting unevaluated forms for non-literal tags (#217) 4 | 5 | ## 2.0.0-RC4 (2024-11-29) 6 | 7 | * Fixed compiler bug that emitted unevaluated forms (#214) 8 | 9 | ## 2.0.0-RC3 (2024-02-11) 10 | 11 | * Optimized amount of bytecode generated (#206) 12 | * Fixed literal child elements from being escaped (#207) 13 | * Fixed formatting of nil elements at runtime (#208) 14 | 15 | ## 2.0.0-RC2 (2023-10-05) 16 | 17 | * Improved performance (#204) 18 | 19 | ## 2.0.0-RC1 (2023-06-21) 20 | 21 | * Reverted behaviour of `hiccup.core/h` to 1.0 (#198) 22 | 23 | ## 2.0.0-alpha2 (2019-01-22) 24 | 25 | * Fixed issue with dot-notation and non-literal classes (#151) 26 | 27 | ## 2.0.0-alpha1 (2017-01-15) 28 | 29 | * Added `hiccup2.core` namespace that escapes strings automatically 30 | * Added new syntax for class and style attributes 31 | * Fixed issue with pre-compiled `html` macro accepting new mode bindings 32 | 33 | ## 1.0.5 (2014-01-25) 34 | 35 | * Inverted container tag check to look for void tags instead 36 | * Added apostrophes to list of characters to escape 37 | 38 | ## 1.0.4 (2013-07-21) 39 | 40 | * Fixed merging of class and id attributes 41 | * Fixed keyword rendering 42 | * Added explicit ending tag for ` element." 29 | [type name value] 30 | [:input {:type type 31 | :name (make-name name) 32 | :id (make-id name) 33 | :value value}]) 34 | 35 | (defelem hidden-field 36 | "Creates a hidden input field." 37 | ([name] (hidden-field name nil)) 38 | ([name value] (input-field "hidden" name value))) 39 | 40 | (defelem text-field 41 | "Creates a new text input field." 42 | ([name] (text-field name nil)) 43 | ([name value] (input-field "text" name value))) 44 | 45 | (defelem password-field 46 | "Creates a new password field." 47 | ([name] (password-field name nil)) 48 | ([name value] (input-field "password" name value))) 49 | 50 | (defelem email-field 51 | "Creates a new email input field." 52 | ([name] (email-field name nil)) 53 | ([name value] (input-field "email" name value))) 54 | 55 | (defelem check-box 56 | "Creates a check box." 57 | ([name] (check-box name nil)) 58 | ([name checked?] (check-box name checked? "true")) 59 | ([name checked? value] 60 | [:input {:type "checkbox" 61 | :name (make-name name) 62 | :id (make-id name) 63 | :value value 64 | :checked checked?}])) 65 | 66 | (defelem radio-button 67 | "Creates a radio button." 68 | ([group] (radio-button group nil)) 69 | ([group checked?] (radio-button group checked? "true")) 70 | ([group checked? value] 71 | [:input {:type "radio" 72 | :name (make-name group) 73 | :id (make-id (str (util/as-str group) "-" (util/as-str value))) 74 | :value value 75 | :checked checked?}])) 76 | 77 | (defelem select-options 78 | "Creates a seq of option tags from a collection." 79 | ([coll] (select-options coll nil)) 80 | ([coll selected] 81 | (for [x coll] 82 | (if (sequential? x) 83 | (let [[text val] x] 84 | (if (sequential? val) 85 | [:optgroup {:label text} (select-options val selected)] 86 | [:option {:value val :selected (= val selected)} text])) 87 | [:option {:selected (= x selected)} x])))) 88 | 89 | (defelem drop-down 90 | "Creates a drop-down box using the `")) 31 | (is (= (html [:object]) "")) 32 | (is (= (html [:video]) ""))) 33 | (testing "void tags" 34 | (is (= (html [:br]) "
")) 35 | (is (= (html [:link]) "")) 36 | (is (= (html [:colgroup {:span 2}]) "")) 37 | (is (= (html [:colgroup [:col]]) ""))) 38 | (testing "tags containing text" 39 | (is (= (html [:text "Lorem Ipsum"]) "Lorem Ipsum"))) 40 | (testing "contents are concatenated" 41 | (is (= (html [:body "foo" "bar"]) "foobar")) 42 | (is (= (html [:body [:p] [:br]]) "


"))) 43 | (testing "seqs are expanded" 44 | (is (= (html [:body (list "foo" "bar")]) "foobar")) 45 | (is (= (html (list [:p "a"] [:p "b"])) "

a

b

"))) 46 | (testing "keywords are turned into strings" 47 | (is (= (html [:div :foo]) "
foo
"))) 48 | (testing "vecs don't expand - error if vec doesn't have tag name" 49 | (is (thrown? IllegalArgumentException 50 | (html (vector [:p "a"] [:p "b"]))))) 51 | (testing "tags can contain tags" 52 | (is (= (html [:div [:p]]) "

")) 53 | (is (= (html [:div [:b]]) "
")) 54 | (is (= (html [:p [:span [:a "foo"]]]) 55 | "

foo

")))) 56 | 57 | (deftest tag-attributes 58 | (testing "tag with blank attribute map" 59 | (is (= (html [:xml {}]) ""))) 60 | (testing "tag with populated attribute map" 61 | (is (= (html [:xml {:a 123}]) "")) 62 | (is (= (html [:xml {:a 'sym}]) "")) 63 | (is (= (html [:xml {:a :kw}]) "")) 64 | (is (= (html [:xml {:a [:kw :ns/ns-kw "str" 3 'sym]}]) "")) 65 | (is (= (html [:xml {:a "1", :b "2"}]) "")) 66 | (is (= (html [:img {"id" "foo"}]) "")) 67 | (is (= (html [:img {'id "foo"}]) "")) 68 | (is (= (html [:xml {:a "1", 'b "2", "c" "3"}]) 69 | ""))) 70 | (testing "attribute values are escaped" 71 | (is (= (html [:div {:id "\""}]) "
"))) 72 | (testing "boolean attributes" 73 | (is (= (html [:input {:type "checkbox" :checked true}]) 74 | "")) 75 | (is (= (html [:input {:type "checkbox" :checked false}]) 76 | ""))) 77 | (testing "nil attributes" 78 | (is (= (html [:span {:class nil} "foo"]) 79 | "foo"))) 80 | (testing "resolving conflicts between attributes in the map and tag" 81 | (is (= (html [:div.foo {:class "bar"} "baz"]) 82 | "
baz
")) 83 | (is (= (html [:div#bar.foo {:id "baq"} "baz"]) 84 | "
baz
"))) 85 | (testing "tag with vector class" 86 | (is (= (html [:div {:class [:bar]} "baz"]) 87 | "
baz
")) 88 | (is (= (html [:div.foo {:class ["bar"]} "baz"]) 89 | "
baz
")) 90 | (is (= (html [:div.foo {:class [:bar]} "baz"]) 91 | "
baz
")) 92 | (is (= (html [:div.foo {:class [:bar "box"]} "baz"]) 93 | "
baz
")) 94 | (is (= (html [:div.foo {:class ["bar" "box"]} "baz"]) 95 | "
baz
")) 96 | (is (= (html [:div.foo {:class [:bar :box]} "baz"]) 97 | "
baz
")) 98 | (is (= (html [:div.foo {:class [nil :bar nil]} "baz"]) 99 | "
baz
")))) 100 | 101 | (deftest compiled-tags 102 | (testing "tag content can be vars" 103 | (is (= (let [x "foo"] (html [:span x])) "foo"))) 104 | (testing "tag content can be forms" 105 | (is (= (html [:span (str (+ 1 1))]) "2")) 106 | (is (= (html [:span ({:foo "bar"} :foo)]) "bar"))) 107 | (testing "attributes can contain vars" 108 | (let [x "foo"] 109 | (is (= (html [:xml {:x x}]) "")) 110 | (is (= (html [:xml {x "x"}]) "")) 111 | (is (= (html [:xml {:x x} "bar"]) "bar")))) 112 | (testing "attributes are evaluated" 113 | (is (= (html [:img {:src (str "/foo" "/bar")}]) 114 | "")) 115 | (is (= (html [:div {:id (str "a" "b")} (str "foo")]) 116 | "
foo
"))) 117 | (testing "type hints" 118 | (let [string "x"] 119 | (is (= (html [:span ^String string]) "x")))) 120 | (testing "optimized forms" 121 | (is (= (html [:ul (for [n (range 3)] 122 | [:li n])]) 123 | "")) 124 | (is (= (html [:div (if true 125 | [:span "foo"] 126 | [:span "bar"])]) 127 | "
foo
")) 128 | (is (= (html (let [x "foo"] [:span x])) 129 | "foo")) 130 | (is (= (html (when true [:span "true"])) 131 | "true")) 132 | (is (= (html (when false [:span "true"])) 133 | ""))) 134 | (testing "values are evaluated only once" 135 | (let [times-called (atom 0) 136 | foo #(swap! times-called inc)] 137 | (html [:div (foo)]) 138 | (is (= @times-called 1)))) 139 | (testing "defer evaluation of non-literal class names when combined with tag classes" 140 | (let [x "attr-class"] 141 | (is (= (html [:div.tag-class {:class x}]) 142 | "
"))))) 143 | 144 | (deftest render-modes 145 | (testing "closed tag" 146 | (is (= (html [:p] [:br]) "


")) 147 | (is (= (html {:mode :xhtml} [:p] [:br]) "


")) 148 | (is (= (html {:mode :html} [:p] [:br]) "


")) 149 | (is (= (html {:mode :xml} [:p] [:br]) "


")) 150 | (is (= (html {:mode :sgml} [:p] [:br]) "


"))) 151 | (testing "boolean attributes" 152 | (is (= (html {:mode :xml} [:input {:type "checkbox" :checked true}]) 153 | "")) 154 | (is (= (html {:mode :sgml} [:input {:type "checkbox" :checked true}]) 155 | ""))) 156 | (testing "laziness and binding scope" 157 | (is (= (html {:mode :sgml} [:html [:link] (list [:link])]) 158 | "")))) 159 | -------------------------------------------------------------------------------- /test/hiccup/def_test.clj: -------------------------------------------------------------------------------- 1 | (ns hiccup.def_test 2 | (:require [clojure.test :refer :all] 3 | [hiccup.def :refer :all])) 4 | 5 | (deftest test-defhtml 6 | (testing "basic html function" 7 | (defhtml basic-fn [x] [:span x]) 8 | (is (= (str (basic-fn "foo")) "foo"))) 9 | (testing "html function with overloads" 10 | (defhtml overloaded-fn 11 | ([x] [:span x]) 12 | ([x y] [:span x [:div y]])) 13 | (is (= (str (overloaded-fn "foo")) "foo")) 14 | (is (= (str (overloaded-fn "foo" "bar")) 15 | "foo

bar
")))) 16 | 17 | (deftest test-defelem 18 | (testing "one overload function" 19 | (defelem one-form-two-args [a b] [b a 3]) 20 | (is (thrown? IllegalArgumentException (one-form-two-args))) 21 | (is (thrown? IllegalArgumentException (one-form-two-args {}))) 22 | (is (thrown? IllegalArgumentException (one-form-two-args 1))) 23 | (is (= [1 0 3] (one-form-two-args 0 1))) 24 | (is (= [1 {:foo :bar} 0 3] (one-form-two-args {:foo :bar} 0 1))) 25 | (is (thrown? IllegalArgumentException (one-form-two-args 1 2 3))) 26 | (is (thrown? IllegalArgumentException (one-form-two-args 1 2 3 4)))) 27 | (testing "three overloads function" 28 | (defelem three-forms 29 | ([] [0]) 30 | ([a] [(* a a) 2]) 31 | ([a b] [b a])) 32 | (is (= [0] (three-forms))) 33 | (is (= [0 {:foo :bar}] (three-forms {:foo :bar}))) 34 | (is (= [4 2] (three-forms 2))) 35 | (is (= [4 {:foo :bar} 2] (three-forms {:foo :bar} 2))) 36 | (is (= [1 0] (three-forms 0 1))) 37 | (is (= [1 {:foo :bar} 0] (three-forms {:foo :bar} 0 1))) 38 | (is (thrown? IllegalArgumentException (three-forms 1 2 3))) 39 | (is (thrown? IllegalArgumentException (three-forms 1 2 3 4)))) 40 | (testing "recursive function" 41 | (defelem recursive [a] 42 | (if (< a 1) [a (inc a)] (recursive (- a 1)))) 43 | (is (= [0 1] (recursive 4))) 44 | (is (= [0 {:foo :bar} 1] (recursive {:foo :bar} 4)))) 45 | (testing "merge map if result has one" 46 | (defelem with-map 47 | ([] [1 {:foo :bar} 2]) 48 | ([a b] [a {:foo :bar} b])) 49 | (is (= [1 {:foo :bar} 2] (with-map))) 50 | (is (= [1 {:a :b :foo :bar} 2] (with-map {:a :b}))) 51 | (is (= [1 {:foo :bar} 2] (with-map 1 2))) 52 | (is (= [1 {:foo :bar :a :b} 2] (with-map {:a :b} 1 2)))) 53 | (testing "preserve meta info" 54 | (defelem three-forms-extra 55 | "my documentation" 56 | {:my :attr} 57 | ([] {:pre [false]} [0]) 58 | ([a] {:pre [(> a 1)]} [1]) 59 | ([a b] {:pre [(> a 1)]} [1 2])) 60 | (is (thrown? AssertionError (three-forms-extra))) 61 | (is (thrown? AssertionError (three-forms-extra 0))) 62 | (is (thrown? AssertionError (three-forms-extra 0 0))) 63 | (is (= "my documentation" (:doc (meta #'three-forms-extra)))) 64 | (is (= :attr (:my (meta #'three-forms-extra)))))) 65 | -------------------------------------------------------------------------------- /test/hiccup/element_test.clj: -------------------------------------------------------------------------------- 1 | (ns hiccup.element_test 2 | (:require [clojure.test :refer :all] 3 | [hiccup.element :refer :all]) 4 | (:import java.net.URI)) 5 | 6 | (deftest javascript-tag-test 7 | (is (= (javascript-tag "alert('hello');") 8 | [:script {:type "text/javascript"} 9 | "//"]))) 10 | 11 | (deftest link-to-test 12 | (is (= (link-to "/") 13 | [:a {:href (URI. "/")} nil])) 14 | (is (= (link-to "/" "foo") 15 | [:a {:href (URI. "/")} (list "foo")])) 16 | (is (= (link-to "/" "foo" "bar") 17 | [:a {:href (URI. "/")} (list "foo" "bar")]))) 18 | 19 | (deftest mail-to-test 20 | (is (= (mail-to "foo@example.com") 21 | [:a {:href "mailto:foo@example.com"} "foo@example.com"])) 22 | (is (= (mail-to "foo@example.com" "foo") 23 | [:a {:href "mailto:foo@example.com"} (list "foo")]))) 24 | 25 | (deftest unordered-list-test 26 | (is (= (unordered-list ["foo" "bar" "baz"]) 27 | [:ul (list [:li "foo"] 28 | [:li "bar"] 29 | [:li "baz"])]))) 30 | 31 | (deftest ordered-list-test 32 | (is (= (ordered-list ["foo" "bar" "baz"]) 33 | [:ol (list [:li "foo"] 34 | [:li "bar"] 35 | [:li "baz"])]))) 36 | 37 | -------------------------------------------------------------------------------- /test/hiccup/form_test.clj: -------------------------------------------------------------------------------- 1 | (ns hiccup.form_test 2 | (:require [clojure.test :refer :all] 3 | [hiccup.core :refer [html]] 4 | [hiccup.form :refer :all])) 5 | 6 | (deftest test-hidden-field 7 | (is (= (html (hidden-field :foo "bar")) 8 | ""))) 9 | 10 | (deftest test-hidden-field-with-extra-atts 11 | (is (= (html (hidden-field {:class "classy"} :foo "bar")) 12 | ""))) 13 | 14 | (deftest test-text-field 15 | (is (= (html (text-field :foo)) 16 | ""))) 17 | 18 | (deftest test-text-field-with-extra-atts 19 | (is (= (html (text-field {:class "classy"} :foo "bar")) 20 | ""))) 21 | 22 | (deftest test-check-box 23 | (is (= (html (check-box :foo true)) 24 | (str "")))) 26 | 27 | (deftest test-check-box-with-extra-atts 28 | (is (= (html (check-box {:class "classy"} :foo true 1)) 29 | (str "")))) 31 | 32 | (deftest test-password-field 33 | (is (= (html (password-field :foo "bar")) 34 | ""))) 35 | 36 | (deftest test-password-field-with-extra-atts 37 | (is (= (html (password-field {:class "classy"} :foo "bar")) 38 | ""))) 39 | 40 | (deftest test-email-field 41 | (is (= (html (email-field :foo "bar")) 42 | ""))) 43 | 44 | (deftest test-email-field-with-extra-atts 45 | (is (= (html (email-field {:class "classy"} :foo "bar")) 46 | ""))) 47 | 48 | (deftest test-radio-button 49 | (is (= (html (radio-button :foo true 1)) 50 | (str "")))) 52 | 53 | (deftest test-radio-button-with-extra-atts 54 | (is (= (html (radio-button {:class "classy"} :foo true 1)) 55 | (str "")))) 57 | 58 | (deftest test-select-options 59 | (are [x y] (= (html x) y) 60 | (select-options ["foo" "bar" "baz"]) 61 | "" 62 | (select-options ["foo" "bar"] "bar") 63 | "" 64 | (select-options [["Foo" 1] ["Bar" 2]]) 65 | "" 66 | (select-options [["Foo" [1 2]] ["Bar" [3 4]]]) 67 | (str "" 68 | "") 69 | (select-options [["Foo" [["bar" 1] ["baz" 2]]]]) 70 | (str "" 71 | "") 72 | (select-options [["Foo" [1 2]]] 2) 73 | (str "" 74 | ""))) 75 | 76 | 77 | 78 | (deftest test-drop-down 79 | (let [options ["op1" "op2"] 80 | selected "op1" 81 | select-options (html (select-options options selected))] 82 | (is (= (html (drop-down :foo options selected)) 83 | (str ""))))) 84 | 85 | (deftest test-drop-down-with-extra-atts 86 | (let [options ["op1" "op2"] 87 | selected "op1" 88 | select-options (html (select-options options selected))] 89 | (is (= (html (drop-down {:class "classy"} :foo options selected)) 90 | (str ""))))) 92 | 93 | (deftest test-text-area 94 | (is (= (html (text-area :foo "bar")) 95 | ""))) 96 | 97 | (deftest test-text-area-field-with-extra-atts 98 | (is (= (html (text-area {:class "classy"} :foo "bar")) 99 | ""))) 100 | 101 | (deftest test-text-area-escapes 102 | (is (= (html (text-area :foo "bar")) 103 | ""))) 104 | 105 | (deftest test-file-field 106 | (is (= (html (file-upload :foo)) 107 | ""))) 108 | 109 | (deftest test-file-field-with-extra-atts 110 | (is (= (html (file-upload {:class "classy"} :foo)) 111 | (str "")))) 113 | 114 | (deftest test-label 115 | (is (= (html (label :foo "bar")) 116 | ""))) 117 | 118 | (deftest test-label-with-extra-atts 119 | (is (= (html (label {:class "classy"} :foo "bar")) 120 | ""))) 121 | 122 | (deftest test-submit 123 | (is (= (html (submit-button "bar")) 124 | ""))) 125 | 126 | (deftest test-submit-button-with-extra-atts 127 | (is (= (html (submit-button {:class "classy"} "bar")) 128 | ""))) 129 | 130 | (deftest test-reset-button 131 | (is (= (html (reset-button "bar")) 132 | ""))) 133 | 134 | (deftest test-reset-button-with-extra-atts 135 | (is (= (html (reset-button {:class "classy"} "bar")) 136 | ""))) 137 | 138 | (deftest test-form-to 139 | (is (= (html (form-to [:post "/path"] "foo" "bar")) 140 | "
foobar
"))) 141 | 142 | (deftest test-form-to-with-hidden-method 143 | (is (= (html (form-to [:put "/path"] "foo" "bar")) 144 | (str "
" 145 | "" 146 | "foobar
")))) 147 | 148 | (deftest test-form-to-with-extr-atts 149 | (is (= (html (form-to {:class "classy"} [:post "/path"] "foo" "bar")) 150 | "
foobar
"))) 151 | 152 | (deftest test-with-group 153 | (testing "hidden-field" 154 | (is (= (html (with-group :foo (hidden-field :bar "val"))) 155 | ""))) 156 | (testing "text-field" 157 | (is (= (html (with-group :foo (text-field :bar))) 158 | ""))) 159 | (testing "checkbox" 160 | (is (= (html (with-group :foo (check-box :bar))) 161 | ""))) 162 | (testing "password-field" 163 | (is (= (html (with-group :foo (password-field :bar))) 164 | ""))) 165 | (testing "radio-button" 166 | (is (= (html (with-group :foo (radio-button :bar false "val"))) 167 | ""))) 168 | (testing "drop-down" 169 | (is (= (html (with-group :foo (drop-down :bar []))) 170 | (str "")))) 171 | (testing "text-area" 172 | (is (= (html (with-group :foo (text-area :bar))) 173 | (str "")))) 174 | (testing "file-upload" 175 | (is (= (html (with-group :foo (file-upload :bar))) 176 | ""))) 177 | (testing "label" 178 | (is (= (html (with-group :foo (label :bar "Bar"))) 179 | ""))) 180 | (testing "multiple with-groups" 181 | (is (= (html (with-group :foo (with-group :bar (text-field :baz)))) 182 | ""))) 183 | (testing "multiple elements" 184 | (is (= (html (with-group :foo (label :bar "Bar") (text-field :var))) 185 | "")))) 186 | -------------------------------------------------------------------------------- /test/hiccup/middleware_test.clj: -------------------------------------------------------------------------------- 1 | (ns hiccup.middleware_test 2 | (:require [clojure.test :refer :all] 3 | [hiccup.core :refer [html]] 4 | [hiccup.element :refer [link-to]] 5 | [hiccup.middleware :refer :all])) 6 | 7 | (defn test-handler [request] 8 | {:status 200 9 | :headers {"Content-Type" "text/html"} 10 | :body (html [:html [:body (link-to "/bar" "bar")]])}) 11 | 12 | (deftest test-wrap-base-url 13 | (let [resp ((wrap-base-url test-handler "/foo") {})] 14 | (is (= (:body resp) 15 | "bar")))) 16 | -------------------------------------------------------------------------------- /test/hiccup/page_test.clj: -------------------------------------------------------------------------------- 1 | (ns hiccup.page_test 2 | (:require [clojure.test :refer :all] 3 | [hiccup.page :refer :all]) 4 | (:import java.net.URI)) 5 | 6 | (deftest html4-test 7 | (is (= (html4 [:body [:p "Hello" [:br] "World"]]) 8 | (str "\n" 10 | "

Hello
World

")))) 11 | 12 | (deftest xhtml-test 13 | (is (= (xhtml [:body [:p "Hello" [:br] "World"]]) 14 | (str "\n" 15 | "\n" 17 | "" 18 | "

Hello
World

"))) 19 | (is (= (xhtml {:lang "en"} [:body "Hello World"]) 20 | (str "\n" 21 | "\n" 23 | "" 24 | "Hello World"))) 25 | (is (= (xhtml {:encoding "ISO-8859-1"} [:body "Hello World"]) 26 | (str "\n" 27 | "\n" 29 | "" 30 | "Hello World")))) 31 | 32 | (deftest html5-test 33 | (testing "HTML mode" 34 | (is (= (html5 [:body [:p "Hello" [:br] "World"]]) 35 | "\n

Hello
World

")) 36 | (is (= (html5 {:lang "en"} [:body "Hello World"]) 37 | "\nHello World")) 38 | (is (= (html5 {:prefix "og: http://ogp.me/ns#"} 39 | [:body "Hello World"]) 40 | (str "\n" 41 | "" 42 | "Hello World"))) 43 | (is (= (html5 {:prefix "og: http://ogp.me/ns#" 44 | :lang "en"} 45 | [:body "Hello World"]) 46 | (str "\n" 47 | "" 48 | "Hello World")))) 49 | (testing "XML mode" 50 | (is (= (html5 {:xml? true} [:body [:p "Hello" [:br] "World"]]) 51 | (str "\n" 52 | "\n" 53 | "

Hello
World

"))) 54 | (is (= (html5 {:xml? true, :lang "en"} [:body "Hello World"]) 55 | (str "\n" 56 | "\n" 57 | "" 58 | "Hello World"))) 59 | (is (= (html5 {:xml? true, 60 | "xml:og" "http://ogp.me/ns#"} [:body "Hello World"]) 61 | (str "\n" 62 | "\n" 63 | "" 64 | "Hello World"))) 65 | (is (= (html5 {:xml? true, :lang "en" 66 | "xml:og" "http://ogp.me/ns#"} [:body "Hello World"]) 67 | (str "\n" 68 | "\n" 69 | "" 70 | "Hello World"))))) 71 | 72 | (deftest include-js-test 73 | (is (= (include-js "foo.js") 74 | (list [:script {:type "text/javascript", :src (URI. "foo.js")}]))) 75 | (is (= (include-js "foo.js" "bar.js") 76 | (list [:script {:type "text/javascript", :src (URI. "foo.js")}] 77 | [:script {:type "text/javascript", :src (URI. "bar.js")}])))) 78 | 79 | (deftest include-css-test 80 | (is (= (include-css "foo.css") 81 | (list [:link {:type "text/css", :href (URI. "foo.css"), :rel "stylesheet"}]))) 82 | (is (= (include-css "foo.css" "bar.css") 83 | (list [:link {:type "text/css", :href (URI. "foo.css"), :rel "stylesheet"}] 84 | [:link {:type "text/css", :href (URI. "bar.css"), :rel "stylesheet"}])))) 85 | -------------------------------------------------------------------------------- /test/hiccup/util_test.clj: -------------------------------------------------------------------------------- 1 | (ns hiccup.util_test 2 | (:require [clojure.test :refer :all] 3 | [hiccup.util :refer :all]) 4 | (:import java.net.URI)) 5 | 6 | (deftest test-escaped-chars 7 | (is (= (escape-html "\"") """)) 8 | (is (= (escape-html "<") "<")) 9 | (is (= (escape-html ">") ">")) 10 | (is (= (escape-html "&") "&")) 11 | (is (= (escape-html "foo") "foo")) 12 | (is (= (escape-html "'") "'")) 13 | (is (= (binding [*html-mode* :sgml] (escape-html "'")) "'"))) 14 | 15 | (deftest test-as-str 16 | (is (= (as-str "foo") "foo")) 17 | (is (= (as-str :foo) "foo")) 18 | (is (= (as-str 100) "100")) 19 | (is (= (as-str 4/3) (str (float 4/3)))) 20 | (is (= (as-str "a" :b 3) "ab3")) 21 | (is (= (as-str (URI. "/foo")) "/foo")) 22 | (is (= (as-str (URI. "localhost:3000/foo")) "localhost:3000/foo"))) 23 | 24 | (deftest test-to-uri 25 | (testing "with no base URL" 26 | (is (= (to-str (to-uri "foo")) "foo")) 27 | (is (= (to-str (to-uri "/foo/bar")) "/foo/bar")) 28 | (is (= (to-str (to-uri "/foo#bar")) "/foo#bar"))) 29 | (testing "with base URL" 30 | (with-base-url "/foo" 31 | (is (= (to-str (to-uri "/bar")) "/foo/bar")) 32 | (is (= (to-str (to-uri "http://example.com")) "http://example.com")) 33 | (is (= (to-str (to-uri "https://example.com/bar")) "https://example.com/bar")) 34 | (is (= (to-str (to-uri "bar")) "bar")) 35 | (is (= (to-str (to-uri "../bar")) "../bar")) 36 | (is (= (to-str (to-uri "//example.com/bar")) "//example.com/bar")))) 37 | (testing "with base URL for root context" 38 | (with-base-url "/" 39 | (is (= (to-str (to-uri "/bar")) "/bar")) 40 | (is (= (to-str (to-uri "http://example.com")) "http://example.com")) 41 | (is (= (to-str (to-uri "https://example.com/bar")) "https://example.com/bar")) 42 | (is (= (to-str (to-uri "bar")) "bar")) 43 | (is (= (to-str (to-uri "../bar")) "../bar")) 44 | (is (= (to-str (to-uri "//example.com/bar")) "//example.com/bar")))) 45 | (testing "with base URL containing trailing slash" 46 | (with-base-url "/foo/" 47 | (is (= (to-str (to-uri "/bar")) "/foo/bar")) 48 | (is (= (to-str (to-uri "http://example.com")) "http://example.com")) 49 | (is (= (to-str (to-uri "https://example.com/bar")) "https://example.com/bar")) 50 | (is (= (to-str (to-uri "bar")) "bar")) 51 | (is (= (to-str (to-uri "../bar")) "../bar")) 52 | (is (= (to-str (to-uri "//example.com/bar")) "//example.com/bar"))))) 53 | 54 | (deftest test-url-encode 55 | (testing "strings" 56 | (are [s e] (= (url-encode s) e) 57 | "a" "a" 58 | "a b" "a+b" 59 | "&" "%26")) 60 | (testing "parameter maps" 61 | (are [m e] (= (url-encode m) e) 62 | {"a" "b"} "a=b" 63 | {:a "b"} "a=b" 64 | (sorted-map :a "b" :c "d") "a=b&c=d" 65 | {:a "&"} "a=%26" 66 | {:é "è"} "%C3%A9=%C3%A8")) 67 | (testing "different encodings" 68 | (are [e s] (= (with-encoding e (url-encode {:iroha "いろは"})) s) 69 | "UTF-8" "iroha=%E3%81%84%E3%82%8D%E3%81%AF" 70 | "Shift_JIS" "iroha=%82%A2%82%EB%82%CD" 71 | "EUC-JP" "iroha=%A4%A4%A4%ED%A4%CF" 72 | "ISO-2022-JP" "iroha=%1B%24%42%24%24%24%6D%24%4F%1B%28%42"))) 73 | 74 | (deftest test-url 75 | (testing "URL parts and parameters" 76 | (are [u s] (= u s) 77 | (url "foo") (URI. "foo") 78 | (url "foo/" 1) (URI. "foo/1") 79 | (url "/foo/" "bar") (URI. "/foo/bar") 80 | (url {:a "b"}) (URI. "?a=b") 81 | (url "foo" {:a "&"}) (URI. "foo?a=%26") 82 | (url "/foo/" 1 "/bar" {:page 2}) (URI. "/foo/1/bar?page=2")))) 83 | -------------------------------------------------------------------------------- /test/hiccup2/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns hiccup2.core_test 2 | (:require [clojure.test :refer :all] 3 | [hiccup2.core :refer :all] 4 | [hiccup.util :as util])) 5 | 6 | (deftest return-types 7 | (testing "html returns a RawString" 8 | (is (util/raw-string? (html [:div])))) 9 | (testing "converting to string" 10 | (is (= (str (html [:div])) "
")))) 11 | 12 | (deftest tag-names 13 | (testing "basic tags" 14 | (is (= (str (html [:div])) "
")) 15 | (is (= (str (html ["div"])) "
")) 16 | (is (= (str (html ['div])) "
"))) 17 | (testing "tag syntax sugar" 18 | (is (= (str (html [:div#foo])) "
")) 19 | (is (= (str (html [:div.foo])) "
")) 20 | (is (= (str (html [:div.foo (str "bar" "baz")])) 21 | "
barbaz
")) 22 | (is (= (str (html [:div.a.b])) "
")) 23 | (is (= (str (html [:div.a.b.c])) "
")) 24 | (is (= (str (html [:div#foo.bar.baz])) 25 | "
")))) 26 | 27 | (deftest tag-contents 28 | (testing "empty tags" 29 | (is (= (str (html [:div])) "
")) 30 | (is (= (str (html [:h1])) "

")) 31 | (is (= (str (html [:script])) "")) 32 | (is (= (str (html [:text])) "")) 33 | (is (= (str (html [:a])) "")) 34 | (is (= (str (html [:iframe])) "")) 35 | (is (= (str (html [:title])) "")) 36 | (is (= (str (html [:section])) "
")) 37 | (is (= (str (html [:select])) "")) 38 | (is (= (str (html [:object])) "")) 39 | (is (= (str (html [:video])) ""))) 40 | (testing "void tags" 41 | (is (= (str (html [:br])) "
")) 42 | (is (= (str (html [:link])) "")) 43 | (is (= (str (html [:colgroup {:span 2}])) "")) 44 | (is (= (str (html [:colgroup [:col]])) ""))) 45 | (testing "tags containing text" 46 | (is (= (str (html [:text "Lorem Ipsum"])) "Lorem Ipsum"))) 47 | (testing "contents are concatenated" 48 | (is (= (str (html [:body "foo" "bar"])) "foobar")) 49 | (is (= (str (html [:body [:p] [:br]])) "


"))) 50 | (testing "seqs are expanded" 51 | (is (= (str (html [:body (list "foo" "bar")])) "foobar")) 52 | (is (= (str (html (list [:p "a"] [:p "b"]))) "

a

b

"))) 53 | (testing "keywords are turned into strings" 54 | (is (= (str (html [:div :foo])) "
foo
"))) 55 | (testing "vecs don't expand - error if vec doesn't have tag name" 56 | (is (thrown? IllegalArgumentException 57 | (html (vector [:p "a"] [:p "b"]))))) 58 | (testing "tags can contain tags" 59 | (is (= (str (html [:div [:p]])) "

")) 60 | (is (= (str (html [:div [:b]])) "
")) 61 | (is (= (str (html [:p [:span [:a "foo"]]])) 62 | "

foo

")))) 63 | 64 | (deftest tag-attributes 65 | (testing "tag with blank attribute map" 66 | (is (= (str (html [:xml {}])) ""))) 67 | (testing "tag with populated attribute map" 68 | (is (= (str (html [:xml {:a "1", :b "2"}])) "")) 69 | (is (= (str (html [:img {"id" "foo"}])) "")) 70 | (is (= (str (html [:img {'id "foo"}])) "")) 71 | (is (= (str (html [:xml {:a "1", 'b "2", "c" "3"}])) 72 | ""))) 73 | (testing "attribute values are escaped" 74 | (is (= (str (html [:div {:id "\""}])) "
"))) 75 | (testing "boolean attributes" 76 | (is (= (str (html [:input {:type "checkbox" :checked true}])) 77 | "")) 78 | (is (= (str (html [:input {:type "checkbox" :checked false}])) 79 | ""))) 80 | (testing "nil attributes" 81 | (is (= (str (html [:span {:class nil} "foo"])) 82 | "foo"))) 83 | (testing "vector attributes" 84 | (is (= (str (html [:span {:class ["bar" "baz"]} "foo"])) 85 | "foo")) 86 | (is (= (str (html [:span {:class ["baz"]} "foo"])) 87 | "foo")) 88 | (is (= (str (html [:span {:class "baz bar"} "foo"])) 89 | "foo"))) 90 | (testing "map attributes" 91 | (is (= (str (html [:span {:style {:background-color :blue, :color "red", 92 | :line-width 1.2, :opacity "100%"}} "foo"])) 93 | "foo"))) 94 | (testing "resolving conflicts between attributes in the map and tag" 95 | (is (= (str (html [:div.foo {:class "bar"} "baz"])) 96 | "
baz
")) 97 | (is (= (str (html [:div.foo {:class ["bar"]} "baz"])) 98 | "
baz
")) 99 | (is (= (str (html [:div#bar.foo {:id "baq"} "baz"])) 100 | "
baz
")))) 101 | 102 | (deftest compiled-tags 103 | (testing "tag content can be vars" 104 | (is (= (let [x "foo"] (str (html [:span x]))) "foo"))) 105 | (testing "tag content can be forms" 106 | (is (= (str (html [:span (str (+ 1 1))])) "2")) 107 | (is (= (str (html [:span ({:foo "bar"} :foo)])) "bar"))) 108 | (testing "attributes can contain vars" 109 | (let [x "foo"] 110 | (is (= (str (html [:xml {:x x}])) "")) 111 | (is (= (str (html [:xml {x "x"}])) "")) 112 | (is (= (str (html [:xml {:x x} "bar"])) "bar")))) 113 | (testing "attributes are evaluated" 114 | (is (= (str (html [:img {:src (str "/foo" "/bar")}])) 115 | "")) 116 | (is (= (str (html [:div {:id (str "a" "b")} (str "foo")])) 117 | "
foo
"))) 118 | (testing "vector attributes are evaluated" 119 | (let [x "bar"] 120 | (is (= (str (html [:span {:class ["foo" x]}])) 121 | "")))) 122 | (testing "map attributes are evaluated" 123 | (let [color "red" 124 | bg-color :blue] 125 | (is (= (str (html [:span {:style {:background-color bg-color, :color color}} "foo"])) 126 | "foo")))) 127 | (testing "type hints" 128 | (let [string "x"] 129 | (is (= (str (html [:span ^String string])) "x")))) 130 | (testing "optimized forms" 131 | (is (= (str (html [:ul (for [n (range 3)] 132 | [:li n])])) 133 | "")) 134 | (is (= (str (html [:div (if true 135 | [:span "foo"] 136 | [:span "bar"])])) 137 | "
foo
"))) 138 | (testing "values are evaluated only once" 139 | (let [times-called (atom 0) 140 | foo #(swap! times-called inc)] 141 | (html [:div (foo)]) 142 | (is (= @times-called 1))))) 143 | 144 | (deftest render-modes 145 | (testing "closed tag" 146 | (is (= (str (html [:p] [:br])) "


")) 147 | (is (= (str (html {:mode :xhtml} [:p] [:br])) "


")) 148 | (is (= (str (html {:mode :html} [:p] [:br])) "


")) 149 | (is (= (str (html {:mode :xml} [:p] [:br])) "


")) 150 | (is (= (str (html {:mode :sgml} [:p] [:br])) "


"))) 151 | (testing "boolean attributes" 152 | (is (= (str (html {:mode :xml} [:input {:type "checkbox" :checked true}])) 153 | "")) 154 | (is (= (str (html {:mode :sgml} [:input {:type "checkbox" :checked true}])) 155 | ""))) 156 | (testing "laziness and binding scope" 157 | (is (= (str (html {:mode :sgml} [:html [:link] (list [:link])])) 158 | ""))) 159 | (testing "function binding scope" 160 | (let [f #(html [:p "<>" [:br]])] 161 | (is (= (str (html (f))) "

<>

")) 162 | (is (= (str (html {:escape-strings? false} (f))) "

<>

")) 163 | (is (= (str (html {:mode :html} (f))) "

<>

")) 164 | (is (= (str (html {:escape-strings? false, :mode :html} (f))) "

<>

"))))) 165 | 166 | (deftest auto-escaping 167 | (testing "literals" 168 | (is (= (str (html "<>")) "<>")) 169 | (is (= (str (html :<>)) "<>")) 170 | (is (= (str (html ^String (str "<>"))) "<>")) 171 | (is (= (str (html {} {"" ""})) "{"<a>" "<b>"}")) 172 | (is (= (str (html #{"<>"})) "#{"<>"}")) 173 | (is (= (str (html 1)) "1")) 174 | (is (= (str (html ^Number (+ 1 1))) "2"))) 175 | (testing "non-literals" 176 | (is (= (str (html (list [:p ""] [:p ""]))) 177 | "

<foo>

<bar>

")) 178 | (is (= (str (html ((constantly "")))) "<foo>")) 179 | (is (= (let [x ""] (str (html x))) "<foo>"))) 180 | (testing "optimized forms" 181 | (is (= (str (html (if true : :))) "<foo>")) 182 | (is (= (str (html (for [x [:]] x))) "<foo>"))) 183 | (testing "elements" 184 | (is (= (str (html [:p "<>"])) "

<>

")) 185 | (is (= (str (html [:p :<>])) "

<>

")) 186 | (is (= (str (html [:p {} {"" ""}])) 187 | "

{"<foo>" "<bar>"}

")) 188 | (is (= (str (html [:p {} #{""}])) 189 | "

#{"<foo>"}

")) 190 | (is (= (str (html [:p {:class "<\">"}])) 191 | "

")) 192 | (is (= (str (html [:p {:class ["<\">"]}])) 193 | "

")) 194 | (is (= (str (html [:ul [:li ""]])) 195 | "
  • <foo>
"))) 196 | (testing "raw strings" 197 | (is (= (str (html (util/raw-string ""))) "")) 198 | (is (= (str (html [:p (util/raw-string "")])) "

")) 199 | (is (= (str (html (html [:p "<>"]))) "

<>

")) 200 | (is (= (str (html [:ul (html [:li "<>"])])) "
  • <>
")))) 201 | 202 | (deftest html-escaping 203 | (testing "precompilation" 204 | (is (= (str (html {:escape-strings? true} [:p "<>"])) "

<>

")) 205 | (is (= (str (html {:escape-strings? false} [:p "<>"])) "

<>

"))) 206 | (testing "dynamic generation" 207 | (let [x [:p "<>"]] 208 | (is (= (str (html {:escape-strings? true} x)) "

<>

")) 209 | (is (= (str (html {:escape-strings? false} x)) "

<>

")))) 210 | (testing "attributes" 211 | (is (= (str (html {:escape-strings? true} [:p {:class "<>"}])) 212 | "

")) 213 | (is (= (str (html {:escape-strings? false} [:p {:class "<>"}])) 214 | "

"))) 215 | (testing "raw strings" 216 | (is (= (str (html {:escape-strings? true} [:p (util/raw-string "<>")])) 217 | "

<>

")) 218 | (is (= (str (html {:escape-strings? false} [:p (util/raw-string "<>")])) 219 | "

<>

")) 220 | (is (= (str (html {:escape-strings? true} [:p (raw "<>")])) 221 | "

<>

")) 222 | (is (= (str (html {:escape-strings? false} [:p (raw "<>")])) 223 | "

<>

")))) 224 | -------------------------------------------------------------------------------- /test/hiccup2/optimizations_test.clj: -------------------------------------------------------------------------------- 1 | (ns hiccup2.optimizations-test 2 | (:require [clojure.test :refer :all] 3 | [clojure.walk :as walk] 4 | [hiccup2.core :as h])) 5 | 6 | (defn- count-forms [data] 7 | (count (filter seq? (tree-seq coll? seq data)))) 8 | 9 | (deftest method-code-size 10 | ;; With Hiccup 2.0.0-RC2, it was easy to cause the hiccup2.core/html macro to 11 | ;; generate so much bytecode that it would go over the 64KB limit of how much 12 | ;; bytecode one Java method may contain. It would crash the Clojure compiler 13 | ;; with a "Method code too large!" exception. These are a regression tests for 14 | ;; that. See https://github.com/weavejester/hiccup/issues/205 15 | 16 | (testing "static elements should be concatenated to one string, also when they have dynamic sibling elements" 17 | (let [baseline (walk/macroexpand-all 18 | `(h/html [:div 19 | [:p] 20 | (identity nil) 21 | [:p]])) 22 | pathological (walk/macroexpand-all 23 | `(h/html [:div 24 | [:p] [:p] [:p] [:p] [:p] 25 | (identity nil) 26 | [:p] [:p] [:p] [:p] [:p]]))] 27 | (is (= (count-forms baseline) 28 | (count-forms pathological))))) 29 | 30 | (testing "code size should grow O(n), instead of O(n^2), as more dynamic first-child elements are added" 31 | (let [example-0 (walk/macroexpand-all 32 | `(h/html [:div 33 | [:div 34 | [:div 35 | [:div 36 | [:div]]]]])) 37 | example-1 (walk/macroexpand-all 38 | `(h/html [:div (identity nil) 39 | [:div 40 | [:div 41 | [:div 42 | [:div]]]]])) 43 | example-2 (walk/macroexpand-all 44 | `(h/html [:div (identity nil) 45 | [:div (identity nil) 46 | [:div 47 | [:div 48 | [:div]]]]])) 49 | example-3 (walk/macroexpand-all 50 | `(h/html [:div (identity nil) 51 | [:div (identity nil) 52 | [:div (identity nil) 53 | [:div 54 | [:div]]]]])) 55 | example-4 (walk/macroexpand-all 56 | `(h/html [:div (identity nil) 57 | [:div (identity nil) 58 | [:div (identity nil) 59 | [:div (identity nil) 60 | [:div]]]]])) 61 | example-5 (walk/macroexpand-all 62 | `(h/html [:div (identity nil) 63 | [:div (identity nil) 64 | [:div (identity nil) 65 | [:div (identity nil) 66 | [:div (identity nil)]]]]])) 67 | examples [example-0 68 | example-1 69 | example-2 70 | example-3 71 | example-4 72 | example-5] 73 | diffs (->> examples 74 | (map count-forms) 75 | (partition 2 1) 76 | (map (fn [[a b]] (- b a))))] 77 | (is (< (apply max diffs) 78 | (* 1.2 (apply min diffs))))))) 79 | --------------------------------------------------------------------------------