(ns dompa.nodes (:require [clojure.zip :as zip])) (def ^:private default-void-nodes #{:!doctype :!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 ">")))) :else html))) (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 zip "Creates a zipper for given a given `node`." [node] (zip/zipper (fn branch? [node] (boolean (seq (:node/children node)))) (fn children [node] (:node/children node)) (fn make-node [node children] (assoc node :node/children children)) node)) (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. Optionally accepts a docstring between the name and the argument vector. Functions created with `defhtml` can be nested and composed with each other. Example usage: ```clojure (defhtml greeting [who] ($ :span who)) (defhtml about-page \"Renders the about page.\" [who] ($ :div \"Hello, \" (greeting who))) (about-page \"world\") ;;=> \"