rlm@10
|
1 ;;; strint.clj -- String interpolation for Clojure
|
rlm@10
|
2 ;; originally proposed/published at http://muckandbrass.com/web/x/AgBP
|
rlm@10
|
3
|
rlm@10
|
4 ;; by Chas Emerick <cemerick@snowtide.com>
|
rlm@10
|
5 ;; December 4, 2009
|
rlm@10
|
6
|
rlm@10
|
7 ;; Copyright (c) Chas Emerick, 2009. All rights reserved. The use
|
rlm@10
|
8 ;; and distribution terms for this software are covered by the Eclipse
|
rlm@10
|
9 ;; Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
|
rlm@10
|
10 ;; which can be found in the file epl-v10.html at the root of this
|
rlm@10
|
11 ;; distribution. By using this software in any fashion, you are
|
rlm@10
|
12 ;; agreeing to be bound by the terms of this license. You must not
|
rlm@10
|
13 ;; remove this notice, or any other, from this software.
|
rlm@10
|
14
|
rlm@10
|
15 (ns
|
rlm@10
|
16 ^{:author "Chas Emerick",
|
rlm@10
|
17 :doc "String interpolation for Clojure."}
|
rlm@10
|
18 clojure.contrib.strint)
|
rlm@10
|
19
|
rlm@10
|
20 (defn- silent-read
|
rlm@10
|
21 "Attempts to clojure.core/read a single form from the provided String, returning
|
rlm@10
|
22 a vector containing the read form and a String containing the unread remainder
|
rlm@10
|
23 of the provided String. Returns nil if no valid form can be read from the
|
rlm@10
|
24 head of the String."
|
rlm@10
|
25 [s]
|
rlm@10
|
26 (try
|
rlm@10
|
27 (let [r (-> s java.io.StringReader. java.io.PushbackReader.)]
|
rlm@10
|
28 [(read r) (slurp r)])
|
rlm@10
|
29 (catch Exception e))) ; this indicates an invalid form -- the head of s is just string data
|
rlm@10
|
30
|
rlm@10
|
31 (defn- interpolate
|
rlm@10
|
32 "Yields a seq of Strings and read forms."
|
rlm@10
|
33 ([s atom?]
|
rlm@10
|
34 (lazy-seq
|
rlm@10
|
35 (if-let [[form rest] (silent-read (subs s (if atom? 2 1)))]
|
rlm@10
|
36 (cons form (interpolate (if atom? (subs rest 1) rest)))
|
rlm@10
|
37 (cons (subs s 0 2) (interpolate (subs s 2))))))
|
rlm@10
|
38 ([^String s]
|
rlm@10
|
39 (if-let [start (->> ["~{" "~("]
|
rlm@10
|
40 (map #(.indexOf s %))
|
rlm@10
|
41 (remove #(== -1 %))
|
rlm@10
|
42 sort
|
rlm@10
|
43 first)]
|
rlm@10
|
44 (lazy-seq (cons
|
rlm@10
|
45 (subs s 0 start)
|
rlm@10
|
46 (interpolate (subs s start) (= \{ (.charAt s (inc start))))))
|
rlm@10
|
47 [s])))
|
rlm@10
|
48
|
rlm@10
|
49 (defmacro <<
|
rlm@10
|
50 "Takes a single string argument and emits a str invocation that concatenates
|
rlm@10
|
51 the string data and evaluated expressions contained within that argument.
|
rlm@10
|
52 Evaluation is controlled using ~{} and ~() forms. The former is used for
|
rlm@10
|
53 simple value replacement using clojure.core/str; the latter can be used to
|
rlm@10
|
54 embed the results of arbitrary function invocation into the produced string.
|
rlm@10
|
55
|
rlm@10
|
56 Examples:
|
rlm@10
|
57 user=> (def v 30.5)
|
rlm@10
|
58 #'user/v
|
rlm@10
|
59 user=> (<< \"This trial required ~{v}ml of solution.\")
|
rlm@10
|
60 \"This trial required 30.5ml of solution.\"
|
rlm@10
|
61 user=> (<< \"There are ~(int v) days in November.\")
|
rlm@10
|
62 \"There are 30 days in November.\"
|
rlm@10
|
63 user=> (def m {:a [1 2 3]})
|
rlm@10
|
64 #'user/m
|
rlm@10
|
65 user=> (<< \"The total for your order is $~(->> m :a (apply +)).\")
|
rlm@10
|
66 \"The total for your order is $6.\"
|
rlm@10
|
67
|
rlm@10
|
68 Note that quotes surrounding string literals within ~() forms must be
|
rlm@10
|
69 escaped."
|
rlm@10
|
70 [string]
|
rlm@10
|
71 `(str ~@(interpolate string)))
|
rlm@10
|
72
|