mustache 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/LICENSE +20 -0
- data/README.md +307 -0
- data/Rakefile +36 -0
- data/benchmarks/complex.erb +15 -0
- data/benchmarks/helper.rb +20 -0
- data/benchmarks/simple.erb +5 -0
- data/benchmarks/speed.rb +46 -0
- data/examples/comments.html +1 -0
- data/examples/comments.rb +14 -0
- data/examples/complex_view.html +16 -0
- data/examples/complex_view.rb +34 -0
- data/examples/escaped.html +1 -0
- data/examples/escaped.rb +14 -0
- data/examples/inner_partial.html +1 -0
- data/examples/simple.html +5 -0
- data/examples/simple.rb +26 -0
- data/examples/template_partial.html +2 -0
- data/examples/template_partial.rb +14 -0
- data/examples/unescaped.html +1 -0
- data/examples/unescaped.rb +14 -0
- data/examples/view_partial.html +3 -0
- data/examples/view_partial.rb +18 -0
- data/lib/mustache.rb +218 -0
- data/lib/mustache/sinatra.rb +56 -0
- data/test/mustache_test.rb +111 -0
- metadata +88 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
doc
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Chris Wanstrath
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,307 @@
|
|
1
|
+
Mustache
|
2
|
+
=========
|
3
|
+
|
4
|
+
Inspired by [ctemplate](http://code.google.com/p/google-ctemplate/)
|
5
|
+
and
|
6
|
+
[et](http://www.ivan.fomichev.name/2008/05/erlang-template-engine-prototype.html),
|
7
|
+
Mustache is a framework-agnostic way to render logic-free views.
|
8
|
+
|
9
|
+
As ctemplates says, "It emphasizes separating logic from presentation:
|
10
|
+
it is impossible to embed application logic in this template language."
|
11
|
+
|
12
|
+
|
13
|
+
Overview
|
14
|
+
--------
|
15
|
+
|
16
|
+
Think of Mustache as a replacement for your views. Instead of views
|
17
|
+
consisting of ERB or HAML with random helpers and arbitrary logic,
|
18
|
+
your views are broken into two parts: a Ruby class and an HTML
|
19
|
+
template.
|
20
|
+
|
21
|
+
We call the Ruby class the "view" and the HTML template the
|
22
|
+
"template."
|
23
|
+
|
24
|
+
All your logic, decisions, and code is contained in your view. All
|
25
|
+
your markup is contained in your template. The template does nothing
|
26
|
+
but reference methods in your view.
|
27
|
+
|
28
|
+
This strict separation makes it easier to write clean templates,
|
29
|
+
easier to test your views, and more fun to work on your app's front end.
|
30
|
+
|
31
|
+
|
32
|
+
Why?
|
33
|
+
----
|
34
|
+
|
35
|
+
I like writing Ruby. I like writing HTML. I like writing JavaScript.
|
36
|
+
|
37
|
+
I don't like writing ERB, Haml, Liquid, Django Templates, putting Ruby
|
38
|
+
in my HTML, or putting JavaScript in my HTML.
|
39
|
+
|
40
|
+
|
41
|
+
Usage
|
42
|
+
-----
|
43
|
+
|
44
|
+
We've got an `examples` folder but here's the canonical one:
|
45
|
+
|
46
|
+
class Simple < Mustache
|
47
|
+
def name
|
48
|
+
"Chris"
|
49
|
+
end
|
50
|
+
|
51
|
+
def value
|
52
|
+
10_000
|
53
|
+
end
|
54
|
+
|
55
|
+
def taxed_value
|
56
|
+
value - (value * 0.4)
|
57
|
+
end
|
58
|
+
|
59
|
+
def in_ca
|
60
|
+
true
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
We simply create a normal Ruby class and define methods. Some methods
|
65
|
+
reference others, some return values, some return only booleans.
|
66
|
+
|
67
|
+
Now let's write the template:
|
68
|
+
|
69
|
+
Hello {{name}}
|
70
|
+
You have just won ${{value}}!
|
71
|
+
{{#in_ca}}
|
72
|
+
Well, ${{taxed_value}}, after taxes.
|
73
|
+
{{/in_ca}}
|
74
|
+
|
75
|
+
This template references our view methods. To bring it all together,
|
76
|
+
here's the code to render actual HTML;
|
77
|
+
|
78
|
+
Simple.new.to_html
|
79
|
+
|
80
|
+
Which returns the following:
|
81
|
+
|
82
|
+
Hello Chris
|
83
|
+
You have just won $10000!
|
84
|
+
Well, $6000.0, after taxes.
|
85
|
+
|
86
|
+
Simple.
|
87
|
+
|
88
|
+
|
89
|
+
Tag Types
|
90
|
+
---------
|
91
|
+
|
92
|
+
Tags are indicated by the double mustaches. `{{name}}` is a tag. Let's
|
93
|
+
talk about the different types of tags.
|
94
|
+
|
95
|
+
### Variables
|
96
|
+
|
97
|
+
The most basic tag is the variable. A `{{name}}` tag in a basic
|
98
|
+
template will try to call the `name` method on your view. If there is
|
99
|
+
no `name` method, an exception will be raised.
|
100
|
+
|
101
|
+
All variables are HTML escaped by default. If you want, for some
|
102
|
+
reason, to return unescaped HTML you can use the triple mustache:
|
103
|
+
`{{{name}}}`.
|
104
|
+
|
105
|
+
### Boolean Sections
|
106
|
+
|
107
|
+
A section begins with a pound and ends with a slash. That is,
|
108
|
+
`{{#person}}` begins a "person" section while `{{/person}}` ends it.
|
109
|
+
|
110
|
+
If the `person` method exists and calling it returns false, the HTML
|
111
|
+
between the pound and slash will not be displayed.
|
112
|
+
|
113
|
+
If the `person` method exists and calling it returns true, the HTML
|
114
|
+
between the pound and slash will be rendered and displayed.
|
115
|
+
|
116
|
+
### Enumerable Sections
|
117
|
+
|
118
|
+
Enumerable sections are syntactically identical to boolean sections in
|
119
|
+
that they begin with a pound and end with a slash. The difference,
|
120
|
+
however, is in the view: if the method called returns an enumerable,
|
121
|
+
the section is repeated as the enumerable is iterated over.
|
122
|
+
|
123
|
+
Each item in the enumerable is expected to be a hash which will then
|
124
|
+
become the context of the corresponding iteration. In this way we can
|
125
|
+
construct loops.
|
126
|
+
|
127
|
+
For example, imagine this template:
|
128
|
+
|
129
|
+
{{#repo}}
|
130
|
+
<b>{{name}}</b>
|
131
|
+
{{/repo}}
|
132
|
+
|
133
|
+
And this view code:
|
134
|
+
|
135
|
+
def repo
|
136
|
+
Repository.all.map { |r| { :name => r.to_s } }
|
137
|
+
end
|
138
|
+
|
139
|
+
When rendered, our view will contain a list of all repository names in
|
140
|
+
the database.
|
141
|
+
|
142
|
+
### Comments
|
143
|
+
|
144
|
+
Comments begin with a bang and are ignored. The following template:
|
145
|
+
|
146
|
+
<h1>Today{{! ignore me }}.</h1>
|
147
|
+
|
148
|
+
Will render as follows:
|
149
|
+
|
150
|
+
<h1>Today.</h1>
|
151
|
+
|
152
|
+
### Partials
|
153
|
+
|
154
|
+
Partials begin with a less than sign, like `{{< box}}`.
|
155
|
+
|
156
|
+
If a partial's view is loaded, we use that to render the HTML. If
|
157
|
+
nothing is loaded we render the template directly using our current context.
|
158
|
+
|
159
|
+
In this way partials can reference variables or sections the calling
|
160
|
+
view defines.
|
161
|
+
|
162
|
+
|
163
|
+
Dict-Style Views
|
164
|
+
----------------
|
165
|
+
|
166
|
+
ctemplate and friends want you to hand a dictionary to the template
|
167
|
+
processor. Naturally Mustache supports a similar concept. Feel free
|
168
|
+
to mix the class-based and this more procedural style at your leisure.
|
169
|
+
|
170
|
+
Given this template (dict.html):
|
171
|
+
|
172
|
+
Hello {{name}}
|
173
|
+
You have just won ${{value}}!
|
174
|
+
|
175
|
+
We can fill in the values at will:
|
176
|
+
|
177
|
+
dict = Dict.new
|
178
|
+
dict[:name] = 'George'
|
179
|
+
dict[:value] = 100
|
180
|
+
dict.to_html
|
181
|
+
|
182
|
+
Which returns:
|
183
|
+
|
184
|
+
Hello George
|
185
|
+
You have just won $100!
|
186
|
+
|
187
|
+
We can re-use the same object, too:
|
188
|
+
|
189
|
+
dict[:name] = 'Tony'
|
190
|
+
dict.to_html
|
191
|
+
Hello Tony
|
192
|
+
You have just won $100!
|
193
|
+
|
194
|
+
|
195
|
+
Templates
|
196
|
+
---------
|
197
|
+
|
198
|
+
A word on templates. By default, a view will try to find its template
|
199
|
+
on disk by searching for an HTML file in the current directory that
|
200
|
+
follows the classic Ruby naming convention.
|
201
|
+
|
202
|
+
TemplatePartial => ./template_partial.html
|
203
|
+
|
204
|
+
You can set the search path using `Mustache.path`. It can be set on a
|
205
|
+
class by class basis:
|
206
|
+
|
207
|
+
class Simple < Mustache
|
208
|
+
self.path = File.dirname(__FILE__)
|
209
|
+
... etc ...
|
210
|
+
end
|
211
|
+
|
212
|
+
Now `Simple` will look for `simple.html` in the directory it resides
|
213
|
+
in, no matter the cwd.
|
214
|
+
|
215
|
+
If you want to just change what template is used you can set
|
216
|
+
`Mustache.template_file` directly:
|
217
|
+
|
218
|
+
Simple.template_file = './blah.html'
|
219
|
+
|
220
|
+
You can also go ahead and set the template directly:
|
221
|
+
|
222
|
+
Simple.template = 'Hi {{person}}!'
|
223
|
+
|
224
|
+
You can also set a different template for only a single instance:
|
225
|
+
|
226
|
+
Simple.new.template = 'Hi {{person}}!'
|
227
|
+
|
228
|
+
Whatever works.
|
229
|
+
|
230
|
+
|
231
|
+
Helpers
|
232
|
+
-------
|
233
|
+
|
234
|
+
What about global helpers? Maybe you have a nifty `gravatar` function
|
235
|
+
you want to use in all your views? No problem.
|
236
|
+
|
237
|
+
This is just Ruby, after all.
|
238
|
+
|
239
|
+
module ViewHelpers
|
240
|
+
def gravatar(email, size = 30)
|
241
|
+
gravatar_id = Digest::MD5.hexdigest(email.to_s.strip.downcase)
|
242
|
+
gravatar_for_id(gravatar_id, size)
|
243
|
+
end
|
244
|
+
|
245
|
+
def gravatar_for_id(gid, size = 30)
|
246
|
+
"#{gravatar_host}/avatar/#{gid}?s=#{size}"
|
247
|
+
end
|
248
|
+
|
249
|
+
def gravatar_host
|
250
|
+
@ssl ? 'https://secure.gravatar.com' : 'http://www.gravatar.com'
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
Then just include it:
|
255
|
+
|
256
|
+
class Simple < Mustache
|
257
|
+
include ViewHelpers
|
258
|
+
|
259
|
+
def name
|
260
|
+
"Chris"
|
261
|
+
end
|
262
|
+
|
263
|
+
def value
|
264
|
+
10_000
|
265
|
+
end
|
266
|
+
|
267
|
+
def taxed_value
|
268
|
+
value - (value * 0.4)
|
269
|
+
end
|
270
|
+
|
271
|
+
def in_ca
|
272
|
+
true
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
Great, but what about that `@ssl` ivar in `gravatar_host`? There are
|
277
|
+
many ways we can go about setting it.
|
278
|
+
|
279
|
+
Here's on example which illustrates a key feature of Mustache: you
|
280
|
+
are free to use the `initialize` method just as you would in any
|
281
|
+
normal class.
|
282
|
+
|
283
|
+
class Simple < Mustache
|
284
|
+
include ViewHelpers
|
285
|
+
|
286
|
+
def initialize(ssl = false)
|
287
|
+
@ssl = ssl
|
288
|
+
end
|
289
|
+
|
290
|
+
... etc ...
|
291
|
+
end
|
292
|
+
|
293
|
+
Now:
|
294
|
+
|
295
|
+
Simple.new(request.ssl?).to_html
|
296
|
+
|
297
|
+
Convoluted but you get the idea.
|
298
|
+
|
299
|
+
|
300
|
+
Meta
|
301
|
+
----
|
302
|
+
|
303
|
+
* Code: `git clone git://github.com/defunkt/mustache.git`
|
304
|
+
* Bugs: <http://github.com/defunkt/mustache/issues>
|
305
|
+
* List: <http://groups.google.com/group/mustache-rb>
|
306
|
+
* Test: <http://runcoderun.com/defunkt/mustache>
|
307
|
+
* Boss: Chris Wanstrath :: <http://github.com/defunkt>
|
data/Rakefile
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
|
3
|
+
task :default => :test
|
4
|
+
|
5
|
+
Rake::TestTask.new do |t|
|
6
|
+
t.libs << 'lib'
|
7
|
+
t.pattern = 'test/**/*_test.rb'
|
8
|
+
t.verbose = false
|
9
|
+
end
|
10
|
+
|
11
|
+
begin
|
12
|
+
require 'jeweler'
|
13
|
+
$LOAD_PATH.unshift 'lib'
|
14
|
+
require 'mustache/version'
|
15
|
+
Jeweler::Tasks.new do |gemspec|
|
16
|
+
gemspec.name = "mustache"
|
17
|
+
gemspec.summary = "Mustache is a framework-agnostic way to render logic-free views."
|
18
|
+
gemspec.description = "Mustache is a framework-agnostic way to render logic-free views."
|
19
|
+
gemspec.email = "chris@ozmm.org"
|
20
|
+
gemspec.homepage = "http://github.com/defunkt/mustache"
|
21
|
+
gemspec.authors = ["Chris Wanstrath"]
|
22
|
+
gemspec.version = Mustache::Version
|
23
|
+
end
|
24
|
+
rescue LoadError
|
25
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "Build sdoc documentation"
|
29
|
+
task :doc do
|
30
|
+
File.open('README.html', 'w') do |f|
|
31
|
+
require 'rdiscount'
|
32
|
+
f.puts Markdown.new(File.read('README.md')).to_html
|
33
|
+
end
|
34
|
+
|
35
|
+
exec "sdoc -N --main=README.html README.html LICENSE lib"
|
36
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<h1><%= header %></h1>
|
2
|
+
<% if not item.empty? %>
|
3
|
+
<ul>
|
4
|
+
<% for i in item %>
|
5
|
+
<% if i[:current] %>
|
6
|
+
<li><strong><%= i[:name] %></strong></li>
|
7
|
+
<% else %>
|
8
|
+
<li><a href="<%= i[:url] %>"><%= i[:name] %></a></li>
|
9
|
+
<% end %>
|
10
|
+
<% end %>
|
11
|
+
</ul>
|
12
|
+
<% end %>
|
13
|
+
<% if item.empty? %>
|
14
|
+
<p>The list is empty.</p>
|
15
|
+
<% end %>
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
|
3
|
+
count = (ENV['COUNT'] || 5000).to_i
|
4
|
+
|
5
|
+
$benches = []
|
6
|
+
def bench(name, &block)
|
7
|
+
$benches.push([name, block])
|
8
|
+
end
|
9
|
+
|
10
|
+
at_exit do
|
11
|
+
Benchmark.bmbm do |x|
|
12
|
+
$benches.each do |name, block|
|
13
|
+
x.report name.to_s do
|
14
|
+
count.times do
|
15
|
+
block.call
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/benchmarks/speed.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.dirname(__FILE__)
|
4
|
+
require 'helper'
|
5
|
+
|
6
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../examples'
|
7
|
+
require 'complex_view'
|
8
|
+
|
9
|
+
## erb
|
10
|
+
template = File.read(File.dirname(__FILE__) + '/complex.erb')
|
11
|
+
|
12
|
+
unless ENV['NOERB']
|
13
|
+
bench 'ERB w/o caching' do
|
14
|
+
ERB.new(template).result(ComplexView.new.send(:binding))
|
15
|
+
end
|
16
|
+
|
17
|
+
erb = ERB.new(template)
|
18
|
+
bench 'ERB w/ caching' do
|
19
|
+
erb.result(ComplexView.new.send(:binding))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
## mustache
|
24
|
+
tpl = ComplexView.new
|
25
|
+
tpl.template
|
26
|
+
|
27
|
+
tpl[:header] = 'Chris'
|
28
|
+
tpl[:empty] = false
|
29
|
+
tpl[:list] = true
|
30
|
+
|
31
|
+
items = []
|
32
|
+
items << { :name => 'red', :current => true, :url => '#Red' }
|
33
|
+
items << { :name => 'green', :current => false, :url => '#Green' }
|
34
|
+
items << { :name => 'blue', :current => false, :url => '#Blue' }
|
35
|
+
|
36
|
+
tpl[:item] = items
|
37
|
+
|
38
|
+
bench '{ w/ caching' do
|
39
|
+
tpl.to_html
|
40
|
+
end
|
41
|
+
|
42
|
+
bench '{ w/o caching' do
|
43
|
+
tpl = ComplexView.new
|
44
|
+
tpl[:item] = items
|
45
|
+
tpl.to_html
|
46
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
<h1>{{title}}{{! just something interesting... or not... }}</h1>
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<h1>{{header}}</h1>
|
2
|
+
{{#list}}
|
3
|
+
<ul>
|
4
|
+
{{#item}}
|
5
|
+
{{#current}}
|
6
|
+
<li><strong>{{name}}</strong></li>
|
7
|
+
{{/current}}
|
8
|
+
{{#link}}
|
9
|
+
<li><a href="{{url}}">{{name}}</a></li>
|
10
|
+
{{/link}}
|
11
|
+
{{/item}}
|
12
|
+
</ul>
|
13
|
+
{{/list}}
|
14
|
+
{{#empty}}
|
15
|
+
<p>The list is empty.</p>
|
16
|
+
{{/empty}}
|
@@ -0,0 +1,34 @@
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
|
2
|
+
require 'mustache'
|
3
|
+
|
4
|
+
class ComplexView < Mustache
|
5
|
+
self.path = File.dirname(__FILE__)
|
6
|
+
|
7
|
+
def header
|
8
|
+
"Colors"
|
9
|
+
end
|
10
|
+
|
11
|
+
def item
|
12
|
+
items = []
|
13
|
+
items << { :name => 'red', :current => true, :url => '#Red' }
|
14
|
+
items << { :name => 'green', :current => false, :url => '#Green' }
|
15
|
+
items << { :name => 'blue', :current => false, :url => '#Blue' }
|
16
|
+
items
|
17
|
+
end
|
18
|
+
|
19
|
+
def link
|
20
|
+
not context[:current]
|
21
|
+
end
|
22
|
+
|
23
|
+
def list
|
24
|
+
not item.empty?
|
25
|
+
end
|
26
|
+
|
27
|
+
def empty
|
28
|
+
item.empty?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
if $0 == __FILE__
|
33
|
+
puts ComplexView.to_html
|
34
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
<h1>{{title}}</h1>
|
data/examples/escaped.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Again, {{title}}!
|
data/examples/simple.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
|
2
|
+
require 'mustache'
|
3
|
+
|
4
|
+
class Simple < Mustache
|
5
|
+
self.path = File.dirname(__FILE__)
|
6
|
+
|
7
|
+
def name
|
8
|
+
"Chris"
|
9
|
+
end
|
10
|
+
|
11
|
+
def value
|
12
|
+
10_000
|
13
|
+
end
|
14
|
+
|
15
|
+
def taxed_value
|
16
|
+
value - (value * 0.4)
|
17
|
+
end
|
18
|
+
|
19
|
+
def in_ca
|
20
|
+
true
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
if $0 == __FILE__
|
25
|
+
puts Simple.to_html
|
26
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
<h1>{{{title}}}</h1>
|
@@ -0,0 +1,18 @@
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
|
2
|
+
require 'mustache'
|
3
|
+
|
4
|
+
class ViewPartial < Mustache
|
5
|
+
self.path = File.dirname(__FILE__)
|
6
|
+
|
7
|
+
def greeting
|
8
|
+
"Welcome"
|
9
|
+
end
|
10
|
+
|
11
|
+
def farewell
|
12
|
+
"Fair enough, right?"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
if $0 == __FILE__
|
17
|
+
puts ViewPartial.to_html
|
18
|
+
end
|
data/lib/mustache.rb
ADDED
@@ -0,0 +1,218 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
class Mustache
|
4
|
+
# A Template is a compiled version of a Mustache template.
|
5
|
+
class Template
|
6
|
+
def initialize(source, template_path)
|
7
|
+
@source = source
|
8
|
+
@template_path = template_path
|
9
|
+
@tmpid = 0
|
10
|
+
end
|
11
|
+
|
12
|
+
def render(context)
|
13
|
+
class << self; self; end.class_eval <<-EOF, __FILE__, __LINE__ - 1
|
14
|
+
def render(ctx)
|
15
|
+
#{compile}
|
16
|
+
end
|
17
|
+
EOF
|
18
|
+
render(context)
|
19
|
+
end
|
20
|
+
|
21
|
+
def compile(src = @source)
|
22
|
+
"\"#{compile_sections(src)}\""
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
# {{#sections}}okay{{/sections}}
|
27
|
+
#
|
28
|
+
# Sections can return true, false, or an enumerable.
|
29
|
+
# If true, the section is displayed.
|
30
|
+
# If false, the section is not displayed.
|
31
|
+
# If enumerable, the return value is iterated over (a for loop).
|
32
|
+
def compile_sections(src)
|
33
|
+
res = ""
|
34
|
+
while src =~ /^\s*\{\{\#(.+)\}\}\n*(.+)^\s*\{\{\/\1\}\}\n*/m
|
35
|
+
res << compile_tags($`)
|
36
|
+
name = $1.strip.to_sym.inspect
|
37
|
+
code = compile($2)
|
38
|
+
ctxtmp = "ctx#{tmpid}"
|
39
|
+
res << ev("(v = ctx[#{name}]) ? v.respond_to?(:each) ? "\
|
40
|
+
"(#{ctxtmp}=ctx.dup; r=v.map{|h|ctx.merge!(h);#{code}}.join; "\
|
41
|
+
"ctx.replace(#{ctxtmp});r) : #{code} : ''")
|
42
|
+
src = $'
|
43
|
+
end
|
44
|
+
res << compile_tags(src)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Find and replace all non-section tags.
|
48
|
+
# In particular we look for four types of tags:
|
49
|
+
# 1. Escaped variable tags - {{var}}
|
50
|
+
# 2. Unescaped variable tags - {{{var}}}
|
51
|
+
# 3. Comment variable tags - {{! comment}
|
52
|
+
# 4. Partial tags - {{< partial_name }}
|
53
|
+
def compile_tags(src)
|
54
|
+
res = ""
|
55
|
+
while src =~ /\{\{(!|<|\{)?([^\/#]+?)\1?\}\}+/
|
56
|
+
res << str($`)
|
57
|
+
case $1
|
58
|
+
when '!'
|
59
|
+
# ignore comments
|
60
|
+
when '<'
|
61
|
+
res << compile_partial($2.strip)
|
62
|
+
when '{'
|
63
|
+
res << utag($2.strip)
|
64
|
+
else
|
65
|
+
res << etag($2.strip)
|
66
|
+
end
|
67
|
+
src = $'
|
68
|
+
end
|
69
|
+
res << str(src)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Partials are basically a way to render views from inside other views.
|
73
|
+
def compile_partial(name)
|
74
|
+
klass = Mustache.classify(name)
|
75
|
+
if Object.const_defined?(klass)
|
76
|
+
ev("#{klass}.to_html")
|
77
|
+
else
|
78
|
+
src = File.read(@template_path + '/' + name + '.html')
|
79
|
+
compile(src)[1..-2]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Generate a temporary id.
|
84
|
+
def tmpid
|
85
|
+
@tmpid += 1
|
86
|
+
end
|
87
|
+
|
88
|
+
def str(s)
|
89
|
+
s.inspect[1..-2]
|
90
|
+
end
|
91
|
+
|
92
|
+
def etag(s)
|
93
|
+
ev("Mustache.escape(ctx[#{s.strip.to_sym.inspect}])")
|
94
|
+
end
|
95
|
+
|
96
|
+
def utag(s)
|
97
|
+
ev("ctx[#{s.strip.to_sym.inspect}]")
|
98
|
+
end
|
99
|
+
|
100
|
+
def ev(s)
|
101
|
+
"#\{#{s}}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class Context < Hash
|
106
|
+
def initialize(mustache)
|
107
|
+
@mustache = mustache
|
108
|
+
super()
|
109
|
+
end
|
110
|
+
|
111
|
+
def [](name)
|
112
|
+
if has_key?(name)
|
113
|
+
super
|
114
|
+
elsif @mustache.respond_to?(name)
|
115
|
+
@mustache.send(name)
|
116
|
+
else
|
117
|
+
raise "Can't find #{name} in #{inspect}"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Helper method for quickly instantiating and rendering a view.
|
123
|
+
def self.to_html
|
124
|
+
new.to_html
|
125
|
+
end
|
126
|
+
|
127
|
+
# The path informs your Mustache subclass where to look for its
|
128
|
+
# corresponding template.
|
129
|
+
def self.path=(path)
|
130
|
+
@path = File.expand_path(path)
|
131
|
+
end
|
132
|
+
|
133
|
+
def self.path
|
134
|
+
@path || '.'
|
135
|
+
end
|
136
|
+
|
137
|
+
# Templates are self.class.name.underscore + '.html' -- a class of
|
138
|
+
# Dashboard would have a template (relative to the path) of
|
139
|
+
# dashboard.html
|
140
|
+
def self.template_file
|
141
|
+
@template_file ||= path + '/' + underscore(to_s) + '.html'
|
142
|
+
end
|
143
|
+
|
144
|
+
def self.template_file=(template_file)
|
145
|
+
@template_file = template_file
|
146
|
+
end
|
147
|
+
|
148
|
+
def self.template
|
149
|
+
@template ||= templateify(File.read(template_file))
|
150
|
+
end
|
151
|
+
|
152
|
+
# template_partial => TemplatePartial
|
153
|
+
def self.classify(underscored)
|
154
|
+
underscored.split(/[-_]/).map { |part| part[0] = part[0].chr.upcase; part }.join
|
155
|
+
end
|
156
|
+
|
157
|
+
# TemplatePartial => template_partial
|
158
|
+
def self.underscore(classified)
|
159
|
+
string = classified.dup.split('::').last
|
160
|
+
string[0] = string[0].chr.downcase
|
161
|
+
string.gsub(/[A-Z]/) { |s| "_#{s.downcase}"}
|
162
|
+
end
|
163
|
+
|
164
|
+
# Escape HTML.
|
165
|
+
def self.escape(string)
|
166
|
+
CGI.escapeHTML(string.to_s)
|
167
|
+
end
|
168
|
+
|
169
|
+
def self.templateify(obj)
|
170
|
+
obj.is_a?(Template) ? obj : Template.new(obj.to_s, path)
|
171
|
+
end
|
172
|
+
|
173
|
+
# The template itself. You can override this if you'd like.
|
174
|
+
def template
|
175
|
+
@template ||= self.class.template
|
176
|
+
end
|
177
|
+
|
178
|
+
def template=(template)
|
179
|
+
@template = self.class.templateify(template)
|
180
|
+
end
|
181
|
+
|
182
|
+
# Pass a block to `debug` with your debug putses. Set the `DEBUG`
|
183
|
+
# env variable when you want to run those blocks.
|
184
|
+
#
|
185
|
+
# e.g.
|
186
|
+
# debug { puts @context.inspect }
|
187
|
+
def debug
|
188
|
+
yield if ENV['DEBUG']
|
189
|
+
end
|
190
|
+
|
191
|
+
# A helper method which gives access to the context at a given time.
|
192
|
+
# Kind of a hack for now, but useful when you're in an iterating section
|
193
|
+
# and want access to the hash currently being iterated over.
|
194
|
+
def context
|
195
|
+
@context ||= Context.new(self)
|
196
|
+
end
|
197
|
+
|
198
|
+
# Context accessors
|
199
|
+
def [](key)
|
200
|
+
context[key.to_sym]
|
201
|
+
end
|
202
|
+
|
203
|
+
def []=(key, value)
|
204
|
+
context[key.to_sym] = value
|
205
|
+
end
|
206
|
+
|
207
|
+
# How we turn a view object into HTML. The main method, if you will.
|
208
|
+
def to_html
|
209
|
+
render template
|
210
|
+
end
|
211
|
+
|
212
|
+
# Parses our fancy pants template HTML and returns normal HTML with
|
213
|
+
# all special {{tags}} and {{#sections}}replaced{{/sections}}.
|
214
|
+
def render(html, ctx = {})
|
215
|
+
html = self.class.templateify(html)
|
216
|
+
html.render(context.update(ctx))
|
217
|
+
end
|
218
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
=begin
|
2
|
+
Support for Mustache in your Sinatra app.
|
3
|
+
|
4
|
+
require 'mustache/sinatra'
|
5
|
+
|
6
|
+
class App < Sinatra::Base
|
7
|
+
helpers Mustache::Sinatra
|
8
|
+
|
9
|
+
get '/stats' do
|
10
|
+
mustache :stats
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
If a `Views::Stats` class exists in the above example,
|
15
|
+
Mustache will try to instantiate and use it for the rendering.
|
16
|
+
|
17
|
+
If no `Views::Stats` class exists Mustache will render the template
|
18
|
+
file directly.
|
19
|
+
|
20
|
+
You can indeed use layouts with this library. Where you'd normally
|
21
|
+
<%= yield %> you instead {{yield}} - the body of the subview is
|
22
|
+
set to the `yield` variable and made available to you.
|
23
|
+
=end
|
24
|
+
require 'mustache'
|
25
|
+
|
26
|
+
class Mustache
|
27
|
+
module Sinatra
|
28
|
+
# Call this in your Sinatra routes.
|
29
|
+
def mustache(template, options={}, locals={})
|
30
|
+
render :mustache, template, options, locals
|
31
|
+
end
|
32
|
+
|
33
|
+
# This is called by Sinatra's `render` with the proper paths
|
34
|
+
# and, potentially, a block containing a sub-view
|
35
|
+
def render_mustache(template, data, options, locals, &block)
|
36
|
+
name = Mustache.classify(template.to_s)
|
37
|
+
|
38
|
+
if defined?(Views) && Views.const_defined?(name)
|
39
|
+
instance = Views.const_get(name).new
|
40
|
+
else
|
41
|
+
instance = Mustache.new
|
42
|
+
end
|
43
|
+
|
44
|
+
locals.each do |local, value|
|
45
|
+
instance[local] = value
|
46
|
+
end
|
47
|
+
|
48
|
+
# If we're paseed a block it's a subview. Sticking it in yield
|
49
|
+
# lets us use {{yield}} in layout.html to render the actual page.
|
50
|
+
instance[:yield] = block.call if block
|
51
|
+
|
52
|
+
instance.template = data
|
53
|
+
instance.to_html
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../examples'
|
4
|
+
require 'simple'
|
5
|
+
require 'complex_view'
|
6
|
+
require 'view_partial'
|
7
|
+
require 'template_partial'
|
8
|
+
require 'escaped'
|
9
|
+
require 'unescaped'
|
10
|
+
require 'comments'
|
11
|
+
|
12
|
+
class MustacheTest < Test::Unit::TestCase
|
13
|
+
def test_complex_view
|
14
|
+
assert_equal <<-end_complex, ComplexView.to_html
|
15
|
+
<h1>Colors</h1>
|
16
|
+
<ul>
|
17
|
+
<li><strong>red</strong></li>
|
18
|
+
<li><a href="#Green">green</a></li>
|
19
|
+
<li><a href="#Blue">blue</a></li>
|
20
|
+
</ul>
|
21
|
+
end_complex
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_simple
|
25
|
+
assert_equal <<-end_simple, Simple.to_html
|
26
|
+
Hello Chris
|
27
|
+
You have just won $10000!
|
28
|
+
Well, $6000.0, after taxes.
|
29
|
+
end_simple
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_hash_assignment
|
33
|
+
view = Simple.new
|
34
|
+
view[:name] = 'Bob'
|
35
|
+
view[:value] = '4000'
|
36
|
+
view[:in_ca] = false
|
37
|
+
|
38
|
+
assert_equal <<-end_simple, view.to_html
|
39
|
+
Hello Bob
|
40
|
+
You have just won $4000!
|
41
|
+
end_simple
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_crazier_hash_assignment
|
45
|
+
view = Simple.new
|
46
|
+
view[:name] = 'Crazy'
|
47
|
+
view[:in_ca] = [
|
48
|
+
{ :taxed_value => 1 },
|
49
|
+
{ :taxed_value => 2 },
|
50
|
+
{ :taxed_value => 3 },
|
51
|
+
]
|
52
|
+
|
53
|
+
assert_equal <<-end_simple, view.to_html
|
54
|
+
Hello Crazy
|
55
|
+
You have just won $10000!
|
56
|
+
Well, $1, after taxes.
|
57
|
+
Well, $2, after taxes.
|
58
|
+
Well, $3, after taxes.
|
59
|
+
end_simple
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_fileless_templates
|
63
|
+
view = Simple.new
|
64
|
+
view.template = 'Hi {{person}}!'
|
65
|
+
view[:person] = 'mom'
|
66
|
+
|
67
|
+
assert_equal 'Hi mom!', view.to_html
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_view_partial
|
71
|
+
assert_equal <<-end_partial.strip, ViewPartial.to_html
|
72
|
+
<h1>Welcome</h1>
|
73
|
+
Hello Chris
|
74
|
+
You have just won $10000!
|
75
|
+
Well, $6000.0, after taxes.
|
76
|
+
|
77
|
+
<h3>Fair enough, right?</h3>
|
78
|
+
end_partial
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_template_partial
|
82
|
+
assert_equal <<-end_partial.strip, TemplatePartial.to_html
|
83
|
+
<h1>Welcome</h1>
|
84
|
+
Again, Welcome!
|
85
|
+
end_partial
|
86
|
+
end
|
87
|
+
|
88
|
+
def test_comments
|
89
|
+
assert_equal "<h1>A Comedy of Errors</h1>\n", Comments.to_html
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_escaped
|
93
|
+
assert_equal '<h1>Bear > Shark</h1>', Escaped.to_html
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_unescaped
|
97
|
+
assert_equal '<h1>Bear > Shark</h1>', Unescaped.to_html
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_classify
|
101
|
+
assert_equal 'TemplatePartial', Mustache.classify('template_partial')
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_underscore
|
105
|
+
assert_equal 'template_partial', Mustache.underscore('TemplatePartial')
|
106
|
+
end
|
107
|
+
|
108
|
+
def test_namespaced_underscore
|
109
|
+
assert_equal 'stat_stuff', Mustache.underscore('Views::StatStuff')
|
110
|
+
end
|
111
|
+
end
|
metadata
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mustache
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Chris Wanstrath
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-10-05 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Mustache is a framework-agnostic way to render logic-free views.
|
17
|
+
email: chris@ozmm.org
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- LICENSE
|
24
|
+
- README.md
|
25
|
+
files:
|
26
|
+
- .gitignore
|
27
|
+
- LICENSE
|
28
|
+
- README.md
|
29
|
+
- Rakefile
|
30
|
+
- benchmarks/complex.erb
|
31
|
+
- benchmarks/helper.rb
|
32
|
+
- benchmarks/simple.erb
|
33
|
+
- benchmarks/speed.rb
|
34
|
+
- examples/comments.html
|
35
|
+
- examples/comments.rb
|
36
|
+
- examples/complex_view.html
|
37
|
+
- examples/complex_view.rb
|
38
|
+
- examples/escaped.html
|
39
|
+
- examples/escaped.rb
|
40
|
+
- examples/inner_partial.html
|
41
|
+
- examples/simple.html
|
42
|
+
- examples/simple.rb
|
43
|
+
- examples/template_partial.html
|
44
|
+
- examples/template_partial.rb
|
45
|
+
- examples/unescaped.html
|
46
|
+
- examples/unescaped.rb
|
47
|
+
- examples/view_partial.html
|
48
|
+
- examples/view_partial.rb
|
49
|
+
- lib/mustache.rb
|
50
|
+
- lib/mustache/sinatra.rb
|
51
|
+
- test/mustache_test.rb
|
52
|
+
has_rdoc: true
|
53
|
+
homepage: http://github.com/defunkt/mustache
|
54
|
+
licenses: []
|
55
|
+
|
56
|
+
post_install_message:
|
57
|
+
rdoc_options:
|
58
|
+
- --charset=UTF-8
|
59
|
+
require_paths:
|
60
|
+
- lib
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: "0"
|
66
|
+
version:
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: "0"
|
72
|
+
version:
|
73
|
+
requirements: []
|
74
|
+
|
75
|
+
rubyforge_project:
|
76
|
+
rubygems_version: 1.3.5
|
77
|
+
signing_key:
|
78
|
+
specification_version: 3
|
79
|
+
summary: Mustache is a framework-agnostic way to render logic-free views.
|
80
|
+
test_files:
|
81
|
+
- test/mustache_test.rb
|
82
|
+
- examples/comments.rb
|
83
|
+
- examples/complex_view.rb
|
84
|
+
- examples/escaped.rb
|
85
|
+
- examples/simple.rb
|
86
|
+
- examples/template_partial.rb
|
87
|
+
- examples/unescaped.rb
|
88
|
+
- examples/view_partial.rb
|