newBieDev
newBieDev

Reputation: 504

Executing Functions Inside of Clojure String?

I'm working on a static site generator project and at one point I get a string representation of the site (in hiccup format) that I can manipulate. I convert that string to a map:

(clojure.edn/read-string page)

At which point I'll have my page as something like:

{ :header {
           :title "Index"
           :meta-desc "Indx Page"}
  :page [:div.Page.Home
          !!+Do-Math+!! ; This is the core of the question
          [:p (do-math 2 2)] ; This is also an option to replace the above line
          [:div.Home-primary-image-wrapper]
          ;More HTML stuff here
        ]
}

The goal then, as noted in the two lines above is to allow specific code to be executed before the html is rendered later.

My first thought was to embedded specific strings that are then replaced with function values:

(defn do-math []
  (str [:p (+ 2 2)]))

(clojure.edn/read-string (clojure.string/replace page "!!+Do-Math+!!" (do-math)))

The issue I have with this is that I'd like to be able to embed values into the strings to do more useful stuff.

A non-working example of what I'd like to accomplish

We have this string !!+Do-Math 2 2++!! which would then go to a modified version of the function above expecting two arguments:

(defn do-math [val val2]
      (str [:p (+ val val2)]))

The issuing I'm having is I'm not sure how to reliably replace strings like that, nor have I been able to pass the string values being replaced into the function doing the replacing. I'm not sure if that route is possible.

The other option would be to directly execute the functions in the map that I get back before rendering the html. In the example above I have:

[:p (do-math 2 2)]

Which would give me the same result. Is it possible to look through the map/vector and find instances of these function calls and execute them in the current namespace, and if so is that a better option that the string replace method above?

Upvotes: 3

Views: 156

Answers (2)

Rulle
Rulle

Reputation: 4901

I would define a special syntax that I use for things that can be evaluated and then use postwalk to traverse the tree. For example, you could decide that everything that would be evaluated is a vector starting with the namespaced keyword ::eval. For example, you could have this piece of data:

(def test-data
  [:div.Page.Home
   [:p [::eval
        :add
        [::eval :mul 3 3]
        [::eval :mul 4 4]]]
   [:div.Home-primary-image-wrapper]])

A treenode that satisfies this predicate is said to be a node that can be evaluated:

(defn evaluable? [x]
  (and (vector? x)
       (= ::eval (first x))))

Here is the function that traverses the tree and evaluates the nodes to be evaluated:

(defn evaluate-tree-nodes [tree evaluator-fn]
  (clojure.walk/postwalk
   #(if (evaluable? %)
      (apply evaluator-fn (rest %))
      %)
   tree))

And here is an evaluator function that you can define however you like to perform the evaluation. For example, you could decide that you only support two operations, :mul and :add:

(defn my-evaluator [f & args]
  (case f
    :add (apply + args)
    :mul (apply * args)))

And you would use it like this:

(evaluate-tree-nodes test-data my-evaluator)
;; => [:div.Page.Home [:p 25] [:div.Home-primary-image-wrapper]]

It is true that you could do something more fancy and try to map symbols in the expression to defined functions in the current namespace, but you might end up re-implementing Lisp if you are not careful. And it might introduce other complexities, as well. The above solution is just a hint of what you can do, up to you to extend it.

Upvotes: 2

Alan Thompson
Alan Thompson

Reputation: 29958

You can use clojure.walk/postwalk to traverse an arbitrary data structure, replacing any desired items with modified versions.

Remember to return the original item if you don't want to change it.

The code typically looks like:

(walk/postwalk
  (fn [item]
    (if <desired item>
      <modify item>
      item  ; original item 
    ))
  <some data structure> 
)

Please see this list of documentation, especially the Clojure CheatSheet.

Upvotes: 1

Related Questions