summaryrefslogtreecommitdiff
path: root/src/dompa/nodes.cljc
blob: 24d446e0e43f5a67576f1a588c387ace906590ce (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
(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
  [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
      :else
      (when-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."
  [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#))))))