mustache 0.11.2 → 0.12.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/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.