rlm@10: ;;; strint.clj -- String interpolation for Clojure rlm@10: ;; originally proposed/published at http://muckandbrass.com/web/x/AgBP rlm@10: rlm@10: ;; by Chas Emerick rlm@10: ;; December 4, 2009 rlm@10: rlm@10: ;; Copyright (c) Chas Emerick, 2009. All rights reserved. The use rlm@10: ;; and distribution terms for this software are covered by the Eclipse rlm@10: ;; Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) rlm@10: ;; which can be found in the file epl-v10.html at the root of this rlm@10: ;; distribution. By using this software in any fashion, you are rlm@10: ;; agreeing to be bound by the terms of this license. You must not rlm@10: ;; remove this notice, or any other, from this software. rlm@10: rlm@10: (ns rlm@10: ^{:author "Chas Emerick", rlm@10: :doc "String interpolation for Clojure."} rlm@10: clojure.contrib.strint) rlm@10: rlm@10: (defn- silent-read rlm@10: "Attempts to clojure.core/read a single form from the provided String, returning rlm@10: a vector containing the read form and a String containing the unread remainder rlm@10: of the provided String. Returns nil if no valid form can be read from the rlm@10: head of the String." rlm@10: [s] rlm@10: (try rlm@10: (let [r (-> s java.io.StringReader. java.io.PushbackReader.)] rlm@10: [(read r) (slurp r)]) rlm@10: (catch Exception e))) ; this indicates an invalid form -- the head of s is just string data rlm@10: rlm@10: (defn- interpolate rlm@10: "Yields a seq of Strings and read forms." rlm@10: ([s atom?] rlm@10: (lazy-seq rlm@10: (if-let [[form rest] (silent-read (subs s (if atom? 2 1)))] rlm@10: (cons form (interpolate (if atom? (subs rest 1) rest))) rlm@10: (cons (subs s 0 2) (interpolate (subs s 2)))))) rlm@10: ([^String s] rlm@10: (if-let [start (->> ["~{" "~("] rlm@10: (map #(.indexOf s %)) rlm@10: (remove #(== -1 %)) rlm@10: sort rlm@10: first)] rlm@10: (lazy-seq (cons rlm@10: (subs s 0 start) rlm@10: (interpolate (subs s start) (= \{ (.charAt s (inc start)))))) rlm@10: [s]))) rlm@10: rlm@10: (defmacro << rlm@10: "Takes a single string argument and emits a str invocation that concatenates rlm@10: the string data and evaluated expressions contained within that argument. rlm@10: Evaluation is controlled using ~{} and ~() forms. The former is used for rlm@10: simple value replacement using clojure.core/str; the latter can be used to rlm@10: embed the results of arbitrary function invocation into the produced string. rlm@10: rlm@10: Examples: rlm@10: user=> (def v 30.5) rlm@10: #'user/v rlm@10: user=> (<< \"This trial required ~{v}ml of solution.\") rlm@10: \"This trial required 30.5ml of solution.\" rlm@10: user=> (<< \"There are ~(int v) days in November.\") rlm@10: \"There are 30 days in November.\" rlm@10: user=> (def m {:a [1 2 3]}) rlm@10: #'user/m rlm@10: user=> (<< \"The total for your order is $~(->> m :a (apply +)).\") rlm@10: \"The total for your order is $6.\" rlm@10: rlm@10: Note that quotes surrounding string literals within ~() forms must be rlm@10: escaped." rlm@10: [string] rlm@10: `(str ~@(interpolate string))) rlm@10: