5
\$\begingroup\$

I wrote a simple text-templating/processing system which resembles PHP, except that it works with Clojure. The main goal was to provide an easy way to use Clojure PHP-style for small files.

This is a usage example, the following file

<?clj
        (def theName "Soonog")

        (defn replaceAs
                [input]
                (clojure.string/replace input "a" "X")
        )
?>

Hello <% theName %>, this is a simple template which should allow
you to <% (replaceAs "see the basics how this might work some day") %>.

should output (here are two newlines at the start missing)

Hello Soonog, this is a simple template which should allow
you to see the bXsics how this might work some dXy.

A short info on how it works:

  • Accepts files as arguments
    • If no arguments are provided or the argument is "-" it will read from stdin
  • Outputs to stdout
  • Uses regular expressions to split/process the text
  • Everything in <?clj ... ?> is evaluated with load-string
  • Everything in <% ... %> is wrapped in (echo ... ) and then treated like above

A few things I'm unsure about:

  • The treatment of static variables (f.e. tag-start)
  • The style itself
  • Using regular expressions to parse files
  • load-strings will load it into the current context, so all files passed in share the same context...this is by design (for now) so that you can "include" files. Though, having a function that uses a separate context might be interesting if this is used as library.

And here's the code:

(ns closeddoor.core
    (:gen-class :main true))

(require 'clojure.string)


(def tag-start
    "The long/normal start tag."
    "<\\?clj")
(def tag-end
    "The long/normal end tag."
    "(\\?>|\\Z)")


(def echo-start
    "The short/echo start tag."
    "<%")
(def echo-end
    "The short/echo end tag."
    "%>")


(def tag-pattern
    "The compiled pattern for the long/normal tags."
    (re-pattern (str tag-start "((?s:.+?))" tag-end)))
(def echo-pattern
    "The compiled pattern for the short/echo tags."
    (re-pattern (str echo-start "((?s:.+?))" echo-end)))


(defn- process-match
    "Processes the given match, and returns the output of
    the given match."
    [[match group]]

    ; Create a StringBuilder which we'll use for our content.
    (def buffer (StringBuilder.))

    (defn echo
        "A small helper function to allow to echo things."
        [string]
        (.append buffer string))

    ; Now load what the regex delivered to use.
    (load-string (str "(use 'closeddoor.core)" group))

    ; Return the buffer.
    ; We're replacing the $ with an escaped version because this output
    ; is directly passed into string/replace.
    (clojure.string/replace (.toString buffer) "$" "\\$"))

(defn- process-match-echo-wrapped
    "Process the given match, but wraps it first in the echo function."
    [[match group]]
    (process-match [match (str "(echo " group ")")]))

(defn parse
    "Parses the given input, processes the matches and returns the result.
    The long/normal tags are processed first, after that the short/echo tags.
    Order of appearance does not matter, all normal tags are processed first."
    [input]
    (clojure.string/replace
        (clojure.string/replace
            input
            tag-pattern
            process-match)
        echo-pattern
        process-match-echo-wrapped))

(defn process
    "Processes the given source, which means that it reads everything from
    the source with slurp, runs it through parse and spits it out into out.

    So (process input *out*) is basically shorthand for (spit *out* (parse input))."
    [source out]
    (spit out (parse (slurp source))))



(defn -main
    "The main function which does everything."
    [& args]
    (if (empty? args)
        (process *in* *out*)
        (doseq [arg args]
            (if (= arg "-")
                (process *in* *out*)
                (process arg *out*)))))
\$\endgroup\$
5
\$\begingroup\$

Cool idea! I could definitely see this coming in handy in a variety of domains.

I noticed that you have some def and defn statements within the function definition of process-match. It's generally considered un-idiomatic to use def/defn anywhere except for at the top level of the namespace you're in. Usually you would use let instead if what you're trying to define isn't used outside of the function, but it doesn't look like that's the case here -- I think you are wanting buffer and echo to be used at the top level, so I would pull them out of the process-match function and do something like this:

(ns closeddoor.core
  (:require clojure.string)
  (:gen-class :main true))

(def tag-start "<\\?clj")  ; The long/normal start tag.
(def tag-end "(\\?>|\\Z)") ; The long/normal end tag.

(def echo-start "<%") ; The short/echo start tag.
(def echo-end "%>")   ; The short/echo end tag.

; the compiled regex patterns for each of the above
(def tag-pattern (re-pattern (str tag-start "((?s:.+?))" tag-end)))
(def echo-pattern (re-pattern (str echo-start "((?s:.+?))" echo-end)))

(def buffer (StringBuilder.))

(defn- echo [string]
  "A small helper function for echoing things."
  (.append buffer string))

(defn- process-match [[match group]]
  "Processes the given match, and returns the output of the given match."
  (load-string (str "(use 'closeddoor.core)" group))
  (clojure.string/replace (.toString buffer) "$" "\\$"))

(defn- process-match-echo-wrapped [[match group]]
  "Process the given match, but wraps it first in the echo function."
  (process-match [match (str "(echo " group ")")]))

(defn parse [input]
  "Parses the given input, processes the matches and returns the result.
   The long/normal tags are processed first, after that the short/echo tags.
   Order of appearance does not matter, all normal tags are processed first."
  (clojure.string/replace
    (clojure.string/replace input tag-pattern process-match)
    echo-pattern
    process-match-echo-wrapped))

(defn process [source out]
  "Processes the given source, which means that it reads everything from
   the source with slurp, runs it through parse and spits it out into out.
   So (process input *out*) is basically shorthand for (spit *out* (parse input))."
  (spit out (parse (slurp source))))

(defn -main [& args]
  "The main function which does everything."
  (if (empty? args)
    (process *in* *out*)
    (doseq [arg args]
      (if (= arg "-")
        (process *in* *out*)
        (process arg *out*)))))

BTW, I also changed your spacing from 4 spaces to 2 which is the Clojure standard. I guess there's nothing wrong with using 4 spaces, but 2 is predominantly what you see.

I've generally rearranged the spacing and line breaks to suit my own aesthetic preference, but it isn't necessarily "the right way," just my opinion on what formatting I find more readable. Just my 2 cents!

I also moved the (require 'clojure.string) into the namespace definition, which is idiomatic and more concise.

I'm a bit confused about your parse function... did you mean for the last argument of each call to clojure.string/replace function to be a function (process-match and process-match-echo-wrapped, respectively)? I believe you need to have strings there, indicating what to replace each match with. Maybe you meant (process-match match group), where match and group are replaced with the actual match and group? Maybe I'm missing something.

By the way, if you wanted to simplify the nested calls to clojure.string/replace in that function, you could do something like this:

(defn parse [input]
  "Parses the given input, processes the matches and returns the result.
   The long/normal tags are processed first, after that the short/echo tags.
   Order of appearance does not matter, all normal tags are processed first."
  (reduce (fn [s [pat rep]] 
            (clojure.string/replace s pat rep))
          input
          [[tag-pattern process-match]
           [echo-pattern pattern-match-echo-wrapped]]))

If I can offer one last criticism: your methods that deal with modifying buffer (which is initially defined as (StringBuilder.) are destructive actions, so you might consider adding ! onto the end of your function names (echo!, process-match!, etc.)

Hope this helps!

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.