mustache 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|