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.
- checksums.yaml +7 -0
- data/README.md +183 -183
- data/Rakefile +6 -14
- data/bin/mustache +28 -14
- data/lib/mustache/context.rb +98 -46
- data/lib/mustache/context_miss.rb +55 -0
- data/lib/mustache/enumerable.rb +3 -0
- data/lib/mustache/generator.rb +63 -42
- data/lib/mustache/parser.rb +173 -65
- data/lib/mustache/settings.rb +84 -24
- data/lib/mustache/template.rb +74 -4
- data/lib/mustache/utils.rb +31 -0
- data/lib/mustache/version.rb +1 -1
- data/lib/mustache.rb +116 -96
- data/man/mustache.1 +25 -40
- data/man/mustache.1.html +90 -81
- data/man/mustache.1.ron +6 -6
- data/man/mustache.5 +338 -298
- data/man/mustache.5.html +400 -115
- data/man/mustache.5.ron +292 -32
- data/test/autoloading_test.rb +7 -3
- data/test/fixtures/comments.rb +0 -1
- data/test/fixtures/complex_view.rb +0 -1
- data/test/fixtures/crazy_recursive.rb +0 -1
- data/test/fixtures/delimiters.rb +0 -1
- data/test/fixtures/dot_notation.rb +0 -1
- data/test/fixtures/double_section.rb +0 -1
- data/test/fixtures/inverted_section.rb +0 -1
- data/test/fixtures/lambda.rb +0 -1
- data/test/fixtures/liberal.mustache +1 -0
- data/test/fixtures/liberal.rb +25 -0
- data/test/fixtures/method_missing.rb +0 -1
- data/test/fixtures/namespaced.rb +0 -1
- data/test/fixtures/nested_objects.rb +0 -1
- data/test/fixtures/override/passenger.conf +6 -0
- data/test/fixtures/partial_with_module.rb +0 -1
- data/test/fixtures/passenger.rb +0 -1
- data/test/fixtures/recursive.rb +0 -1
- data/test/fixtures/simple.rb +0 -1
- data/test/fixtures/simply_complicated.mustache +25 -0
- data/test/fixtures/template_partial.rb +0 -1
- data/test/fixtures/unescaped.rb +0 -1
- data/test/helper.rb +5 -2
- data/test/mustache_test.rb +210 -25
- data/test/parser_test.rb +90 -7
- data/test/partial_test.rb +16 -4
- data/test/path_test.rb +49 -0
- data/test/spec_test.rb +5 -5
- data/test/template_test.rb +35 -3
- metadata +37 -59
- data/lib/mustache/sinatra.rb +0 -186
- data/lib/rack/bug/panels/mustache_panel/mustache_extension.rb +0 -27
- data/lib/rack/bug/panels/mustache_panel/view.mustache +0 -46
- data/lib/rack/bug/panels/mustache_panel.rb +0 -81
data/lib/mustache/parser.rb
CHANGED
|
@@ -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 :
|
|
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
|
-
|
|
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
|
-
@
|
|
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
|
-
|
|
137
|
+
unless @sections.empty?
|
|
98
138
|
# We have parsed the whole file, but there's still opened sections.
|
|
99
|
-
type, pos,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
data/lib/mustache/settings.rb
CHANGED
|
@@ -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.
|
|
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
|
|
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 =
|
|
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 =
|
|
66
|
+
@template_path = self.class.setup_path(path)
|
|
30
67
|
@template = nil
|
|
31
68
|
end
|
|
32
69
|
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
#
|
|
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 || "#{
|
|
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=(
|
|
116
|
-
@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 || "#{
|
|
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=(
|
|
127
|
-
@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
|
data/lib/mustache/template.rb
CHANGED
|
@@ -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
|
data/lib/mustache/version.rb
CHANGED