From d654135ffebe935317a1f946c123bd25e4fb6aa3 Mon Sep 17 00:00:00 2001 From: Asko Nõmm Date: Thu, 9 Oct 2025 22:05:47 +0300 Subject: Do away with the $ macro for runtime-agnostic purposes --- src/dompa/nodes.cljc | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 src/dompa/nodes.cljc (limited to 'src/dompa/nodes.cljc') 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 ""))))))) + +(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))))) -- cgit v1.2.3