mustache 0.11.2 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -208,9 +208,9 @@ you want to use in all your views? No problem.
208
208
  This is just Ruby, after all.
209
209
 
210
210
  module ViewHelpers
211
- def gravatar(email, size = 30)
212
- gravatar_id = Digest::MD5.hexdigest(email.to_s.strip.downcase)
213
- gravatar_for_id(gravatar_id, size)
211
+ def gravatar
212
+ gravatar_id = Digest::MD5.hexdigest(self[:email].to_s.strip.downcase)
213
+ gravatar_for_id(gravatar_id)
214
214
  end
215
215
 
216
216
  def gravatar_for_id(gid, size = 30)
@@ -242,6 +242,10 @@ Then just include it:
242
242
  def in_ca
243
243
  true
244
244
  end
245
+
246
+ def users
247
+ User.all
248
+ end
245
249
  end
246
250
 
247
251
  Great, but what about that `@ssl` ivar in `gravatar_host`? There are
@@ -265,7 +269,13 @@ Now:
265
269
 
266
270
  Simple.new(request.ssl?).render
267
271
 
268
- Convoluted but you get the idea.
272
+ Finally, our template might look like this:
273
+
274
+ <ul>
275
+ {{# users}}
276
+ <li><img src="{{ gravatar }}"> {{ login }}</li>
277
+ {{/ users}}
278
+ </ul>
269
279
 
270
280
 
271
281
  Sinatra
data/lib/mustache.rb CHANGED
@@ -70,7 +70,10 @@ require 'mustache/context'
70
70
  # for files containing view classes when using the `view_class` method.
71
71
  #
72
72
  class Mustache
73
- # Helper method for quickly instantiating and rendering a view.
73
+ # Instantiates an instance of this class and calls `render` with
74
+ # the passed args.
75
+ #
76
+ # Returns a rendered String version of a template
74
77
  def self.render(*args)
75
78
  new.render(*args)
76
79
  end
@@ -288,7 +291,7 @@ class Mustache
288
291
  end
289
292
  end
290
293
 
291
- # Return the value of the configuration setting on the superclass, or return
294
+ # Return the value of the configuration setting on the superclass, or return
292
295
  # the default.
293
296
  #
294
297
  # attr_name - Symbol name of the attribute. It should match the instance variable.
@@ -341,8 +344,27 @@ class Mustache
341
344
 
342
345
  # Parses our fancy pants template file and returns normal file with
343
346
  # all special {{tags}} and {{#sections}}replaced{{/sections}}.
347
+ #
348
+ # data - A String template or a Hash context. If a Hash is given,
349
+ # we'll try to figure out the template from the class.
350
+ # ctx - A Hash context if `data` is a String template.
351
+ #
352
+ # Examples
353
+ #
354
+ # @view.render("Hi {{thing}}!", :thing => :world)
355
+ #
356
+ # View.template = "Hi {{thing}}!"
357
+ # @view = View.new
358
+ # @view.render(:thing => :world)
359
+ #
360
+ # Returns a rendered String version of a template
344
361
  def render(data = template, ctx = {})
345
- tpl = templateify(data)
362
+ if data.is_a? Hash
363
+ ctx = data
364
+ tpl = templateify(template)
365
+ else
366
+ tpl = templateify(data)
367
+ end
346
368
 
347
369
  return tpl.render(context) if ctx == {}
348
370
 
@@ -12,6 +12,8 @@ class Mustache
12
12
  # A Context represents the context which a Mustache template is
13
13
  # executed within. All Mustache tags reference keys in the Context.
14
14
  class Context
15
+ attr_accessor :frame, :key
16
+
15
17
  # Expect to be passed an instance of `Mustache`.
16
18
  def initialize(mustache)
17
19
  @stack = [mustache]
@@ -23,12 +25,15 @@ class Mustache
23
25
  # If the Mustache view handling the rendering (e.g. the view
24
26
  # representing your profile page or some other template) responds
25
27
  # to `partial`, we call it and render the result.
26
- def partial(name)
28
+ def partial(name, indentation = '')
27
29
  # Look for the first Mustache in the stack.
28
30
  mustache = mustache_in_stack
29
31
 
30
32
  # Call its `partial` method and render the result.
31
- mustache.render(mustache.partial(name), self)
33
+ result = mustache.render(mustache.partial(name), self)
34
+
35
+ # Indent the rendered partial by the given indentation.
36
+ result.gsub(/^/, indentation)
32
37
  end
33
38
 
34
39
  # Find the first Mustache in the stack. If we're being rendered
@@ -82,9 +87,12 @@ class Mustache
82
87
  # If no second parameter is passed (or raise_on_context_miss is
83
88
  # set to true), will raise a ContextMiss exception on miss.
84
89
  def fetch(name, default = :__raise)
90
+ @key = name
91
+
85
92
  @stack.each do |frame|
86
93
  # Prevent infinite recursion.
87
94
  next if frame == self
95
+ @frame = frame
88
96
 
89
97
  # Is this frame a hash?
90
98
  hash = frame.respond_to?(:has_key?)
@@ -92,12 +100,16 @@ class Mustache
92
100
  if hash && frame.has_key?(name)
93
101
  return frame[name]
94
102
  elsif hash && frame.has_key?(name.to_s)
103
+ @key = name.to_s
95
104
  return frame[name.to_s]
96
105
  elsif !hash && frame.respond_to?(name)
106
+ @frame = nil
97
107
  return frame.__send__(name)
98
108
  end
99
109
  end
100
110
 
111
+ @frame = @key = nil
112
+
101
113
  if default == :__raise || mustache_in_stack.raise_on_context_miss?
102
114
  raise ContextMiss.new("Can't find #{name} in #{@stack.inspect}")
103
115
  else
@@ -90,7 +90,7 @@ class Mustache
90
90
 
91
91
  # Callback fired when the compiler finds a section token. We're
92
92
  # passed the section name and the array of tokens.
93
- def on_section(name, content)
93
+ def on_section(name, content, raw)
94
94
  # Convert the tokenized content of this section into a Ruby
95
95
  # string we can use.
96
96
  code = compile(content)
@@ -102,9 +102,11 @@ class Mustache
102
102
  if v == true
103
103
  #{code}
104
104
  elsif v.is_a?(Proc)
105
- v.call(#{code})
105
+ Mustache::Template.new(v.call(#{raw.inspect}).to_s).render(ctx.dup)
106
106
  else
107
- v = [v] unless v.is_a?(Array) # shortcut when passed non-array
107
+ # Shortcut when passed non-array
108
+ v = [v] if v.respond_to?(:has_key?) || !v.respond_to?(:map)
109
+
108
110
  v.map { |h| ctx.push(h); r = #{code}; ctx.pop; r }.join
109
111
  end
110
112
  end
@@ -113,7 +115,7 @@ class Mustache
113
115
 
114
116
  # Fired when we find an inverted section. Just like `on_section`,
115
117
  # we're passed the inverted section name and the array of tokens.
116
- def on_inverted_section(name, content)
118
+ def on_inverted_section(name, content, raw)
117
119
  # Convert the tokenized content of this section into a Ruby
118
120
  # string we can use.
119
121
  code = compile(content)
@@ -131,18 +133,28 @@ class Mustache
131
133
  # Fired when the compiler finds a partial. We want to return code
132
134
  # which calls a partial at runtime instead of expanding and
133
135
  # including the partial's body to allow for recursive partials.
134
- def on_partial(name)
135
- ev("ctx.partial(#{name.to_sym.inspect})")
136
+ def on_partial(name, indentation)
137
+ ev("ctx.partial(#{name.to_sym.inspect}, #{indentation.inspect})")
136
138
  end
137
139
 
138
140
  # An unescaped tag.
139
141
  def on_utag(name)
140
- ev("ctx[#{name.to_sym.inspect}]")
142
+ ev(<<-compiled)
143
+ v = ctx[#{name.to_sym.inspect}]
144
+ v = Mustache::Template.new(v.call.to_s).render(ctx.dup) if v.is_a?(Proc)
145
+ ctx.frame[ctx.key] = v if ctx.frame.is_a?(Hash)
146
+ v.to_s
147
+ compiled
141
148
  end
142
149
 
143
150
  # An escaped tag.
144
151
  def on_etag(name)
145
- ev("CGI.escapeHTML(ctx[#{name.to_sym.inspect}].to_s)")
152
+ ev(<<-compiled)
153
+ v = ctx[#{name.to_sym.inspect}]
154
+ v = Mustache::Template.new(v.call.to_s).render(ctx.dup) if v.is_a?(Proc)
155
+ ctx.frame[ctx.key] = v if ctx.frame.is_a?(Hash)
156
+ CGI.escapeHTML(v.to_s)
157
+ compiled
146
158
  end
147
159
 
148
160
  # An interpolation-friendly version of a string, for use within a
@@ -29,7 +29,7 @@ class Mustache
29
29
  class SyntaxError < StandardError
30
30
  def initialize(message, position)
31
31
  @message = message
32
- @lineno, @column, @line = position
32
+ @lineno, @column, @line, _ = position
33
33
  @stripped_line = @line.strip
34
34
  @stripped_column = @column - (@line.size - @line.lstrip.size)
35
35
  end
@@ -44,8 +44,10 @@ EOF
44
44
  end
45
45
  end
46
46
 
47
- # After these types of tags, all whitespace will be skipped.
48
- SKIP_WHITESPACE = [ '#', '^', '/' ]
47
+ # After these types of tags, all whitespace until the end of the line will
48
+ # be skipped if they are the first (and only) non-whitespace content on
49
+ # the line.
50
+ SKIP_WHITESPACE = [ '#', '^', '/', '<', '>', '=', '!' ]
49
51
 
50
52
  # The content allowed in a tag name.
51
53
  ALLOWED_CONTENT = /(\w|[?!\/-])*/
@@ -104,7 +106,18 @@ EOF
104
106
  # Find {{mustaches}} and add them to the @result array.
105
107
  def scan_tags
106
108
  # Scan until we hit an opening delimiter.
107
- return unless @scanner.scan(regexp(otag))
109
+ start_of_line = @scanner.beginning_of_line?
110
+ pre_match_position = @scanner.pos
111
+
112
+ return unless x = @scanner.scan(/([ \t]*)?#{Regexp.escape(otag)}/)
113
+ padding = @scanner[1] || ''
114
+
115
+ # Don't touch the preceding whitespace unless we're matching the start
116
+ # of a new line.
117
+ unless start_of_line
118
+ @result << [:static, padding] unless padding.empty?
119
+ padding = ''
120
+ end
108
121
 
109
122
  # Since {{= rewrites ctag, we store the ctag which should be used
110
123
  # when parsing this specific tag.
@@ -138,7 +151,8 @@ EOF
138
151
  @result = block
139
152
  when '/'
140
153
  section, pos, result = @sections.pop
141
- @result = result
154
+ raw = @scanner.pre_match[pos[3]...pre_match_position] + padding
155
+ (@result = result).last << raw
142
156
 
143
157
  if section.nil?
144
158
  error "Closing unopened #{content.inspect}"
@@ -150,7 +164,7 @@ EOF
150
164
  when '='
151
165
  self.otag, self.ctag = content.split(' ', 2)
152
166
  when '>', '<'
153
- @result << [:mustache, :partial, content]
167
+ @result << [:mustache, :partial, content, padding]
154
168
  when '{', '&'
155
169
  # The closing } in unescaped tags is just a hack for
156
170
  # aesthetics.
@@ -170,24 +184,38 @@ EOF
170
184
  error "Unclosed tag"
171
185
  end
172
186
 
173
- # Skip whitespace following this tag if we need to.
174
- @scanner.skip(/\s+/) if SKIP_WHITESPACE.include?(type)
187
+ # If this tag was the only non-whitespace content on this line, strip
188
+ # the remaining whitespace. If not, but we've been hanging on to padding
189
+ # from the beginning of the line, re-insert the padding as static text.
190
+ if start_of_line
191
+ if SKIP_WHITESPACE.include?(type)
192
+ @scanner.skip(/[ \t]*\n/)
193
+ else
194
+ @result.insert(-2, [:static, padding]) unless padding.empty?
195
+ end
196
+ end
197
+
198
+ return unless @result == [:multi]
199
+
200
+ # Store off the current scanner position now that we've closed the tag
201
+ # and consumed any irrelevant whitespace.
202
+ @sections.last[1] << @scanner.pos unless @sections.empty?
175
203
  end
176
204
 
177
205
  # Try to find static text, e.g. raw HTML with no {{mustaches}}.
178
206
  def scan_text
179
- text = scan_until_exclusive(regexp(otag))
207
+ text = scan_until_exclusive(/(^[ \t]*)?#{Regexp.escape(otag)}/)
180
208
 
181
209
  if text.nil?
182
210
  # Couldn't find any otag, which means the rest is just static text.
183
211
  text = @scanner.rest
184
212
  # Mark as done.
185
- @scanner.clear
213
+ @scanner.terminate
186
214
  end
187
215
 
188
216
  text.force_encoding(@encoding) if @encoding
189
217
 
190
- @result << [:static, text]
218
+ @result << [:static, text] unless text.empty?
191
219
  end
192
220
 
193
221
  # Scans the string until the pattern is matched. Returns the substring
@@ -51,11 +51,6 @@ class Mustache
51
51
  options = settings.send(:mustache).merge(options)
52
52
  end
53
53
 
54
- # Find and cache the view class we want. This ensures the
55
- # compiled template is cached, too - no looking up and
56
- # compiling templates on each page load.
57
- klass = mustache_class(template, options)
58
-
59
54
  # If they aren't explicitly disabling layouts, try to find
60
55
  # one.
61
56
  if options[:layout] != false
@@ -76,12 +71,18 @@ class Mustache
76
71
  layout = layout.new
77
72
  end
78
73
 
79
- # Does the view subclass the layout? If so we'll use the
80
- # view to render the layout so you can override layout
81
- # methods in your view - tricky.
82
- view_subclasses_layout = klass < layout.class if layout
83
74
  end
84
75
 
76
+ # Find and cache the view class we want. This ensures the
77
+ # compiled template is cached, too - no looking up and
78
+ # compiling templates on each page load.
79
+ klass = mustache_class(template, options)
80
+
81
+ # Does the view subclass the layout? If so we'll use the
82
+ # view to render the layout so you can override layout
83
+ # methods in your view - tricky.
84
+ view_subclasses_layout = klass < layout.class if layout
85
+
85
86
  # Create a new instance for playing with.
86
87
  instance = klass.new
87
88
 
@@ -21,7 +21,6 @@ class Mustache
21
21
  # path, which it uses to find partials.
22
22
  def initialize(source)
23
23
  @source = source
24
- @tmpid = 0
25
24
  end
26
25
 
27
26
  # Renders the `@source` Mustache template using the given
@@ -47,7 +46,7 @@ class Mustache
47
46
  # Does the dirty work of transforming a Mustache template into an
48
47
  # interpolation-friendly Ruby string.
49
48
  def compile(src = @source)
50
- Generator.new.compile(tokens)
49
+ Generator.new.compile(tokens(src))
51
50
  end
52
51
  alias_method :to_s, :compile
53
52
 
@@ -1,3 +1,3 @@
1
1
  class Mustache
2
- Version = VERSION = '0.11.2'
2
+ Version = VERSION = '0.12.0'
3
3
  end
@@ -2,7 +2,7 @@
2
2
  <body>
3
3
  <ul>
4
4
  {{#top_nodes}}
5
- {{> node}}
5
+ {{> node}}
6
6
  {{/top_nodes}}
7
7
  </ul>
8
8
  </body>
@@ -1,3 +1,7 @@
1
- {{#cached}}
1
+ {{#rendered}}
2
2
  Hi {{name}}.
3
- {{/cached}}
3
+ {{/rendered}}
4
+
5
+ {{#not_rendered}}
6
+ Hi {{name}}.
7
+ {{/not_rendered}}
@@ -12,7 +12,7 @@ class Lambda < Mustache
12
12
  @cached = nil
13
13
  end
14
14
 
15
- def cached
15
+ def rendered
16
16
  lambda do |text|
17
17
  return @cached if @cached
18
18
 
@@ -20,6 +20,10 @@ class Lambda < Mustache
20
20
  @cached = render(text)
21
21
  end
22
22
  end
23
+
24
+ def not_rendered
25
+ lambda { |text| "{{= | =}}#{text}" }
26
+ end
23
27
  end
24
28
 
25
29
  if $0 == __FILE__
@@ -2,7 +2,7 @@
2
2
  {{contents}}
3
3
  <ul>
4
4
  {{#children}}
5
- {{> node}}
5
+ {{> node}}
6
6
  {{/children}}
7
7
  </ul>
8
- </li>
8
+ </li>
@@ -1,3 +1,4 @@
1
1
  <h1>{{greeting}}</h1>
2
2
  {{>simple}}
3
+
3
4
  <h3>{{farewell}}</h3>
@@ -1 +1 @@
1
- <h2>中文又来啦</h2>
1
+ <h2>中文又来啦</h2>
@@ -3,6 +3,17 @@ $LOAD_PATH.unshift File.dirname(__FILE__)
3
3
  require 'helper'
4
4
 
5
5
  class MustacheTest < Test::Unit::TestCase
6
+ def test_instance_render
7
+ klass = Class.new(Mustache)
8
+ klass.template = "Hi {{thing}}!"
9
+ assert_equal "Hi world!", klass.render(:thing => :world)
10
+ assert_equal "Nice.", klass.render("{{compliment}}.", :compliment => "Nice")
11
+ assert_equal <<-end_simple, Simple.new.render(:name => "yo", :in_ca => false)
12
+ Hello yo
13
+ You have just won $10000!
14
+ end_simple
15
+ end
16
+
6
17
  def test_passenger
7
18
  assert_equal <<-end_passenger, Passenger.to_text
8
19
  <VirtualHost *>
@@ -16,22 +27,24 @@ end_passenger
16
27
  def test_complex_view
17
28
  assert_equal <<-end_complex, ComplexView.render
18
29
  <h1>Colors</h1>
19
- <ul>
20
- <li><strong>red</strong></li>
21
- <li><a href="#Green">green</a></li>
22
- <li><a href="#Blue">blue</a></li>
23
- </ul>
30
+ <ul>
31
+ <li><strong>red</strong></li>
32
+ <li><a href="#Green">green</a></li>
33
+ <li><a href="#Blue">blue</a></li>
34
+ </ul>
35
+
24
36
  end_complex
25
37
  end
26
38
 
27
39
  def test_nested_objects
28
40
  assert_equal <<-end_complex, NestedObjects.render
29
41
  <h1>Colors</h1>
30
- <ul>
31
- <li><strong>red</strong></li>
32
- <li><a href="#Green">green</a></li>
33
- <li><a href="#Blue">blue</a></li>
34
- </ul>
42
+ <ul>
43
+ <li><strong>red</strong></li>
44
+ <li><a href="#Green">green</a></li>
45
+ <li><a href="#Blue">blue</a></li>
46
+ </ul>
47
+
35
48
  end_complex
36
49
  end
37
50
 
@@ -41,9 +54,7 @@ end_complex
41
54
  instance = Mustache.new
42
55
  instance.template = html
43
56
  instance[:no_flash] = true
44
- assert_equal <<-rendered.strip, instance.render
45
- <p class="flash-notice" style="display: none;">
46
- rendered
57
+ assert_equal %Q'<p class="flash-notice" style="display: none;">', instance.render
47
58
  end
48
59
 
49
60
  def test_two_line_sections
@@ -52,9 +63,7 @@ rendered
52
63
  instance = Mustache.new
53
64
  instance.template = html
54
65
  instance[:no_flash] = true
55
- assert_equal <<-rendered.strip, instance.render
56
- <p class="flash-notice" style="display: none;"\n>
57
- rendered
66
+ assert_equal %Q'<p class="flash-notice" style="display: none;"\n>', instance.render
58
67
  end
59
68
 
60
69
  def test_multi_line_sections_preserve_trailing_newline
@@ -67,7 +76,7 @@ Howday.
67
76
  template
68
77
 
69
78
  view[:something] = true
70
- assert_equal <<-rendered.strip, view.render.strip
79
+ assert_equal <<-rendered, view.render
71
80
  yay
72
81
  Howday.
73
82
  rendered
@@ -78,9 +87,7 @@ rendered
78
87
 
79
88
  instance = Mustache.new
80
89
  instance.template = html
81
- assert_equal <<-rendered.strip, instance.render
82
- <p class="flash-notice" style="display: none;">
83
- rendered
90
+ assert_equal %Q'<p class="flash-notice" style="display: none;">', instance.render
84
91
  end
85
92
 
86
93
  def test_simple
@@ -131,29 +138,26 @@ end_simple
131
138
 
132
139
  def test_delimiters
133
140
  assert_equal <<-end_template, Delimiters.render
134
-
135
141
  * It worked the first time.
136
-
137
142
  * And it worked the second time.
138
143
  * As well as the third.
139
-
140
144
  * Then, surprisingly, it worked the final time.
141
145
  end_template
142
146
  end
143
147
 
144
148
  def test_double_section
145
- assert_equal <<-end_section.strip, DoubleSection.render.strip
146
- * first
149
+ assert_equal <<-end_section, DoubleSection.render
150
+ * first
147
151
  * second
148
- * third
152
+ * third
149
153
  end_section
150
154
  end
151
155
 
152
156
  def test_inverted_section
153
- assert_equal <<-end_section.strip, InvertedSection.render.strip
154
- * first
157
+ assert_equal <<-end_section, InvertedSection.render
158
+ * first
155
159
  * second
156
- * third
160
+ * third
157
161
  end_section
158
162
  end
159
163
 
@@ -231,7 +235,7 @@ data
231
235
  end
232
236
  instance.template = '{{#show}} <li>{{die}}</li> {{/show}} yay'
233
237
 
234
- assert_equal "yay", instance.render
238
+ assert_equal " yay", instance.render
235
239
  end
236
240
 
237
241
  def test_reports_unclosed_sections
@@ -265,7 +269,7 @@ data
265
269
  instance[:list] = { :item => 1234 }
266
270
  instance.template = '{{#list}} <li>{{item}}</li> {{/list}}'
267
271
 
268
- assert_equal '<li>1234</li>', instance.render.strip
272
+ assert_equal ' <li>1234</li> ', instance.render
269
273
  end
270
274
 
271
275
  def test_enumerable_sections_accept_a_string_keyed_hash_as_a_context
@@ -273,14 +277,14 @@ data
273
277
  instance[:list] = { 'item' => 1234 }
274
278
  instance.template = '{{#list}} <li>{{item}}</li> {{/list}}'
275
279
 
276
- assert_equal '<li>1234</li>', instance.render.strip
280
+ assert_equal ' <li>1234</li> ', instance.render
277
281
  end
278
282
 
279
283
  def test_not_found_in_context_renders_empty_string
280
284
  instance = Mustache.new
281
285
  instance.template = '{{#list}} <li>{{item}}</li> {{/list}}'
282
286
 
283
- assert_equal '', instance.render.strip
287
+ assert_equal '', instance.render
284
288
  end
285
289
 
286
290
  def test_not_found_in_nested_context_renders_empty_string
@@ -288,7 +292,7 @@ data
288
292
  instance[:list] = { :item => 1234 }
289
293
  instance.template = '{{#list}} <li>{{prefix}}{{item}}</li> {{/list}}'
290
294
 
291
- assert_equal '<li>1234</li>', instance.render.strip
295
+ assert_equal ' <li>1234</li> ', instance.render
292
296
  end
293
297
 
294
298
  def test_not_found_in_context_raises_when_asked_to
@@ -297,7 +301,7 @@ data
297
301
  instance.template = '{{#list}} <li>{{item}}</li> {{/list}}'
298
302
 
299
303
  assert_raises Mustache::ContextMiss do
300
- instance.render.strip
304
+ instance.render
301
305
  end
302
306
  end
303
307
 
@@ -343,13 +347,13 @@ data
343
347
  view = Lambda.new
344
348
  view[:name] = 'Chris'
345
349
 
346
- assert_equal "Hi Chris.", view.render.chomp
350
+ assert_equal "Hi Chris.\n\nHi {{name}}.", view.render.chomp
347
351
  assert_equal 1, view.calls
348
352
 
349
- assert_equal "Hi Chris.", view.render.chomp
353
+ assert_equal "Hi Chris.\n\nHi {{name}}.", view.render.chomp
350
354
  assert_equal 1, view.calls
351
355
 
352
- assert_equal "Hi Chris.", view.render.chomp
356
+ assert_equal "Hi Chris.\n\nHi {{name}}.", view.render.chomp
353
357
  assert_equal 1, view.calls
354
358
  end
355
359
 
@@ -380,7 +384,7 @@ data
380
384
  {{#items}}
381
385
  start
382
386
  {{#items}}
383
- {{a}}
387
+ {{a}}
384
388
  {{/items}}
385
389
  end
386
390
  {{/items}}
@@ -414,14 +418,14 @@ expected
414
418
  end
415
419
 
416
420
  def test_id_with_nested_context
417
- html = %(<div>{{id}}</div>\n<div>{{# has_a? }}{{id}}{{/ has_a? }}</div>\n<div>{{# has_b? }}{{id}}{{/ has_b? }}</div>)
421
+ html = %(<div>{{id}}</div>\n<div>{{# has_a? }}{{id}}{{/ has_a? }}</div>\n<div>{{# has_b? }}{{id}}{{/ has_b? }}</div>\n)
418
422
 
419
423
  instance = Mustache.new
420
424
  instance.template = html
421
425
  instance[:id] = 3
422
426
  instance[:has_a?] = true
423
427
  instance[:has_b?] = true
424
- assert_equal <<-rendered.strip, instance.render
428
+ assert_equal <<-rendered, instance.render
425
429
  <div>3</div>
426
430
  <div>3</div>
427
431
  <div>3</div>
@@ -473,4 +477,64 @@ template
473
477
  assert_equal value, tmpl.send(attr)
474
478
  end
475
479
  end
480
+ def test_array_of_arrays
481
+ template = <<template
482
+ {{#items}}
483
+ start
484
+ {{#map}}
485
+ {{a}}
486
+ {{/map}}
487
+ end
488
+ {{/items}}
489
+ template
490
+
491
+ data = {
492
+ "items" => [
493
+ [ {"a" => 1}, {"a" => 2}, {"a" => 3} ],
494
+ [ {"a" => 4}, {"a" => 5}, {"a" => 6} ],
495
+ [ {"a" => 7}, {"a" => 8}, {"a" => 9} ]
496
+ ]
497
+ }
498
+
499
+ assert_equal <<expected, Mustache.render(template, data)
500
+ start
501
+ 1
502
+ 2
503
+ 3
504
+ end
505
+ start
506
+ 4
507
+ 5
508
+ 6
509
+ end
510
+ start
511
+ 7
512
+ 8
513
+ 9
514
+ end
515
+ expected
516
+ end
517
+ def test_indentation_again
518
+ template = <<template
519
+ SELECT
520
+ {{#cols}}
521
+ {{name}},
522
+ {{/cols}}
523
+ FROM
524
+ DUMMY1
525
+ template
526
+
527
+ view = Mustache.new
528
+ view[:cols] = [{:name => 'Name'}, {:name => 'Age'}, {:name => 'Weight'}]
529
+ view.template = template
530
+
531
+ assert_equal <<template, view.render
532
+ SELECT
533
+ Name,
534
+ Age,
535
+ Weight,
536
+ FROM
537
+ DUMMY1
538
+ template
539
+ end
476
540
  end
data/test/parser_test.rb CHANGED
@@ -32,22 +32,27 @@ EOF
32
32
  :section,
33
33
  "first",
34
34
  [:multi,
35
- [:static, "<li><strong>"],
35
+ [:static, " <li><strong>"],
36
36
  [:mustache, :etag, "name"],
37
- [:static, "</strong></li>\n"]]],
37
+ [:static, "</strong></li>\n"]],
38
+ %Q' <li><strong>{{name}}</strong></li>\n'],
38
39
  [:mustache,
39
40
  :section,
40
41
  "link",
41
42
  [:multi,
42
- [:static, "<li><a href=\""],
43
+ [:static, " <li><a href=\""],
43
44
  [:mustache, :etag, "url"],
44
45
  [:static, "\">"],
45
46
  [:mustache, :etag, "name"],
46
- [:static, "</a></li>\n"]]]]],
47
+ [:static, "</a></li>\n"]],
48
+ %Q' <li><a href="{{url}}">{{name}}</a></li>\n']],
49
+ %Q'{{#first}}\n <li><strong>{{name}}</strong></li>\n{{/first}}\n{{#link}}\n <li><a href="{{url}}">{{name}}</a></li>\n{{/link}}\n'],
50
+ [:static, "\n"],
47
51
  [:mustache,
48
52
  :section,
49
53
  "empty",
50
- [:multi, [:static, "<p>The list is empty.</p>\n"]]]]
54
+ [:multi, [:static, "<p>The list is empty.</p>\n"]],
55
+ %Q'<p>The list is empty.</p>\n']]
51
56
 
52
57
  assert_equal expected, tokens
53
58
  end
data/test/partial_test.rb CHANGED
@@ -99,36 +99,36 @@ end_partial
99
99
  <body>
100
100
  <ul>
101
101
  <li>
102
- 1
103
- <ul>
104
- <li>
105
- 2
106
- <ul>
107
- <li>
108
- 3
109
- <ul>
102
+ 1
103
+ <ul>
104
+ <li>
105
+ 2
106
+ <ul>
107
+ <li>
108
+ 3
109
+ <ul>
110
+ </ul>
111
+ </li>
112
+ </ul>
113
+ </li>
114
+ <li>
115
+ 4
116
+ <ul>
117
+ <li>
118
+ 5
119
+ <ul>
120
+ <li>
121
+ 6
122
+ <ul>
123
+ </ul>
124
+ </li>
125
+ </ul>
126
+ </li>
127
+ </ul>
128
+ </li>
129
+ </ul>
130
+ </li>
110
131
  </ul>
111
- </li>
112
- </ul>
113
- </li>
114
- <li>
115
- 4
116
- <ul>
117
- <li>
118
- 5
119
- <ul>
120
- <li>
121
- 6
122
- <ul>
123
- </ul>
124
- </li>
125
- </ul>
126
- </li>
127
- </ul>
128
- </li>
129
- </ul>
130
- </li>
131
- </ul>
132
132
  </body>
133
133
  </html>
134
134
  end_partial
data/test/spec_test.rb ADDED
@@ -0,0 +1,68 @@
1
+ require 'mustache'
2
+ require 'tmpdir'
3
+ require 'yaml'
4
+ require 'test/unit'
5
+
6
+ # Automatically process !code types into Proc objects
7
+ YAML::add_builtin_type('code') { |_, val| eval(val['ruby']) }
8
+
9
+ # A simple base class for Mustache specs.
10
+ # Creates a partials directory, then points a (dynamic) subclass of Mustache at
11
+ # that directory before each test; the partials directory is destroyed after
12
+ # each test is run.
13
+ class MustacheSpec < Test::Unit::TestCase
14
+ def setup
15
+ @partials = File.join(File.dirname(__FILE__), 'partials')
16
+ Dir.mkdir(@partials)
17
+
18
+ @Mustache = Class.new(Mustache)
19
+ @Mustache.template_path = @partials
20
+ end
21
+
22
+ def teardown
23
+ Dir[File.join(@partials, '*')].each { |file| File.delete(file) }
24
+ Dir.rmdir(@partials)
25
+ end
26
+
27
+ # Extracts the partials from the test, and dumps them into the partials
28
+ # directory for inclusion.
29
+ def setup_partials(test)
30
+ (test['partials'] || {}).each do |name, content|
31
+ File.open(File.join(@partials, "#{name}.mustache"), 'w') do |f|
32
+ f.print(content)
33
+ end
34
+ end
35
+ end
36
+
37
+ # Asserts equality between the rendered template and the expected value,
38
+ # printing additional context data on failure.
39
+ def assert_mustache_spec(test)
40
+ actual = @Mustache.render(test['template'], test['data'])
41
+
42
+ assert_equal test['expected'], actual, "" <<
43
+ "#{ test['desc'] }\n" <<
44
+ "Data: #{ test['data'].inspect }\n" <<
45
+ "Template: #{ test['template'].inspect }\n" <<
46
+ "Partials: #{ (test['partials'] || {}).inspect }\n"
47
+ end
48
+
49
+ def test_noop; assert(true); end
50
+ end
51
+
52
+ spec_files = File.join(File.dirname(__FILE__), '..', 'ext', 'spec', 'specs', '*.yml')
53
+ Dir[spec_files].each do |file|
54
+ spec = YAML.load_file(file)
55
+
56
+ Class.new(MustacheSpec) do
57
+ define_method :name do
58
+ File.basename(file).sub(/^./, &:upcase)
59
+ end
60
+
61
+ spec['tests'].each do |test|
62
+ define_method :"test - #{test['name']}" do
63
+ setup_partials(test)
64
+ assert_mustache_spec(test)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,20 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__)
2
+ require 'helper'
3
+
4
+ class TemplateTest < Test::Unit::TestCase
5
+ def test_compile
6
+ assert_equal %("foo"), Mustache::Template.new("foo").compile
7
+ end
8
+
9
+ def test_compile_with_source
10
+ assert_equal %("bar"), Mustache::Template.new("foo").compile("bar")
11
+ end
12
+
13
+ def test_token
14
+ assert_equal [:multi, [:static, "foo"]], Mustache::Template.new("foo").tokens
15
+ end
16
+
17
+ def test_token_with_source
18
+ assert_equal [:multi, [:static, "bar"]], Mustache::Template.new("foo").tokens("bar")
19
+ end
20
+ end
metadata CHANGED
@@ -1,12 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mustache
3
3
  version: !ruby/object:Gem::Version
4
+ hash: 47
4
5
  prerelease: false
5
6
  segments:
6
7
  - 0
7
- - 11
8
- - 2
9
- version: 0.11.2
8
+ - 12
9
+ - 0
10
+ version: 0.12.0
10
11
  platform: ruby
11
12
  authors:
12
13
  - Chris Wanstrath
@@ -14,7 +15,7 @@ autorequire:
14
15
  bindir: bin
15
16
  cert_chain: []
16
17
 
17
- date: 2010-05-13 00:00:00 -07:00
18
+ date: 2010-12-10 00:00:00 -08:00
18
19
  default_executable:
19
20
  dependencies: []
20
21
 
@@ -102,6 +103,8 @@ files:
102
103
  - test/mustache_test.rb
103
104
  - test/parser_test.rb
104
105
  - test/partial_test.rb
106
+ - test/spec_test.rb
107
+ - test/template_test.rb
105
108
  has_rdoc: true
106
109
  homepage: http://github.com/defunkt/mustache
107
110
  licenses: []
@@ -112,23 +115,27 @@ rdoc_options: []
112
115
  require_paths:
113
116
  - lib
114
117
  required_ruby_version: !ruby/object:Gem::Requirement
118
+ none: false
115
119
  requirements:
116
120
  - - ">="
117
121
  - !ruby/object:Gem::Version
122
+ hash: 3
118
123
  segments:
119
124
  - 0
120
125
  version: "0"
121
126
  required_rubygems_version: !ruby/object:Gem::Requirement
127
+ none: false
122
128
  requirements:
123
129
  - - ">="
124
130
  - !ruby/object:Gem::Version
131
+ hash: 3
125
132
  segments:
126
133
  - 0
127
134
  version: "0"
128
135
  requirements: []
129
136
 
130
137
  rubyforge_project:
131
- rubygems_version: 1.3.6
138
+ rubygems_version: 1.3.7
132
139
  signing_key:
133
140
  specification_version: 3
134
141
  summary: Mustache is a framework-agnostic way to render logic-free views.