summaryrefslogtreecommitdiff
path: root/src/dompa/nodes.cljc
diff options
context:
space:
mode:
authorAsko Nõmm <asko@nmm.ee>2025-10-09 22:05:47 +0300
committerAsko Nõmm <asko@nmm.ee>2025-10-09 22:05:47 +0300
commitd654135ffebe935317a1f946c123bd25e4fb6aa3 (patch)
tree4568d42e8732c0f80b55afee1e5efa302b8007c9 /src/dompa/nodes.cljc
parent06085b955bb2959a88844dae9f77a0f35ee6a8c5 (diff)
Do away with the $ macro for runtime-agnostic purposes
Diffstat (limited to 'src/dompa/nodes.cljc')
-rw-r--r--src/dompa/nodes.cljc117
1 files changed, 117 insertions, 0 deletions
diff --git a/src/dompa/nodes.cljc b/src/dompa/nodes.cljc
new file mode 100644
index 0000000..236271d
--- /dev/null
+++ b/src/dompa/nodes.cljc
@@ -0,0 +1,117 @@
+(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 [~@elements]))))
+
+(defn $
+ "Creates a new node
+
+ Usage:
+
+ ```clojure
+ ($ :div
+ ($ \"hello world\" ))
+ ```"
+ [name & opts]
+ (if (string? name)
+ {:node/name :dompa/text
+ :node/value (apply str name opts)}
+ (let [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 children)))))