blob: a434cf0a780a3fe15f346e4ca28caa047893e1b5 (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
|
(ns dompa.nodes)
(def ^:private default-void-nodes
#{:!doctype :area :base :br :col :embed :hr :img :input
:link :meta :source :track :wbr})
(defn- node-attrs-reducer [attrs k v]
(let [attr-name (-> k name)]
(if (true? v)
(str attrs " " attr-name)
(str attrs " " attr-name "=\"" v "\""))))
(defn- node->html-reducer-fn
"Returns a reducer function with the initial state of `void-nodes`
set and `nodes->html-fn` function for recursive HTML creation."
[void-nodes nodes->html-fn]
(fn [html node]
(cond
; fragment nodes expand their children to replace themselves
(and (not (nil? node))
(= (:node/name node) :<>))
(str html (nodes->html-fn (:node/children node)))
; otherwise business as usual
(not (nil? node))
(let [node-name (-> node :node/name name)
node-attrs (reduce-kv node-attrs-reducer "" (-> node :node/attrs))]
(cond
(= (-> node :node/name) :dompa/text)
(str html (-> node :node/value))
(contains? void-nodes (-> node :node/name))
(str html "<" node-name node-attrs">")
:else
(let [value (nodes->html-fn (-> node :node/children))]
(str html "<" node-name node-attrs ">" value "</" node-name ">")))))))
(defn traverse
"Recursively traverses given tree of `nodes` with a `traverser-fn`
that gets a single node passed to it and returns a new updated tree.
If the traverses function returns `nil`, the node will be removed.
In any other case the node will be replaced. If you wish to keep
a node unchanged, just return it as-is."
[nodes traverser-fn]
(-> (fn [updated-nodes node]
(if-let [updated-node (traverser-fn node)]
(let [children (traverse (-> updated-node :node/children) traverser-fn)]
(conj updated-nodes (assoc updated-node :node/children children)))
updated-nodes))
(reduce [] nodes)))
(defn ->html
"Transform a vector of `nodes` into an HTML string.
Options:
- `void-nodes` - A set of node names that are self-closing, defaults to:
- `:!doctype`
- `:area`
- `:base`
- `:br`
- `:col`
- `:embed`
- `:hr`
- `:img`
- `:input`
- `:link`
- `:meta`
- `:source`
- `:track`
- `:wbr`
"
([nodes]
(->html nodes {:void-nodes default-void-nodes}))
([nodes {:keys [void-nodes]}]
(-> (node->html-reducer-fn void-nodes ->html)
(reduce "" nodes))))
(defmacro defhtml
"Creates a new function with `name` that outputs HTML.
Example usage:
```clojure
(defhtml about-page [who]
($ :div
($ \"hello \" who)))
(about-page \"world\")
```
"
[name & args-and-elements]
(let [[args & elements] args-and-elements]
`(defn ~name ~args
(->html (vector ~@elements)))))
(defn $->flat-xf []
(fn [rf]
(letfn [(step [result input]
(if (sequential? input)
(reduce step result input)
(rf result input)))]
(fn
([] (rf))
([result] (rf result))
([result input] (step result input))))))
(defn $->flat [children]
(into [] ($->flat-xf) children))
(defmacro $
"A helper that simplifies node creation. Particularly useful
where you need compile-time composition over run-time, like when
combined with the `defhtml` macro to create HTML string outputs."
[name & opts]
`(if (string? ~name)
{:node/name :dompa/text
:node/value (str ~name ~@opts)}
(let [opts# (list ~@opts)
first-opt# (first opts#)
attrs?# (and (map? first-opt#)
(not (contains? first-opt# :node/name)))
attrs# (if attrs?# first-opt# {})
children# (if attrs?# (rest opts#) opts#)]
(cond-> {:node/name ~name}
attrs?# (assoc :node/attrs attrs#)
(seq children#) (assoc :node/children ($->flat children#))))))
|