Back
html (clj)
(source)macro
(html options & content)
Render Clojure data structures to a compiled representation of HTML. To turn
the representation into a string, use clojure.core/str. Strings inside the
macro are automatically HTML-escaped. To insert a string without it being
escaped, use the [[raw]] function.
A literal option map may be specified as the first argument. It accepts two
keys that control how the HTML is outputted:
`:mode`
: One of `:html`, `:xhtml`, `:xml` or `:sgml` (defaults to `:xhtml`).
Controls how tags are rendered.
`:escape-strings?`
: True if strings should be escaped (defaults to true).
Examples
hiccup
(ns hiccup2.optimizations-test
(:require [clojure.test :refer :all]
[clojure.walk :as walk]
[hiccup2.core :as h]))
(deftest method-code-size
;; With Hiccup 2.0.0-RC2, it was easy to cause the hiccup2.core/html macro to
;; generate so much bytecode that it would go over the 64KB limit of how much
;; bytecode one Java method may contain. It would crash the Clojure compiler
;; with a "Method code too large!" exception. These are a regression tests for
;; that. See https://github.com/weavejester/hiccup/issues/205
(testing "static elements should be concatenated to one string, also when they have dynamic sibling elements"
(let [baseline (walk/macroexpand-all
`(h/html [:div
[:p]
(identity nil)
[:p]]))
pathological (walk/macroexpand-all
`(h/html [:div
[:p] [:p] [:p] [:p] [:p]
(identity nil)
[:p] [:p] [:p] [:p] [:p]]))]
(is (= (count-forms baseline)
(count-forms pathological)))))
(testing "code size should grow O(n), instead of O(n^2), as more dynamic first-child elements are added"
(let [example-0 (walk/macroexpand-all
`(h/html [:div
[:div
[:div
[:div
[:div]]]]]))
example-1 (walk/macroexpand-all
`(h/html [:div (identity nil)
[:div
[:div
[:div
[:div]]]]]))
example-2 (walk/macroexpand-all
`(h/html [:div (identity nil)
[:div (identity nil)
[:div
[:div
[:div]]]]]))
example-3 (walk/macroexpand-all
`(h/html [:div (identity nil)
[:div (identity nil)
[:div (identity nil)
[:div
[:div]]]]]))
example-4 (walk/macroexpand-all
`(h/html [:div (identity nil)
[:div (identity nil)
[:div (identity nil)
[:div (identity nil)
[:div]]]]]))
example-5 (walk/macroexpand-all
`(h/html [:div (identity nil)
[:div (identity nil)
[:div (identity nil)
[:div (identity nil)
[:div (identity nil)]]]]]))
examples [example-0
example-1
example-2
example-3
example-4
example-5]
diffs (->> examples
(map count-forms)
(partition 2 1)
(map (fn [[a b]] (- b a))))]
(is (< (apply max diffs)
(* 1.2 (apply min diffs)))))))
hiccup
(ns hiccup2.core_test
(:require [clojure.test :refer :all]
[hiccup2.core :refer :all]
[hiccup.util :as util]))
(deftest return-types
(testing "html returns a RawString"
(is (util/raw-string? (html [:div]))))
(testing "converting to string"
(= (str (html [:div])) "<div></div>")))
(deftest tag-names
(testing "basic tags"
(is (= (str (html [:div])) "<div></div>"))
(is (= (str (html ["div"])) "<div></div>"))
(is (= (str (html ['div])) "<div></div>")))
(testing "tag syntax sugar"
(is (= (str (html [:div#foo])) "<div id=\"foo\"></div>"))
(is (= (str (html [:div.foo])) "<div class=\"foo\"></div>"))
(is (= (str (html [:div.foo (str "bar" "baz")]))
"<div class=\"foo\">barbaz</div>"))
(is (= (str (html [:div.a.b])) "<div class=\"a b\"></div>"))
(is (= (str (html [:div.a.b.c])) "<div class=\"a b c\"></div>"))
(is (= (str (html [:div#foo.bar.baz]))
"<div class=\"bar baz\" id=\"foo\"></div>"))))
(deftest tag-contents
(testing "empty tags"
(is (= (str (html [:div])) "<div></div>"))
(is (= (str (html [:h1])) "<h1></h1>"))
(is (= (str (html [:script])) "<script></script>"))
(is (= (str (html [:text])) "<text></text>"))
(is (= (str (html [:a])) "<a></a>"))
(is (= (str (html [:iframe])) "<iframe></iframe>"))
(is (= (str (html [:title])) "<title></title>"))
(is (= (str (html [:section])) "<section></section>"))
(is (= (str (html [:select])) "<select></select>"))
(is (= (str (html [:object])) "<object></object>"))
(is (= (str (html [:video])) "<video></video>")))
(testing "void tags"
(is (= (str (html [:br])) "<br />"))
(is (= (str (html [:link])) "<link />"))
(is (= (str (html [:colgroup {:span 2}])) "<colgroup span=\"2\"></colgroup>"))
(is (= (str (html [:colgroup [:col]])) "<colgroup><col /></colgroup>")))
(testing "tags containing text"
(is (= (str (html [:text "Lorem Ipsum"])) "<text>Lorem Ipsum</text>")))
(testing "contents are concatenated"
(is (= (str (html [:body "foo" "bar"])) "<body>foobar</body>"))
(is (= (str (html [:body [:p] [:br]])) "<body><p></p><br /></body>")))
(testing "seqs are expanded"
(is (= (str (html [:body (list "foo" "bar")])) "<body>foobar</body>"))
(is (= (str (html (list [:p "a"] [:p "b"]))) "<p>a</p><p>b</p>")))
(testing "keywords are turned into strings"
(is (= (str (html [:div :foo])) "<div>foo</div>")))
(testing "vecs don't expand - error if vec doesn't have tag name"
(is (thrown? IllegalArgumentException
(html (vector [:p "a"] [:p "b"])))))
(testing "tags can contain tags"
(is (= (str (html [:div [:p]])) "<div><p></p></div>"))
(is (= (str (html [:div [:b]])) "<div><b></b></div>"))
(is (= (str (html [:p [:span [:a "foo"]]]))
"<p><span><a>foo</a></span></p>"))))
(deftest tag-attributes
(testing "tag with blank attribute map"
(is (= (str (html [:xml {}])) "<xml></xml>")))
(testing "tag with populated attribute map"
(is (= (str (html [:xml {:a "1", :b "2"}])) "<xml a=\"1\" b=\"2\"></xml>"))
(is (= (str (html [:img {"id" "foo"}])) "<img id=\"foo\" />"))
(is (= (str (html [:img {'id "foo"}])) "<img id=\"foo\" />"))
(is (= (str (html [:xml {:a "1", 'b "2", "c" "3"}]))
"<xml a=\"1\" b=\"2\" c=\"3\"></xml>")))
(testing "attribute values are escaped"
(is (= (str (html [:div {:id "\""}])) "<div id=\""\"></div>")))
(testing "boolean attributes"
(is (= (str (html [:input {:type "checkbox" :checked true}]))
"<input checked=\"checked\" type=\"checkbox\" />"))
(is (= (str (html [:input {:type "checkbox" :checked false}]))
"<input type=\"checkbox\" />")))
(testing "nil attributes"
(is (= (str (html [:span {:class nil} "foo"]))
"<span>foo</span>")))
(testing "vector attributes"
(is (= (str (html [:span {:class ["bar" "baz"]} "foo"]))
"<span class=\"bar baz\">foo</span>"))
(is (= (str (html [:span {:class ["baz"]} "foo"]))
"<span class=\"baz\">foo</span>"))
(is (= (str (html [:span {:class "baz bar"} "foo"]))
"<span class=\"baz bar\">foo</span>")))
(testing "map attributes"
(is (= (str (html [:span {:style {:background-color :blue, :color "red",
:line-width 1.2, :opacity "100%"}} "foo"]))
"<span style=\"background-color:blue;color:red;line-width:1.2;opacity:100%;\">foo</span>")))
(testing "resolving conflicts between attributes in the map and tag"
(is (= (str (html [:div.foo {:class "bar"} "baz"]))
"<div class=\"foo bar\">baz</div>"))
(is (= (str (html [:div.foo {:class ["bar"]} "baz"]))
"<div class=\"foo bar\">baz</div>"))
(is (= (str (html [:div#bar.foo {:id "baq"} "baz"]))
"<div class=\"foo\" id=\"baq\">baz</div>"))))
(deftest compiled-tags
(testing "tag content can be vars"
(is (= (let [x "foo"] (str (html [:span x]))) "<span>foo</span>")))
(testing "tag content can be forms"
(is (= (str (html [:span (str (+ 1 1))])) "<span>2</span>"))
(is (= (str (html [:span ({:foo "bar"} :foo)])) "<span>bar</span>")))
(testing "attributes can contain vars"
(let [x "foo"]
(is (= (str (html [:xml {:x x}])) "<xml x=\"foo\"></xml>"))
(is (= (str (html [:xml {x "x"}])) "<xml foo=\"x\"></xml>"))
(is (= (str (html [:xml {:x x} "bar"])) "<xml x=\"foo\">bar</xml>"))))
(testing "attributes are evaluated"
(is (= (str (html [:img {:src (str "/foo" "/bar")}]))
"<img src=\"/foo/bar\" />"))
(is (= (str (html [:div {:id (str "a" "b")} (str "foo")]))
"<div id=\"ab\">foo</div>")))
(testing "type hints"
(let [string "x"]
(is (= (str (html [:span ^String string])) "<span>x</span>"))))
(testing "optimized forms"
(is (= (str (html [:ul (for [n (range 3)]
[:li n])]))
"<ul><li>0</li><li>1</li><li>2</li></ul>"))
(is (= (str (html [:div (if true
[:span "foo"]
[:span "bar"])]))
"<div><span>foo</span></div>")))
(testing "values are evaluated only once"
(let [times-called (atom 0)
foo #(swap! times-called inc)]
(html [:div (foo)])
(is (= @times-called 1)))))
(deftest render-modes
(testing "closed tag"
(is (= (str (html [:p] [:br])) "<p></p><br />"))
(is (= (str (html {:mode :xhtml} [:p] [:br])) "<p></p><br />"))
(is (= (str (html {:mode :html} [:p] [:br])) "<p></p><br>"))
(is (= (str (html {:mode :xml} [:p] [:br])) "<p /><br />"))
(is (= (str (html {:mode :sgml} [:p] [:br])) "<p><br>")))
(testing "boolean attributes"
(is (= (str (html {:mode :xml} [:input {:type "checkbox" :checked true}]))
"<input checked=\"checked\" type=\"checkbox\" />"))
(is (= (str (html {:mode :sgml} [:input {:type "checkbox" :checked true}]))
"<input checked type=\"checkbox\">")))
(testing "laziness and binding scope"
(is (= (str (html {:mode :sgml} [:html [:link] (list [:link])]))
"<html><link><link></html>")))
(testing "function binding scope"
(let [f #(html [:p "<>" [:br]])]
(is (= (str (html (f))) "<p><><br /></p>"))
(is (= (str (html {:escape-strings? false} (f))) "<p><><br /></p>"))
(is (= (str (html {:mode :html} (f))) "<p><><br></p>"))
(is (= (str (html {:escape-strings? false, :mode :html} (f))) "<p><><br></p>")))))
(deftest auto-escaping
(testing "literals"
(is (= (str (html "<>")) "<>"))
(is (= (str (html :<>)) "<>"))
(is (= (str (html ^String (str "<>"))) "<>"))
(is (= (str (html {} {"<a>" "<b>"})) "{"<a>" "<b>"}"))
(is (= (str (html #{"<>"})) "#{"<>"}"))
(is (= (str (html 1)) "1"))
(is (= (str (html ^Number (+ 1 1))) "2")))
(testing "non-literals"
(is (= (str (html (list [:p "<foo>"] [:p "<bar>"])))
"<p><foo></p><p><bar></p>"))
(is (= (str (html ((constantly "<foo>")))) "<foo>"))
(is (= (let [x "<foo>"] (str (html x))) "<foo>")))
(testing "optimized forms"
(is (= (str (html (if true :<foo> :<bar>))) "<foo>"))
(is (= (str (html (for [x [:<foo>]] x))) "<foo>")))
(testing "elements"
(is (= (str (html [:p "<>"])) "<p><></p>"))
(is (= (str (html [:p :<>])) "<p><></p>"))
(is (= (str (html [:p {} {"<foo>" "<bar>"}]))
"<p>{"<foo>" "<bar>"}</p>"))
(is (= (str (html [:p {} #{"<foo>"}]))
"<p>#{"<foo>"}</p>"))
(is (= (str (html [:p {:class "<\">"}]))
"<p class=\"<">\"></p>"))
(is (= (str (html [:p {:class ["<\">"]}]))
"<p class=\"<">\"></p>"))
(is (= (str (html [:ul [:li "<foo>"]]))
"<ul><li><foo></li></ul>")))
(testing "raw strings"
(is (= (str (html (util/raw-string "<foo>"))) "<foo>"))
(is (= (str (html [:p (util/raw-string "<foo>")])) "<p><foo></p>"))
(is (= (str (html (html [:p "<>"]))) "<p><></p>"))
(is (= (str (html [:ul (html [:li "<>"])])) "<ul><li><></li></ul>"))))
(deftest html-escaping
(testing "precompilation"
(is (= (str (html {:escape-strings? true} [:p "<>"])) "<p><></p>"))
(is (= (str (html {:escape-strings? false} [:p "<>"])) "<p><></p>")))
(testing "dynamic generation"
(let [x [:p "<>"]]
(is (= (str (html {:escape-strings? true} x)) "<p><></p>"))
(is (= (str (html {:escape-strings? false} x)) "<p><></p>"))))
(testing "attributes"
(is (= (str (html {:escape-strings? true} [:p {:class "<>"}]))
"<p class=\"<>\"></p>"))
(is (= (str (html {:escape-strings? false} [:p {:class "<>"}]))
"<p class=\"<>\"></p>")))
(testing "raw strings"
(is (= (str (html {:escape-strings? true} [:p (util/raw-string "<>")]))
"<p><></p>"))
(is (= (str (html {:escape-strings? false} [:p (util/raw-string "<>")]))
"<p><></p>"))
(is (= (str (html {:escape-strings? true} [:p (raw "<>")]))
"<p><></p>"))
(is (= (str (html {:escape-strings? false} [:p (raw "<>")]))
"<p><></p>"))))
hiccup
(ns hiccup.compiler-test
(:require [clojure.test :refer :all]
[clojure.walk :as walk]
[hiccup2.core :refer [html]])
(:import (hiccup.util RawString)))
(deftest test-compile-element-literal-tag
;; `compile-element ::literal-tag` behavior varies based on the following
;; things, so we need to test all their combinations:
;; - mode: xhtml, html, xml, sgml
;; - runtime type of the first child: attributes, content, nil
;; - tag: normal element, void element
(testing "runtime attributes,"
(testing "normal tag"
(is (= (str (html {:mode :xhtml} [:p (identity {:id 1})]))
"<p id=\"1\"></p>"))
(is (= (str (html {:mode :html} [:p (identity {:id 1})]))
"<p id=\"1\"></p>"))
(is (= (str (html {:mode :xml} [:p (identity {:id 1})]))
"<p id=\"1\" />"))
(is (= (str (html {:mode :sgml} [:p (identity {:id 1})]))
"<p id=\"1\">")))
(testing "void tag"
(is (= (str (html {:mode :xhtml} [:br (identity {:id 1})]))
"<br id=\"1\" />"))
(is (= (str (html {:mode :html} [:br (identity {:id 1})]))
"<br id=\"1\">"))
(is (= (str (html {:mode :xml} [:br (identity {:id 1})]))
"<br id=\"1\" />"))
(is (= (str (html {:mode :sgml} [:br (identity {:id 1})]))
"<br id=\"1\">"))))
(testing "runtime content,"
(testing "normal tag"
(is (= (str (html {:mode :xhtml} [:p (identity "x")])) "<p>x</p>"))
(is (= (str (html {:mode :html} [:p (identity "x")])) "<p>x</p>"))
(is (= (str (html {:mode :xml} [:p (identity "x")])) "<p>x</p>"))
(is (= (str (html {:mode :sgml} [:p (identity "x")])) "<p>x</p>")))
(testing "void tag"
;; it's not valid HTML to have content inside void elements,
;; but Hiccup should still obey what the user told it to do
(is (= (str (html {:mode :xhtml} [:br (identity "x")])) "<br>x</br>"))
(is (= (str (html {:mode :html} [:br (identity "x")])) "<br>x</br>"))
(is (= (str (html {:mode :xml} [:br (identity "x")])) "<br>x</br>"))
(is (= (str (html {:mode :sgml} [:br (identity "x")])) "<br>x</br>"))))
(testing "runtime nil,"
;; use case: a function which returns a map of attributes or nil
(testing "normal tag"
(is (= (str (html {:mode :xhtml} [:p (identity nil)])) "<p></p>"))
(is (= (str (html {:mode :html} [:p (identity nil)])) "<p></p>"))
(is (= (str (html {:mode :xml} [:p (identity nil)])) "<p />"))
(is (= (str (html {:mode :sgml} [:p (identity nil)])) "<p>")))
(testing "void tag"
(is (= (str (html {:mode :xhtml} [:br (identity nil)])) "<br />"))
(is (= (str (html {:mode :html} [:br (identity nil)])) "<br>"))
(is (= (str (html {:mode :xml} [:br (identity nil)])) "<br />"))
(is (= (str (html {:mode :sgml} [:br (identity nil)])) "<br>")))))
(deftest test-compile-element-default
(testing "runtime tag"
(is (= (str (html {:mode :xhtml} [(identity :p)])) "<p></p>"))
(is (= (str (html {:mode :html} [(identity :p)])) "<p></p>"))
(is (= (str (html {:mode :xml} [(identity :p)])) "<p />"))
(is (= (str (html {:mode :sgml} [(identity :p)])) "<p>")))
(testing "runtime tag with attributes"
(is (= (str (html {:mode :xhtml} [(identity :p) {:id 1}]))
(str (html {:mode :xhtml} [(identity :p) (identity {:id 1})]))
"<p id=\"1\"></p>"))
(is (= (str (html {:mode :html} [(identity :p) {:id 1}]))
(str (html {:mode :html} [(identity :p) (identity {:id 1})]))
"<p id=\"1\"></p>"))
(is (= (str (html {:mode :xml} [(identity :p) {:id 1}]))
(str (html {:mode :xml} [(identity :p) (identity {:id 1})]))
"<p id=\"1\" />"))
(is (= (str (html {:mode :sgml} [(identity :p) {:id 1}]))
(str (html {:mode :sgml} [(identity :p) (identity {:id 1})]))
"<p id=\"1\">")))
(testing "runtime tag with text content"
(is (= (str (html {:mode :xhtml} [(identity :p) "x"]))
(str (html {:mode :xhtml} [(identity :p) (identity "x")]))
"<p>x</p>"))
(is (= (str (html {:mode :html} [(identity :p) "x"]))
(str (html {:mode :html} [(identity :p) (identity "x")]))
"<p>x</p>"))
(is (= (str (html {:mode :xml} [(identity :p) "x"]))
(str (html {:mode :xml} [(identity :p) (identity "x")]))
"<p>x</p>"))
(is (= (str (html {:mode :sgml} [(identity :p) "x"]))
(str (html {:mode :sgml} [(identity :p) (identity "x")]))
"<p>x</p>")))
(testing "runtime tag with child elements"
(is (= (str (html {:mode :xhtml} [(identity :p) [:span "x"]]))
(str (html {:mode :xhtml} [(identity :p) (identity [:span "x"])]))
"<p><span>x</span></p>"))
(is (= (str (html {:mode :html} [(identity :p) [:span "x"]]))
(str (html {:mode :html} [(identity :p) (identity [:span "x"])]))
"<p><span>x</span></p>"))
(is (= (str (html {:mode :xml} [(identity :p) [:span "x"]]))
(str (html {:mode :xml} [(identity :p) (identity [:span "x"])]))
"<p><span>x</span></p>"))
(is (= (str (html {:mode :sgml} [(identity :p) [:span "x"]]))
(str (html {:mode :sgml} [(identity :p) (identity [:span "x"])]))
"<p><span>x</span></p>")))
(testing "compiles literal child elements"
(let [code (walk/macroexpand-all `(html [(identity :p) [:span "x"]]))]
(is (= (extract-strings code) #{"" "<span>x</span>"})))))
jaidetree/clj-cgi-example
(ns cgi.index
(:require
[hiccup2.core :refer [html]]))
(println "Content-Type: text/html\n")
(println
(str
(html
[:html
[:head
[:title "Hello"]
[:link
{:rel "stylesheet"
:type "text/css"
:href "/style.css"}]
[:link {:rel "stylesheet"
:href "https://fonts.googleapis.com/css2?family=Sriracha&display=swap"}]
[:script
{:src "https://kit.fontawesome.com/8c366f1e4e.js"
:crossorigin "anonymous"}]]
[:body
[:div.page
[:section
[:h1
[:i.fad.fa-hand-sparkles]
" Hello"]
[:p
"Looks like you made it! This page is running a Clojure script through "
[:a {:href "https://github.com/babashka/babashka"}
"Babashka"]
". To start hacking on this site, look at "
[:code "public_html/hello.clj"]
" for a quick reference."]]
[:section
[:h2
[:i.fad.fa-battery-full]
" Batteries Included"]
[:p
"Unlike a production-grade shared host, this docker development container has bb, Clojure, the JVM, lein, and the build essentials already installed. This means you can directly install deps from the shell and use Babashka's automatic pod downloading capabilities."]]
[:section
[:h2
[:i.fad.fa-info-circle]
" More Help"]
[:p
"For more help take a look at an article that covers the setup for production-grade shared hosts:"
[:br]
[:a {:href "https://eccentric-j.com/blog/clojure-like-its-php.html"}
"https://eccentric-j.com/blog/clojure-like-its-php.html "
[:i.fad.fa-file-alt]]]
[:p
"Check out the repo that maintains this docker file for project updates and reocmmendations."
[:br]
[:a
{:href "https://github.com/eccentric-j/clj-cgi-example"}
"https://github.com/eccentric-j/clj-cgi-example "
[:i.fab.fa-github]]]]
[:section.hacking
[:h3
{:style {:border-bottom "none"
:padding-bottom 0
:color "#7cdbab"}}
"Happy Hacking! "
[:i.fad.fa-laptop-code]]]
[:section.credit
[:p
{:style {:text-align "center"}}
[:a
{:href "https://eccentric-j.com"}
[:img
{:alt "Eccentric J"
:src "/eccentric-j-logo.svg"
:style {:width "10rem" :height "auto"
:margin-bottom "0.5rem"}}]]
[:br]
[:span
{:style {:font-size "0.8rem"
:color "rgba(255, 255, 255, 0.6)"
:font-style "italic"}}
"Powered by jaunty narcissism"]]]]]])))
jaidetree/clj-cgi-example
(ns cgi.lib.index
(:require
[hiccup2.core :refer [html]]))
(println "Content-Type: text/html\n")
(println
(str
(html
[:html
[:head
[:title "Libs"]
[:link
{:rel "stylesheet"
:type "text/css"
:href "/style.css"}]]
[:body
[:div.page
[:h1 "Libraries with CGI-Bin Clojure"]
[:p
"It's recommended to put your libs in this directory. For development purposes leaving them exposed in an accessible directory should be fine but in production they are best place outside of it."]
[:p
"Don't forget to add your libary jars, add pods, and set their permissions appropriately. In this local docker development server you should be able to use Clojure, lein, and babashka to install the libs and pods you need from the shell. However in a production-grade shared host, that is usually not an option so you will need to uberjar or build those libraries locally then upload and use the static binary versions of pods and babashka as well."]]]])))