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 +14 -4
- data/lib/mustache.rb +25 -3
- data/lib/mustache/context.rb +14 -2
- data/lib/mustache/generator.rb +20 -8
- data/lib/mustache/parser.rb +39 -11
- data/lib/mustache/sinatra.rb +10 -9
- data/lib/mustache/template.rb +1 -2
- data/lib/mustache/version.rb +1 -1
- data/test/fixtures/crazy_recursive.mustache +1 -1
- data/test/fixtures/lambda.mustache +6 -2
- data/test/fixtures/lambda.rb +5 -1
- data/test/fixtures/node.mustache +2 -2
- data/test/fixtures/partial_with_module.mustache +1 -0
- data/test/fixtures/utf8_partial.mustache +1 -1
- data/test/mustache_test.rb +105 -41
- data/test/parser_test.rb +10 -5
- data/test/partial_test.rb +29 -29
- data/test/spec_test.rb +68 -0
- data/test/template_test.rb +20 -0
- metadata +12 -5
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
|
212
|
-
gravatar_id = Digest::MD5.hexdigest(email.to_s.strip.downcase)
|
213
|
-
gravatar_for_id(gravatar_id
|
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
|
-
|
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
|
-
#
|
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
|
-
|
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
|
|
data/lib/mustache/context.rb
CHANGED
@@ -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
|
data/lib/mustache/generator.rb
CHANGED
@@ -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(#{
|
105
|
+
Mustache::Template.new(v.call(#{raw.inspect}).to_s).render(ctx.dup)
|
106
106
|
else
|
107
|
-
|
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(
|
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(
|
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
|
data/lib/mustache/parser.rb
CHANGED
@@ -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
|
48
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
174
|
-
|
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(
|
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.
|
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
|
data/lib/mustache/sinatra.rb
CHANGED
@@ -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
|
|
data/lib/mustache/template.rb
CHANGED
@@ -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
|
|
data/lib/mustache/version.rb
CHANGED
data/test/fixtures/lambda.rb
CHANGED
@@ -12,7 +12,7 @@ class Lambda < Mustache
|
|
12
12
|
@cached = nil
|
13
13
|
end
|
14
14
|
|
15
|
-
def
|
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__
|
data/test/fixtures/node.mustache
CHANGED
@@ -1 +1 @@
|
|
1
|
-
<h2>中文又来啦</h2>
|
1
|
+
<h2>中文又来啦</h2>
|
data/test/mustache_test.rb
CHANGED
@@ -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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
-
|
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
|
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
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
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
|
-
-
|
8
|
-
-
|
9
|
-
version: 0.
|
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-
|
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.
|
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.
|