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 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