mustache 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,5 @@
1
+ Hello <%= name %>
2
+ You have just won $<%= value %>!
3
+ <% if in_ca %>
4
+ Well, $<%= taxed_value %>, after taxes.
5
+ <% end %>
@@ -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,14 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'mustache'
3
+
4
+ class Comments < Mustache
5
+ self.path = File.dirname(__FILE__)
6
+
7
+ def title
8
+ "A Comedy of Errors"
9
+ end
10
+ end
11
+
12
+ if $0 == __FILE__
13
+ puts Comments.to_html
14
+ end
@@ -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>
@@ -0,0 +1,14 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'mustache'
3
+
4
+ class Escaped < Mustache
5
+ self.path = File.dirname(__FILE__)
6
+
7
+ def title
8
+ "Bear > Shark"
9
+ end
10
+ end
11
+
12
+ if $0 == __FILE__
13
+ puts Escaped.to_html
14
+ end
@@ -0,0 +1 @@
1
+ Again, {{title}}!
@@ -0,0 +1,5 @@
1
+ Hello {{name}}
2
+ You have just won ${{value}}!
3
+ {{#in_ca}}
4
+ Well, ${{ taxed_value }}, after taxes.
5
+ {{/in_ca}}
@@ -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,2 @@
1
+ <h1>{{title}}</h1>
2
+ {{<inner_partial}}
@@ -0,0 +1,14 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'mustache'
3
+
4
+ class TemplatePartial < Mustache
5
+ self.path = File.dirname(__FILE__)
6
+
7
+ def title
8
+ "Welcome"
9
+ end
10
+ end
11
+
12
+ if $0 == __FILE__
13
+ puts TemplatePartial.to_html
14
+ end
@@ -0,0 +1 @@
1
+ <h1>{{{title}}}</h1>
@@ -0,0 +1,14 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'mustache'
3
+
4
+ class Unescaped < Mustache
5
+ self.path = File.dirname(__FILE__)
6
+
7
+ def title
8
+ "Bear > Shark"
9
+ end
10
+ end
11
+
12
+ if $0 == __FILE__
13
+ puts Unescaped.to_html
14
+ end
@@ -0,0 +1,3 @@
1
+ <h1>{{greeting}}</h1>
2
+ {{<simple}}
3
+ <h3>{{farewell}}</h3>
@@ -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 &gt; 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