forked from boot-clj/boot-new
-
-
Notifications
You must be signed in to change notification settings - Fork 27
/
templates.clj
196 lines (170 loc) · 7.41 KB
/
templates.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
(ns boot.new.templates
"Boot version of leiningen.new.templates. Initially a direct copy
of leiningen.new.templates (modified to depend on Boot code instead
of Leiningen code), but will likely diverge over time."
(:require [clojure.java.io :as io]
[clojure.string :as string]
[stencil.core :as stencil])
(:import (java.util Calendar)))
(defn project-name
"Returns project name from (possibly group-qualified) name:
mygroup/myproj => myproj
myproj => myproj"
[s]
(last (string/split s #"/")))
(defn fix-line-separators
"Replace all \\n with system specific line separators."
[s]
(let [line-sep (if (System/getenv "BOOT_NEW_UNIX_NEWLINES") "\n"
(System/getProperty "line.separator"))]
(string/replace s "\n" line-sep)))
(defn slurp-to-lf
"Returns the entire contents of the given reader as a single string. Converts
all line endings to \\n."
[r]
(let [sb (StringBuilder.)]
(loop [s (.readLine r)]
(if (nil? s)
(str sb)
(do
(.append sb s)
(.append sb "\n")
(recur (.readLine r)))))))
(defn slurp-resource
"Reads the contents of a resource. Temporarily converts line endings in the
resource to \\n before converting them into system specific line separators
using fix-line-separators."
[resource]
(if (string? resource) ; for 2.0.0 compatibility, can break in 3.0.0
(-> resource io/resource io/reader slurp-to-lf fix-line-separators)
(-> resource io/reader slurp-to-lf fix-line-separators)))
(defn sanitize
"Replace hyphens with underscores."
[s]
(string/replace s "-" "_"))
(defn multi-segment
"Make a namespace multi-segmented by adding another segment if necessary.
The additional segment defaults to \"core\"."
([s] (multi-segment s "core"))
([s final-segment]
(if (.contains s ".")
s
(format "%s.%s" s final-segment))))
(defn name-to-path
"Constructs directory structure from fully qualified artifact name:
\"foo-bar.baz\" becomes \"foo_bar/baz\"
and so on. Uses platform-specific file separators."
[s]
(-> s sanitize (string/replace "." java.io.File/separator)))
(defn sanitize-ns
"Returns project namespace name from (possibly group-qualified) project name:
mygroup/myproj => mygroup.myproj
myproj => myproj
mygroup/my_proj => mygroup.my-proj"
[s]
(-> s
(string/replace "/" ".")
(string/replace "_" "-")))
(defn group-name
"Returns group name from (a possibly unqualified) name:
my.long.group/myproj => my.long.group
mygroup/myproj => mygroup
myproj => nil"
[s]
(let [grpseq (butlast (string/split (sanitize-ns s) #"\."))]
(if (seq grpseq)
(->> grpseq (interpose ".") (apply str)))))
(defn year
"Get the current year. Useful for setting copyright years and such."
[] (.get (Calendar/getInstance) Calendar/YEAR))
(defn date
"Get the current date as a string in ISO8601 format."
[]
(let [df (java.text.SimpleDateFormat. "yyyy-MM-dd")]
(.format df (java.util.Date.))))
;; It'd be silly to expect people to pull in stencil just to render a mustache
;; string. We can just provide this function instead. In doing so, it is much
;; less likely that template authors will have to pull in any external
;; libraries. Though they are welcome to if they need.
(defn render-text
[& args]
(apply stencil/render-string args))
(defn renderer
"Create a renderer function that looks for mustache templates in the
right place given the name of your template. If no data is passed, the
file is simply slurped and the content returned unchanged.
render-fn - Optional rendering function that will be used in place of the
default renderer. This allows rendering templates that contain
tags that conflic with the Stencil renderer such as {{..}}."
[name & [render-fn]]
(let [render (or render-fn render-text)]
(fn [template & [data]]
(let [path (string/join "/" ["boot" "new" (sanitize name) template])]
(if-let [resource (io/resource path)]
(if data
(render (slurp-resource resource) data)
(io/reader resource))
(throw (ex-info (format "Template resource '%s' not found." path) {})))))))
(defn raw-resourcer
"Create a renderer function that looks for raw files in the
right place given the name of your template."
[name]
(fn [file]
(let [path (string/join "/" ["boot" "new" (sanitize name) file])]
(if-let [resource (io/resource path)]
(io/input-stream resource)
(throw (ex-info (format "File '%s' not found." path) {}))))))
;; Our file-generating function, `->files` is very simple. We'd like
;; to keep it that way. Sometimes you need your file paths to be
;; templates as well. This function just renders a string that is the
;; path to where a file is supposed to be placed by a template.
;; It is private because you shouldn't have to call it yourself, since
;; `->files` does it for you.
(defn- template-path [name path data]
(io/file name (render-text path data)))
(def ^{:dynamic true} *dir* nil)
(def ^{:dynamic true} *force?* false)
(def ^{:dynamic true} *overwrite?* true)
;; A template, at its core, is meant to generate files and directories that
;; represent a project. This is our way of doing that. `->files` is basically
;; a mini-DSL for generating files. It takes your mustache template data and
;; any number of vectors or strings. It iterates through those arguments and
;; when it sees a vector, it treats the first element as the path to spit to
;; and the second element as the contents to put there. If it encounters a
;; string, it treats it as an empty directory that should be created. Any parent
;; directories for any of our generated files and directories are created
;; automatically. All paths are considered mustache templates and are rendered
;; with our data. Of course, this doesn't effect paths that don't have templates
;; in them, so it is all transparent unless you need it.
(defn ->files
"Generate a file with content. path can be a java.io.File or string.
It will be turned into a File regardless. Any parent directories will be
created automatically. Data should include a key for :name so that the project
is created in the correct directory."
[{:keys [name] :as data} & paths]
(let [dir (or *dir*
(-> (System/getProperty "user.dir")
(io/file name) (.getPath)))]
(if (or (= "." dir) (.mkdir (io/file dir)) *force?*)
(doseq [path (remove nil? paths)]
(if (string? path)
(.mkdirs (template-path dir path data))
(let [[path content & options] path
path (template-path dir path data)
options (apply hash-map options)]
(.mkdirs (.getParentFile path))
(cond (not (.exists path))
(io/copy content path)
(:append options)
(with-open [w (io/writer path :append true)]
(io/copy content w))
(or *overwrite?* *force?* (:overwrite options))
(io/copy content path)
:else
(println (str path " exists."
" Use -f / --force to overwrite it.")))
(when (:executable options)
(.setExecutable path true)))))
(println (str "Could not create directory " dir
". Maybe it already exists?"
" Use -f / --force to overwrite it.")))))