mustache 0.99.4 → 1.1.3

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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +183 -183
  3. data/Rakefile +6 -14
  4. data/bin/mustache +28 -14
  5. data/lib/mustache/context.rb +98 -46
  6. data/lib/mustache/context_miss.rb +55 -0
  7. data/lib/mustache/enumerable.rb +3 -0
  8. data/lib/mustache/generator.rb +63 -42
  9. data/lib/mustache/parser.rb +173 -65
  10. data/lib/mustache/settings.rb +84 -24
  11. data/lib/mustache/template.rb +74 -4
  12. data/lib/mustache/utils.rb +31 -0
  13. data/lib/mustache/version.rb +1 -1
  14. data/lib/mustache.rb +116 -96
  15. data/man/mustache.1 +25 -40
  16. data/man/mustache.1.html +90 -81
  17. data/man/mustache.1.ron +6 -6
  18. data/man/mustache.5 +338 -298
  19. data/man/mustache.5.html +400 -115
  20. data/man/mustache.5.ron +292 -32
  21. data/test/autoloading_test.rb +7 -3
  22. data/test/fixtures/comments.rb +0 -1
  23. data/test/fixtures/complex_view.rb +0 -1
  24. data/test/fixtures/crazy_recursive.rb +0 -1
  25. data/test/fixtures/delimiters.rb +0 -1
  26. data/test/fixtures/dot_notation.rb +0 -1
  27. data/test/fixtures/double_section.rb +0 -1
  28. data/test/fixtures/inverted_section.rb +0 -1
  29. data/test/fixtures/lambda.rb +0 -1
  30. data/test/fixtures/liberal.mustache +1 -0
  31. data/test/fixtures/liberal.rb +25 -0
  32. data/test/fixtures/method_missing.rb +0 -1
  33. data/test/fixtures/namespaced.rb +0 -1
  34. data/test/fixtures/nested_objects.rb +0 -1
  35. data/test/fixtures/override/passenger.conf +6 -0
  36. data/test/fixtures/partial_with_module.rb +0 -1
  37. data/test/fixtures/passenger.rb +0 -1
  38. data/test/fixtures/recursive.rb +0 -1
  39. data/test/fixtures/simple.rb +0 -1
  40. data/test/fixtures/simply_complicated.mustache +25 -0
  41. data/test/fixtures/template_partial.rb +0 -1
  42. data/test/fixtures/unescaped.rb +0 -1
  43. data/test/helper.rb +5 -2
  44. data/test/mustache_test.rb +210 -25
  45. data/test/parser_test.rb +90 -7
  46. data/test/partial_test.rb +16 -4
  47. data/test/path_test.rb +49 -0
  48. data/test/spec_test.rb +5 -5
  49. data/test/template_test.rb +35 -3
  50. metadata +37 -59
  51. data/lib/mustache/sinatra.rb +0 -186
  52. data/lib/rack/bug/panels/mustache_panel/mustache_extension.rb +0 -27
  53. data/lib/rack/bug/panels/mustache_panel/view.mustache +0 -46
  54. data/lib/rack/bug/panels/mustache_panel.rb +0 -81
@@ -44,44 +44,84 @@ EOF
44
44
  end
45
45
  end
46
46
 
47
+ # The sigil types which are valid after an opening `{{`
48
+ VALID_TYPES = [ '#', '^', '/', '=', '!', '<', '>', '&', '{' ].map(&:freeze)
49
+
50
+ def self.valid_types
51
+ @valid_types ||= Regexp.new(VALID_TYPES.map { |t| Regexp.escape(t) }.join('|') )
52
+ end
53
+
54
+ # Add a supported sigil type (with optional aliases) to the Parser.
55
+ #
56
+ # Requires a block, which will be sent the following parameters:
57
+ #
58
+ # * content - The raw content of the tag
59
+ # * fetch- A mustache context fetch expression for the content
60
+ # * padding - Indentation whitespace from the currently-parsed line
61
+ # * pre_match_position - Location of the scanner before a match was made
62
+ #
63
+ # The provided block will be evaluated against the current instance of
64
+ # Parser, and may append to the Parser's @result as needed.
65
+ def self.add_type(*types, &block)
66
+ types = types.map(&:to_s)
67
+ type, *aliases = types
68
+ method_name = "scan_tag_#{type}".to_sym
69
+ define_method(method_name, &block)
70
+ aliases.each { |a| alias_method "scan_tag_#{a}", method_name }
71
+ types.each { |t| VALID_TYPES << t unless VALID_TYPES.include?(t) }
72
+ @valid_types = nil
73
+ end
74
+
47
75
  # After these types of tags, all whitespace until the end of the line will
48
76
  # be skipped if they are the first (and only) non-whitespace content on
49
77
  # the line.
50
- SKIP_WHITESPACE = [ '#', '^', '/', '<', '>', '=', '!' ]
78
+ SKIP_WHITESPACE = [ '#', '^', '/', '<', '>', '=', '!' ].map(&:freeze)
51
79
 
52
80
  # The content allowed in a tag name.
53
- ALLOWED_CONTENT = /(\w|[?!\/.-])*/
81
+ ALLOWED_CONTENT = /(\w|[?!\/.@-])*/
54
82
 
55
83
  # These types of tags allow any content,
56
84
  # the rest only allow ALLOWED_CONTENT.
57
- ANY_CONTENT = [ '!', '=' ]
85
+ ANY_CONTENT = [ '!', '=' ].map(&:freeze)
58
86
 
59
- attr_reader :scanner, :result
60
- attr_writer :otag, :ctag
87
+ attr_reader :otag, :ctag
61
88
 
62
89
  # Accepts an options hash which does nothing but may be used in
63
90
  # the future.
64
91
  def initialize(options = {})
65
- @options = {}
92
+ @options = options
93
+ @option_inline_partials_at_compile_time = options[:inline_partials_at_compile_time]
94
+ if @option_inline_partials_at_compile_time
95
+ @partial_resolver = options[:partial_resolver]
96
+ raise ArgumentError.new "Missing or invalid partial_resolver" unless @partial_resolver.respond_to? :call
97
+ end
98
+
99
+ # Initialize default tags
100
+ self.otag ||= '{{'
101
+ self.ctag ||= '}}'
66
102
  end
67
103
 
68
104
  # The opening tag delimiter. This may be changed at runtime.
69
- def otag
70
- @otag ||= '{{'
105
+ def otag=(value)
106
+ regex = regexp value
107
+ @otag_regex = /([ \t]*)?#{regex}/
108
+ @otag_not_regex = /(^[ \t]*)?#{regex}/
109
+ @otag = value
71
110
  end
72
111
 
73
112
  # The closing tag delimiter. This too may be changed at runtime.
74
- def ctag
75
- @ctag ||= '}}'
113
+ def ctag=(value)
114
+ @ctag_regex = regexp value
115
+ @ctag = value
76
116
  end
77
117
 
78
118
  # Given a string template, returns an array of tokens.
79
119
  def compile(template)
120
+ @encoding = nil
121
+
80
122
  if template.respond_to?(:encoding)
81
123
  @encoding = template.encoding
82
124
  template = template.dup.force_encoding("BINARY")
83
- else
84
- @encoding = nil
85
125
  end
86
126
 
87
127
  # Keeps information about opened sections.
@@ -94,15 +134,36 @@ EOF
94
134
  scan_tags || scan_text
95
135
  end
96
136
 
97
- if !@sections.empty?
137
+ unless @sections.empty?
98
138
  # We have parsed the whole file, but there's still opened sections.
99
- type, pos, result = @sections.pop
139
+ type, pos, _ = @sections.pop
100
140
  error "Unclosed section #{type.inspect}", pos
101
141
  end
102
142
 
103
143
  @result
104
144
  end
105
145
 
146
+
147
+ private
148
+
149
+
150
+ def content_tags type, current_ctag_regex
151
+ if ANY_CONTENT.include?(type)
152
+ r = /\s*#{regexp(type)}?#{current_ctag_regex}/
153
+ scan_until_exclusive(r)
154
+ else
155
+ @scanner.scan(ALLOWED_CONTENT)
156
+ end
157
+ end
158
+
159
+ def dispatch_based_on_type type, content, fetch, padding, pre_match_position
160
+ send("scan_tag_#{type}", content, fetch, padding, pre_match_position)
161
+ end
162
+
163
+ def find_closing_tag scanner, current_ctag_regex
164
+ error "Unclosed tag" unless scanner.scan(current_ctag_regex)
165
+ end
166
+
106
167
  # Find {{mustaches}} and add them to the @result array.
107
168
  def scan_tags
108
169
  # Scan until we hit an opening delimiter.
@@ -110,7 +171,7 @@ EOF
110
171
  pre_match_position = @scanner.pos
111
172
  last_index = @result.length
112
173
 
113
- return unless x = @scanner.scan(/([ \t]*)?#{Regexp.escape(otag)}/)
174
+ return unless @scanner.scan @otag_regex
114
175
  padding = @scanner[1] || ''
115
176
 
116
177
  # Don't touch the preceding whitespace unless we're matching the start
@@ -123,18 +184,13 @@ EOF
123
184
 
124
185
  # Since {{= rewrites ctag, we store the ctag which should be used
125
186
  # when parsing this specific tag.
126
- current_ctag = self.ctag
127
- type = @scanner.scan(/#|\^|\/|=|!|<|>|&|\{/)
187
+ current_ctag_regex = @ctag_regex
188
+ type = @scanner.scan(self.class.valid_types)
128
189
  @scanner.skip(/\s*/)
129
190
 
130
191
  # ANY_CONTENT tags allow any character inside of them, while
131
192
  # other tags (such as variables) are more strict.
132
- if ANY_CONTENT.include?(type)
133
- r = /\s*#{regexp(type)}?#{regexp(current_ctag)}/
134
- content = scan_until_exclusive(r)
135
- else
136
- content = @scanner.scan(ALLOWED_CONTENT)
137
- end
193
+ content = content_tags(type, current_ctag_regex)
138
194
 
139
195
  # We found {{ but we can't figure out what's going on inside.
140
196
  error "Illegal content in tag" if content.empty?
@@ -142,52 +198,18 @@ EOF
142
198
  fetch = [:mustache, :fetch, content.split('.')]
143
199
  prev = @result
144
200
 
145
- # Based on the sigil, do what needs to be done.
146
- case type
147
- when '#'
148
- block = [:multi]
149
- @result << [:mustache, :section, fetch, block]
150
- @sections << [content, position, @result]
151
- @result = block
152
- when '^'
153
- block = [:multi]
154
- @result << [:mustache, :inverted_section, fetch, block]
155
- @sections << [content, position, @result]
156
- @result = block
157
- when '/'
158
- section, pos, result = @sections.pop
159
- raw = @scanner.pre_match[pos[3]...pre_match_position] + padding
160
- (@result = result).last << raw << [self.otag, self.ctag]
161
-
162
- if section.nil?
163
- error "Closing unopened #{content.inspect}"
164
- elsif section != content
165
- error "Unclosed section #{section.inspect}", pos
166
- end
167
- when '!'
168
- # ignore comments
169
- when '='
170
- self.otag, self.ctag = content.split(' ', 2)
171
- when '>', '<'
172
- @result << [:mustache, :partial, content, padding]
173
- when '{', '&'
174
- # The closing } in unescaped tags is just a hack for
175
- # aesthetics.
176
- type = "}" if type == "{"
177
- @result << [:mustache, :utag, fetch]
178
- else
179
- @result << [:mustache, :etag, fetch]
180
- end
201
+ dispatch_based_on_type(type, content, fetch, padding, pre_match_position)
202
+
203
+ # The closing } in unescaped tags is just a hack for
204
+ # aesthetics.
205
+ type = "}" if type == "{"
181
206
 
182
207
  # Skip whitespace and any balancing sigils after the content
183
208
  # inside this tag.
184
209
  @scanner.skip(/\s+/)
185
210
  @scanner.skip(regexp(type)) if type
186
211
 
187
- # Try to find the closing tag.
188
- unless close = @scanner.scan(regexp(current_ctag))
189
- error "Unclosed tag"
190
- end
212
+ find_closing_tag(@scanner, current_ctag_regex)
191
213
 
192
214
  # If this tag was the only non-whitespace content on this line, strip
193
215
  # the remaining whitespace. If not, but we've been hanging on to padding
@@ -209,7 +231,7 @@ EOF
209
231
 
210
232
  # Try to find static text, e.g. raw HTML with no {{mustaches}}.
211
233
  def scan_text
212
- text = scan_until_exclusive(/(^[ \t]*)?#{Regexp.escape(otag)}/)
234
+ text = scan_until_exclusive @otag_not_regex
213
235
 
214
236
  if text.nil?
215
237
  # Couldn't find any otag, which means the rest is just static text.
@@ -234,6 +256,10 @@ EOF
234
256
  end
235
257
  end
236
258
 
259
+ def offset
260
+ position[0, 2]
261
+ end
262
+
237
263
  # Returns [lineno, column, line]
238
264
  def position
239
265
  # The rest of the current line
@@ -250,7 +276,7 @@ EOF
250
276
  # Used to quickly convert a string into a regular expression
251
277
  # usable by the string scanner.
252
278
  def regexp(thing)
253
- /#{Regexp.escape(thing)}/
279
+ Regexp.new Regexp.escape(thing) if thing
254
280
  end
255
281
 
256
282
  # Raises a SyntaxError. The message should be the name of the
@@ -259,5 +285,87 @@ EOF
259
285
  def error(message, pos = position)
260
286
  raise SyntaxError.new(message, pos)
261
287
  end
288
+
289
+
290
+ #
291
+ # Scan tags
292
+ #
293
+ # These methods are called in `scan_tags`. Because they contain nonstandard
294
+ # characters in their method names, they are aliased to
295
+ # better named methods.
296
+ #
297
+
298
+
299
+ # This function handles the cases where the scanned tag does not have
300
+ # a type.
301
+ def scan_tag_ content, fetch, padding, pre_match_position
302
+ @result << [:mustache, :etag, fetch, offset]
303
+ end
304
+
305
+
306
+ def scan_tag_block content, fetch, padding, pre_match_position
307
+ block = [:multi]
308
+ @result << [:mustache, :section, fetch, offset, block]
309
+ @sections << [content, position, @result]
310
+ @result = block
311
+ end
312
+ alias_method :'scan_tag_#', :scan_tag_block
313
+
314
+
315
+ def scan_tag_inverted content, fetch, padding, pre_match_position
316
+ block = [:multi]
317
+ @result << [:mustache, :inverted_section, fetch, offset, block]
318
+ @sections << [content, position, @result]
319
+ @result = block
320
+ end
321
+ alias_method :'scan_tag_^', :scan_tag_inverted
322
+
323
+
324
+ def scan_tag_close content, fetch, padding, pre_match_position
325
+ section, pos, result = @sections.pop
326
+ if section.nil?
327
+ error "Closing unopened #{content.inspect}"
328
+ end
329
+
330
+ raw = @scanner.pre_match[pos[3]...pre_match_position] + padding
331
+ (@result = result).last << raw << [self.otag, self.ctag]
332
+
333
+ if section != content
334
+ error "Unclosed section #{section.inspect}", pos
335
+ end
336
+ end
337
+ alias_method :'scan_tag_/', :scan_tag_close
338
+
339
+
340
+ def scan_tag_comment content, fetch, padding, pre_match_position
341
+ end
342
+ alias_method :'scan_tag_!', :scan_tag_comment
343
+
344
+
345
+ def scan_tag_delimiter content, fetch, padding, pre_match_position
346
+ self.otag, self.ctag = content.split(' ', 2)
347
+ end
348
+ alias_method :'scan_tag_=', :scan_tag_delimiter
349
+
350
+
351
+ def scan_tag_open_partial content, fetch, padding, pre_match_position
352
+ @result << if @option_inline_partials_at_compile_time
353
+ partial = @partial_resolver.call content
354
+ partial.gsub!(/^/, padding) unless padding.empty?
355
+ self.class.new(@options).compile partial
356
+ else
357
+ [:mustache, :partial, content, offset, padding]
358
+ end
359
+ end
360
+ alias_method :'scan_tag_<', :scan_tag_open_partial
361
+ alias_method :'scan_tag_>', :scan_tag_open_partial
362
+
363
+
364
+ def scan_tag_unescaped content, fetch, padding, pre_match_position
365
+ @result << [:mustache, :utag, fetch, offset]
366
+ end
367
+ alias_method :'scan_tag_{', :'scan_tag_unescaped'
368
+ alias_method :'scan_tag_&', :'scan_tag_unescaped'
369
+
262
370
  end
263
371
  end
@@ -2,22 +2,57 @@
2
2
  # view class, or a single Mustache instance.
3
3
  class Mustache
4
4
 
5
+ def initialize_settings
6
+ @template = nil
7
+ @template_path = nil
8
+ @template_extension = nil
9
+ @template_name = nil
10
+ @template_file = nil
11
+ @raise_on_context_miss = nil
12
+ @context_access_security_level = 2
13
+ end
14
+
15
+ def self.initialize_settings
16
+ @template = nil
17
+ @template_path = nil
18
+ @template_extension = nil
19
+ @template_name = nil
20
+ @template_file = nil
21
+ @raise_on_context_miss = nil
22
+ @context_access_security_level = 2
23
+ end
24
+
25
+ initialize_settings
26
+
27
+ def self.inherited(subclass)
28
+ subclass.initialize_settings
29
+ end
30
+
5
31
  #
6
32
  # Template Path
7
33
  #
8
34
 
9
35
  # The template path informs your Mustache view where to look for its
10
- # corresponding template. By default it's the current directory (".")
36
+ # corresponding template. It is an array of paths, by default containing
37
+ # one entry: the current directory (".")
38
+ # When setting up from a string, use path delimiters to create a search path
39
+ # When the template_file is requested, a search is done to find the file in the
40
+ # path, this is then stored as the name.
11
41
  #
12
42
  # A class named Stat with a template_path of "app/templates" will look
13
43
  # for "app/templates/stat.mustache"
14
44
 
45
+ def self.setup_path path
46
+ path = path.split(File::PATH_SEPARATOR) if path.is_a? String
47
+ path.map{|p| File.expand_path(p)}
48
+ end
49
+
15
50
  def self.template_path
16
- @template_path ||= inheritable_config_for :template_path, '.'
51
+ @template_path ||= setup_path(inheritable_config_for(:template_path, '.'))
17
52
  end
18
53
 
19
54
  def self.template_path=(path)
20
- @template_path = File.expand_path(path)
55
+ @template_path = setup_path(path)
21
56
  @template = nil
22
57
  end
23
58
 
@@ -25,22 +60,17 @@ class Mustache
25
60
  @template_path ||= self.class.template_path
26
61
  end
27
62
 
63
+ alias_method :path, :template_path
64
+
28
65
  def template_path=(path)
29
- @template_path = File.expand_path(path)
66
+ @template_path = self.class.setup_path(path)
30
67
  @template = nil
31
68
  end
32
69
 
33
- # Alias for `template_path`
34
- def self.path
35
- template_path
70
+ class << self
71
+ alias_method :path, :template_path
72
+ alias_method :path=, :template_path=
36
73
  end
37
- alias_method :path, :template_path
38
-
39
- # Alias for `template_path`
40
- def self.path=(path)
41
- self.template_path = path
42
- end
43
- alias_method :path=, :template_path=
44
74
 
45
75
 
46
76
  #
@@ -105,26 +135,25 @@ class Mustache
105
135
  # Template File
106
136
  #
107
137
 
108
- # The template file is the absolute path of the file Mustache will
109
- # use as its template. By default it's ./class_name.mustache
138
+ # The template file is the absolute path of the file Mustache will use as its template.
139
+ # By default it's ./class_name.mustache
140
+ #
110
141
 
111
142
  def self.template_file
112
- @template_file || "#{path}/#{template_name}.#{template_extension}"
143
+ @template_file || path.map{|p| "#{p}/#{template_name}.#{template_extension}" }.find{|tf| File.readable? tf}
113
144
  end
114
145
 
115
- def self.template_file=(template_file)
116
- @template_file = template_file
146
+ def self.template_file=(tf)
147
+ @template_file = tf
117
148
  @template = nil
118
149
  end
119
150
 
120
- # The template file is the absolute path of the file Mustache will
121
- # use as its template. By default it's ./class_name.mustache
122
151
  def template_file
123
- @template_file || "#{path}/#{template_name}.#{template_extension}"
152
+ @template_file || path.map{|p| "#{p}/#{template_name}.#{template_extension}" }.find{|tf| File.readable? tf}
124
153
  end
125
154
 
126
- def template_file=(template_file)
127
- @template_file = template_file
155
+ def template_file=(tf)
156
+ @template_file = tf
128
157
  @template = nil
129
158
  end
130
159
 
@@ -191,6 +220,37 @@ class Mustache
191
220
  @raise_on_context_miss = boolean
192
221
  end
193
222
 
223
+ #
224
+ # Context access security level
225
+ #
226
+
227
+ # What object methods could be accessed from the template?
228
+ # This level determines the security restrictions for method access to protect against unsafe reflection
229
+ # Possible values are integers from 1 to 5:
230
+ #
231
+ # 1 - All methods allowed (use only with trusted templates)
232
+ # 2 - Reflection-related methods are blacklisted (default)
233
+ # 3 - All inherited or reflection-related methods are prohibited
234
+ # 4 - Access allowed only for Hash elements
235
+ # 5 - Access allowed only for Hash elements, only numbers, strings, hashes and functions allowed
236
+
237
+ def self.context_access_security_level
238
+ @context_access_security_level
239
+ end
240
+
241
+ def self.context_access_security_level=(integer)
242
+ @context_access_security_level = integer
243
+ end
244
+
245
+ # Instance level version of `Mustache.context_access_security_level`
246
+ def context_access_security_level
247
+ self.class.context_access_security_level || @context_access_security_level
248
+ end
249
+
250
+ def context_access_security_level=(integer)
251
+ @context_access_security_level = integer
252
+ end
253
+
194
254
 
195
255
  #
196
256
  # View Namespace
@@ -18,9 +18,10 @@ class Mustache
18
18
  attr_reader :source
19
19
 
20
20
  # Expects a Mustache template as a string along with a template
21
- # path, which it uses to find partials.
22
- def initialize(source)
21
+ # path, which it uses to find partials. Options may be passed.
22
+ def initialize(source, options = {})
23
23
  @source = source
24
+ @options = options
24
25
  end
25
26
 
26
27
  # Renders the `@source` Mustache template using the given
@@ -46,13 +47,82 @@ class Mustache
46
47
  # Does the dirty work of transforming a Mustache template into an
47
48
  # interpolation-friendly Ruby string.
48
49
  def compile(src = @source)
49
- Generator.new.compile(tokens(src))
50
+ Generator.new(@options).compile(tokens(src))
50
51
  end
51
52
  alias_method :to_s, :compile
52
53
 
53
54
  # Returns an array of tokens for a given template.
55
+ #
56
+ # @return [Array] Array of tokens.
57
+ #
54
58
  def tokens(src = @source)
55
- Parser.new.compile(src)
59
+ Parser.new(@options).compile(src)
60
+ end
61
+
62
+ # Returns an array of tags.
63
+ #
64
+ # Tags that belong to sections will be of the form `section1.tag`.
65
+ #
66
+ # @return [Array] Returns an array of tags.
67
+ #
68
+ def tags
69
+ Template.recursor(tokens, []) do |token, section|
70
+ if [:etag, :utag].include?(token[1])
71
+ [ new_token=nil, new_section=nil, result=((section + [token[2][2][0]]).join('.')), stop=true ]
72
+ elsif [:section, :inverted_section].include?(token[1])
73
+ [ new_token=token[4], new_section=(section + [token[2][2][0]]), result=nil, stop=false ]
74
+ else
75
+ [ new_token=token, new_section=section, result=nil, stop=false ]
76
+ end
77
+ end.flatten.reject(&:nil?).uniq
78
+ end
79
+
80
+ # Returns an array of sections.
81
+ #
82
+ # Sections that belong to other sections will be of the form `section1.childsection`
83
+ #
84
+ # @return [Array] Returns an array of section.
85
+ #
86
+ def sections
87
+ Template.recursor(tokens, []) do |token, section|
88
+ if [:section, :inverted_section].include?(token[1])
89
+ new_section=(section + [token[2][2][0]])
90
+ [ new_token=token[4], new_section, result=new_section.join('.'), stop=false ]
91
+ else
92
+ [ new_token=token, new_section=section, result=nil, stop=false ]
93
+ end
94
+ end.flatten.reject(&:nil?).uniq
95
+ end
96
+
97
+ # Returns an array of partials.
98
+ #
99
+ # Partials that belong to sections are included, but the section name is not preserved
100
+ #
101
+ # @return [Array] Returns an array of partials.
102
+ #
103
+ def partials
104
+ Template.recursor(tokens, []) do |token, section|
105
+ if token[1] == :partial
106
+ [ new_token=token, new_section=section, result=token[2], stop=true ]
107
+ else
108
+ [ new_token=token, new_section=section, result=nil, stop=false ]
109
+ end
110
+ end.flatten.reject(&:nil?).uniq
111
+ end
112
+
113
+
114
+ # Simple recursive iterator for tokens
115
+ def self.recursor(toks, section, &block)
116
+ toks.map do |token|
117
+ next unless token.is_a? Array
118
+
119
+ if token.first == :mustache
120
+ new_token, new_section, result, stop = yield(token, section)
121
+ [ result ] + ( stop ? [] : recursor(new_token, new_section, &block))
122
+ else
123
+ recursor(token, section, &block)
124
+ end
125
+ end
56
126
  end
57
127
  end
58
128
  end
@@ -0,0 +1,31 @@
1
+ class Mustache
2
+ module Utils
3
+ class String
4
+ def initialize string
5
+ @string = string
6
+ end
7
+
8
+ def classify
9
+ @string.split('/').map do |namespace|
10
+ namespace.split(/[-_]/).map do |part|
11
+ part[0] = part.chars.first.upcase
12
+ part
13
+ end.join
14
+ end.join('::')
15
+ end
16
+
17
+ def underscore(view_namespace)
18
+ @string
19
+ .dup
20
+ .split("#{view_namespace}::")
21
+ .last
22
+ .split('::')
23
+ .map do |part|
24
+ part[0] = part[0].downcase
25
+ part.gsub(/[A-Z]/) { |s| '_'.dup << s.downcase }
26
+ end
27
+ .join('/')
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,3 +1,3 @@
1
1
  class Mustache
2
- Version = VERSION = '0.99.4'
2
+ VERSION = '1.1.3'
3
3
  end