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
@@ -1,20 +1,19 @@
1
+ require 'mustache/context_miss'
2
+
1
3
  class Mustache
2
- # A ContextMiss is raised whenever a tag's target can not be found
3
- # in the current context if `Mustache#raise_on_context_miss?` is
4
- # set to true.
5
- #
6
- # For example, if your View class does not respond to `music` but
7
- # your template contains a `{{music}}` tag this exception will be raised.
8
- #
9
- # By default it is not raised. See Mustache.raise_on_context_miss.
10
- class ContextMiss < RuntimeError; end
11
4
 
12
5
  # A Context represents the context which a Mustache template is
13
6
  # executed within. All Mustache tags reference keys in the Context.
7
+ #
14
8
  class Context
15
- # Expect to be passed an instance of `Mustache`.
9
+
10
+ # Initializes a Mustache::Context.
11
+ #
12
+ # @param [Mustache] mustache A Mustache instance.
13
+ #
16
14
  def initialize(mustache)
17
15
  @stack = [mustache]
16
+ @partial_template_cache = {}
18
17
  end
19
18
 
20
19
  # A {{>partial}} tag translates into a call to the context's
@@ -23,6 +22,7 @@ class Mustache
23
22
  # If the Mustache view handling the rendering (e.g. the view
24
23
  # representing your profile page or some other template) responds
25
24
  # to `partial`, we call it and render the result.
25
+ #
26
26
  def partial(name, indentation = '')
27
27
  # Look for the first Mustache in the stack.
28
28
  mustache = mustache_in_stack
@@ -30,38 +30,55 @@ class Mustache
30
30
  # Indent the partial template by the given indentation.
31
31
  part = mustache.partial(name).to_s.gsub(/^/, indentation)
32
32
 
33
- # Call the Mustache's `partial` method and render the result.
34
- result = mustache.render(part, self)
33
+ # Get a template object for the partial and render the result.
34
+ template_for_partial(part).render(self)
35
+ end
36
+
37
+ def template_for_partial(partial)
38
+ @partial_template_cache[partial] ||= Template.new(partial)
35
39
  end
36
40
 
37
- # Find the first Mustache in the stack. If we're being rendered
38
- # inside a Mustache object as a context, we'll use that one.
41
+ # Find the first Mustache in the stack.
42
+ #
43
+ # If we're being rendered inside a Mustache object as a context,
44
+ # we'll use that one.
45
+ #
46
+ # @return [Mustache] First Mustache in the stack.
47
+ #
39
48
  def mustache_in_stack
40
- @stack.detect { |frame| frame.is_a?(Mustache) }
49
+ @mustache_in_stack ||= @stack.find { |frame| frame.is_a?(Mustache) }
41
50
  end
42
51
 
43
52
  # Allows customization of how Mustache escapes things.
44
53
  #
45
- # Returns a String.
46
- def escapeHTML(str)
47
- mustache_in_stack.escapeHTML(str)
54
+ # @param [Object] value Value to escape.
55
+ #
56
+ # @return [String] Escaped string.
57
+ #
58
+ def escape(value)
59
+ mustache_in_stack.escape(value)
48
60
  end
49
61
 
50
62
  # Adds a new object to the context's internal stack.
51
63
  #
52
- # Returns the Context.
53
- def push(new)
54
- @stack.unshift(new)
64
+ # @param [Object] new_obj Object to be added to the internal stack.
65
+ #
66
+ # @return [Context] Returns the Context.
67
+ #
68
+ def push(new_obj)
69
+ @stack.unshift(new_obj)
70
+ @mustache_in_stack = nil
55
71
  self
56
72
  end
57
- alias_method :update, :push
58
73
 
59
74
  # Removes the most recently added object from the context's
60
75
  # internal stack.
61
76
  #
62
- # Returns the Context.
77
+ # @return [Context] Returns the Context.
78
+ #
63
79
  def pop
64
80
  @stack.shift
81
+ @mustache_in_stack = nil
65
82
  self
66
83
  end
67
84
 
@@ -80,7 +97,7 @@ class Mustache
80
97
  # Do we know about a particular key? In other words, will calling
81
98
  # `context[key]` give us a result that was set. Basically.
82
99
  def has_key?(key)
83
- !!fetch(key)
100
+ fetch(key, false)
84
101
  rescue ContextMiss
85
102
  false
86
103
  end
@@ -97,9 +114,7 @@ class Mustache
97
114
  next if frame == self
98
115
 
99
116
  value = find(frame, name, :__missing)
100
- if value != :__missing
101
- return value
102
- end
117
+ return value if :__missing != value
103
118
  end
104
119
 
105
120
  if default == :__raise || mustache_in_stack.raise_on_context_miss?
@@ -114,28 +129,65 @@ class Mustache
114
129
  # If it's an object that responds to the key as a method call,
115
130
  # invokes that method. You get the idea.
116
131
  #
117
- # obj - The object to perform the lookup on.
118
- # key - The key whose value you want.
119
- # default - An optional default value, to return if the
120
- # key is not found.
132
+ # @param [Object] obj The object to perform the lookup on.
133
+ # @param [String,Symbol] key The key whose value you want
134
+ # @param [Object] default An optional default value, to return if the key is not found.
135
+ #
136
+ # @return [Object] The value of key in object if it is found, and default otherwise.
121
137
  #
122
- # Returns the value of key in obj if it is found and default otherwise.
123
138
  def find(obj, key, default = nil)
124
- hash = obj.respond_to?(:has_key?)
125
-
126
- if hash && obj.has_key?(key)
127
- obj[key]
128
- elsif hash && obj.has_key?(key.to_s)
129
- obj[key.to_s]
130
- elsif !hash && obj.respond_to?(key)
131
- meth = obj.method(key) rescue proc { obj.send(key) }
132
- if meth.arity == 1
133
- meth.to_proc
134
- else
135
- meth[]
136
- end
139
+
140
+ if mustache_in_stack.context_access_security_level == 5 and !obj.instance_of?(Hash)
141
+ # on level 5, object must be exactly Hash
142
+ return context_level_violation("Detected key access attempt for a non-Hash object using '#{key}'", default)
143
+ end
144
+
145
+ return find_in_hash(obj.to_hash, key, default) if obj.respond_to?(:to_hash)
146
+
147
+ if mustache_in_stack.context_access_security_level >= 4
148
+ # on levels 4 and 5, method access is not allowed
149
+ return context_level_violation("Detected method access attempt using '#{key}'", default)
150
+ end
151
+
152
+ unless obj.respond_to?(key)
153
+ # no match for the key, but it may include a hyphen, so try again replacing hyphens with underscores.
154
+ key = key.to_s.tr('-', '_')
155
+ return default unless obj.respond_to?(key)
156
+ end
157
+
158
+ if mustache_in_stack.context_access_security_level >= 3 && !obj.class.instance_methods(false).include?(key.to_sym)
159
+ # on level 3, inherited methods are blocked
160
+ return context_level_violation("Detected inherited method '#{key}'", default)
161
+ end
162
+
163
+ if mustache_in_stack.context_access_security_level >= 2 && MethodBlacklist.include?(key.to_s)
164
+ # on levels 2 and 3, reflection-based methods are blacklisted
165
+ return context_level_violation("Detected blacklisted reflection-related key '#{key}'", default)
166
+ end
167
+
168
+ meth = obj.method(key) rescue proc { obj.send(key) }
169
+ meth.arity == 1 ? meth.to_proc : meth.call
170
+ end
171
+
172
+ def current
173
+ @stack.first
174
+ end
175
+
176
+
177
+ private
178
+
179
+ # Fetches a hash key if it exists, or returns the given default.
180
+ def find_in_hash(obj, key, default)
181
+ return obj[key] if obj.has_key?(key)
182
+ return obj[key.to_s] if obj.has_key?(key.to_s)
183
+ return obj[key] if obj.respond_to?(:default_proc) && obj.default_proc && obj[key]
184
+
185
+ # If default is :__missing then we are from #fetch which is hunting through the stack
186
+ # If default is nil then we are reducing dot notation
187
+ if :__missing != default && mustache_in_stack.raise_on_context_miss?
188
+ raise ContextMiss.new("Can't find #{key} in #{obj}")
137
189
  else
138
- default
190
+ obj.fetch(key, default)
139
191
  end
140
192
  end
141
193
  end
@@ -0,0 +1,55 @@
1
+ class Mustache
2
+
3
+ # A ContextMiss is raised whenever a tag's target can not be found
4
+ # in the current context if `Mustache#raise_on_context_miss?` is
5
+ # set to true.
6
+ #
7
+ # For example, if your View class does not respond to `music` but
8
+ # your template contains a `{{music}}` tag this exception will be raised.
9
+ #
10
+ # By default it is not raised. See Mustache.raise_on_context_miss.
11
+ #
12
+ class ContextMiss < RuntimeError; end
13
+
14
+ # A ContextLevelViolation is raised whenever accessing a key in the context is
15
+ # not allowed by the selected context access security level
16
+ #
17
+ # For example, if your context access security level is set to 3 but
18
+ # your template tries to access an inherited method this exception will be raised.
19
+ #
20
+ # By default it is only raised for blacklisted keys in objects.
21
+ # See Mustache.context_access_security_level.
22
+ #
23
+ class ContextLevelViolation < ContextMiss; end
24
+
25
+ class Context
26
+ # Raises ContextLevelViolation or returns default depending on raise_on_context_miss
27
+ #
28
+ # @param [String] message The error message if raise_on_context_miss is set
29
+ # @param [Object] default An optional default value, to return if the key is not found.
30
+ #
31
+ # @return [Object] The default value if errors are not raised
32
+ #
33
+ def context_level_violation(message, default = nil)
34
+ return default unless mustache_in_stack.raise_on_context_miss?
35
+ raise ContextLevelViolation.new(message)
36
+ end
37
+ end
38
+
39
+
40
+ MethodBlacklist = ["allocate", "attached_object", "superclass", "subclasses", "new", "autoload?", "autoload",
41
+ "included_modules", "include?", "set_temporary_name", "ancestors", "attr", "attr_reader", "attr_writer",
42
+ "attr_accessor", "public_instance_method", "instance_methods", "public_instance_methods",
43
+ "protected_instance_methods", "private_instance_methods", "undefined_instance_methods", "freeze", "const_get",
44
+ "constants", "const_missing", "const_defined?", "const_set", "const_source_location", "remove_class_variable",
45
+ "class_variable_get", "class_variables", "private_constant", "class_variable_set", "class_variable_defined?",
46
+ "public_constant", "include", "deprecate_constant", "singleton_class?", "prepend", "refinements", "define_method",
47
+ "module_exec", "class_exec", "module_eval", "class_eval", "remove_method", "undef_method", "alias_method",
48
+ "method_defined?", "public_method_defined?", "private_method_defined?", "protected_method_defined?",
49
+ "public_class_method", "private_class_method", "instance_method", "singleton_class", "dup", "itself", "methods",
50
+ "singleton_methods", "protected_methods", "private_methods", "public_methods", "instance_variables",
51
+ "instance_variable_get", "instance_variable_set", "instance_variable_defined?", "remove_instance_variable",
52
+ "instance_of?", "kind_of?", "is_a?", "display", "public_send", "extend", "clone", "class", "frozen?", "tap",
53
+ "then", "yield_self", "respond_to?", "method", "public_method", "singleton_method", "define_singleton_method",
54
+ "hash", "object_id", "send", "enum_for", "__send__", "instance_eval", "instance_exec", "__id__"]
55
+ end
@@ -0,0 +1,3 @@
1
+ class Mustache
2
+ Enumerable = Module.new
3
+ end
@@ -27,9 +27,10 @@ class Mustache
27
27
  # $ mustache --compile test.mustache
28
28
  # "Hi #{CGI.escapeHTML(ctx[:thing].to_s)}!\n"
29
29
  class Generator
30
- # Options are unused for now but may become useful in the future.
30
+ # Options can be used to manipulate the resulting ruby code string behavior.
31
31
  def initialize(options = {})
32
32
  @options = options
33
+ @option_static_lambdas = options[:static_lambdas] == true
33
34
  end
34
35
 
35
36
  # Given an array of tokens, returns an interpolatable Ruby string.
@@ -37,6 +38,10 @@ class Mustache
37
38
  "\"#{compile!(exp)}\""
38
39
  end
39
40
 
41
+
42
+ private
43
+
44
+
40
45
  # Given an array of tokens, converts them into Ruby code. In
41
46
  # particular there are three types of expressions we are concerned
42
47
  # with:
@@ -64,21 +69,26 @@ class Mustache
64
69
  #
65
70
  # [:multi,
66
71
  # [:static, "Hello "],
67
- # [:mustache, :etag, "name"],
72
+ # [:mustache, :etag,
73
+ # [:mustache, :fetch, ["name"]]],
68
74
  # [:static, "\nYou have just won $"],
69
- # [:mustache, :etag, "value"],
70
- # [:static, "!\n"],
71
- # [:mustache,
72
- # :section,
73
- # "in_ca",
74
- # [:multi,
75
- # [:static, "Well, $"],
76
- # [:mustache, :etag, "taxed_value"],
77
- # [:static, ", after taxes.\n"]]]]
75
+ # [:mustache, :etag,
76
+ # [:mustache, :fetch, ["value"]]],
77
+ # [:static, "!\n"],
78
+ # [:mustache,
79
+ # :section,
80
+ # [:mustache, :fetch, ["in_ca"]],
81
+ # [:multi,
82
+ # [:static, "Well, $"],
83
+ # [:mustache, :etag,
84
+ # [:mustache, :fetch, ["taxed_value"]]],
85
+ # [:static, ", after taxes.\n"]],
86
+ # "Well, ${{taxed_value}}, after taxes.\n",
87
+ # ["{{", "}}"]]]
78
88
  def compile!(exp)
79
89
  case exp.first
80
90
  when :multi
81
- exp[1..-1].map { |e| compile!(e) }.join
91
+ exp[1..-1].reduce("".dup) { |sum, e| sum << compile!(e) }
82
92
  when :static
83
93
  str(exp[1])
84
94
  when :mustache
@@ -90,38 +100,49 @@ class Mustache
90
100
 
91
101
  # Callback fired when the compiler finds a section token. We're
92
102
  # passed the section name and the array of tokens.
93
- def on_section(name, content, raw, delims)
103
+ def on_section(name, offset, content, raw, delims)
94
104
  # Convert the tokenized content of this section into a Ruby
95
105
  # string we can use.
96
106
  code = compile(content)
97
107
 
98
- # Compile the Ruby for this section now that we know what's
99
- # inside the section.
100
- ev(<<-compiled)
101
- if v = #{compile!(name)}
102
- if v == true
103
- #{code}
104
- elsif v.is_a?(Proc)
108
+ # Lambda handling - default handling is to dynamically interpret
109
+ # the returned lambda result as mustache source
110
+ proc_handling = if @option_static_lambdas
111
+ <<-compiled
112
+ v.call(lambda {|v| #{code}}.call(v)).to_s
113
+ compiled
114
+ else
115
+ <<-compiled
105
116
  t = Mustache::Template.new(v.call(#{raw.inspect}).to_s)
106
117
  def t.tokens(src=@source)
107
- p = Parser.new
118
+ p = Mustache::Parser.new
108
119
  p.otag, p.ctag = #{delims.inspect}
109
120
  p.compile(src)
110
121
  end
111
122
  t.render(ctx.dup)
112
- else
113
- # Shortcut when passed non-array
114
- v = [v] unless v.is_a?(Array) || defined?(Enumerator) && v.is_a?(Enumerator)
123
+ compiled
124
+ end
115
125
 
116
- v.map { |h| ctx.push(h); r = #{code}; ctx.pop; r }.join
117
- end
126
+ # Compile the Ruby for this section now that we know what's
127
+ # inside the section.
128
+ ev(<<-compiled)
129
+ case v = #{compile!(name)}
130
+ when NilClass, FalseClass
131
+ when TrueClass
132
+ #{code}
133
+ when Proc
134
+ #{proc_handling}
135
+ when Array, Enumerator, Mustache::Enumerable
136
+ v.map { |_| ctx.push(_); r = #{code}; ctx.pop; r }.join
137
+ else
138
+ ctx.push(v); r = #{code}; ctx.pop; r
118
139
  end
119
140
  compiled
120
141
  end
121
142
 
122
143
  # Fired when we find an inverted section. Just like `on_section`,
123
144
  # we're passed the inverted section name and the array of tokens.
124
- def on_inverted_section(name, content, raw, _)
145
+ def on_inverted_section(name, offset, content, raw, delims)
125
146
  # Convert the tokenized content of this section into a Ruby
126
147
  # string we can use.
127
148
  code = compile(content)
@@ -130,7 +151,7 @@ class Mustache
130
151
  # what's inside.
131
152
  ev(<<-compiled)
132
153
  v = #{compile!(name)}
133
- if v.nil? || v == false || v.respond_to?(:empty?) && v.empty?
154
+ if v.nil? || v == false || (v.class != String && v.respond_to?(:empty?) && v.empty?)
134
155
  #{code}
135
156
  end
136
157
  compiled
@@ -139,45 +160,45 @@ class Mustache
139
160
  # Fired when the compiler finds a partial. We want to return code
140
161
  # which calls a partial at runtime instead of expanding and
141
162
  # including the partial's body to allow for recursive partials.
142
- def on_partial(name, indentation)
163
+ def on_partial(name, offset, indentation)
143
164
  ev("ctx.partial(#{name.to_sym.inspect}, #{indentation.inspect})")
144
165
  end
145
166
 
146
167
  # An unescaped tag.
147
- def on_utag(name)
168
+ def on_utag(name, offset)
148
169
  ev(<<-compiled)
149
170
  v = #{compile!(name)}
150
171
  if v.is_a?(Proc)
151
- v = Mustache::Template.new(v.call.to_s).render(ctx.dup)
172
+ v = #{@option_static_lambdas ? 'v.call' : 'Mustache::Template.new(v.call.to_s).render(ctx.dup)'}
152
173
  end
153
174
  v.to_s
154
175
  compiled
155
176
  end
156
177
 
157
178
  # An escaped tag.
158
- def on_etag(name)
179
+ def on_etag(name, offset)
159
180
  ev(<<-compiled)
160
181
  v = #{compile!(name)}
161
182
  if v.is_a?(Proc)
162
- v = Mustache::Template.new(v.call.to_s).render(ctx.dup)
183
+ v = #{@option_static_lambdas ? 'v.call' : 'Mustache::Template.new(v.call.to_s).render(ctx.dup)'}
163
184
  end
164
- ctx.escapeHTML(v.to_s)
185
+ ctx.escape(v)
165
186
  compiled
166
187
  end
167
188
 
168
189
  def on_fetch(names)
190
+ return "ctx.current" if names.empty?
191
+
169
192
  names = names.map { |n| n.to_sym }
170
193
 
171
- if names.length == 0
172
- "ctx[:to_s]"
173
- elsif names.length == 1
174
- "ctx[#{names.first.to_sym.inspect}]"
194
+ initial, *rest = names
195
+ if rest.any?
196
+ <<-compiled
197
+ #{rest.inspect}.reduce(ctx[#{initial.inspect}]) { |value, key| value && ctx.find(value, key) }
198
+ compiled
175
199
  else
176
- initial, *rest = names
177
200
  <<-compiled
178
- #{rest.inspect}.inject(ctx[#{initial.inspect}]) { |value, key|
179
- value && ctx.find(value, key)
180
- }
201
+ ctx[#{initial.inspect}]
181
202
  compiled
182
203
  end
183
204
  end