liquid 2.4.1 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Liquid Version History
2
2
 
3
+ ## 2.5.0 / 2013-03-06
4
+
5
+ * Prevent Object methods from being called on drops
6
+ * Avoid symbol injection from liquid
7
+ * Added break and continue statements
8
+ * Fix filter parser for args without space separators
9
+ * Add support for filter keyword arguments
10
+
3
11
  ## 2.4.0 / 2012-08-03
4
12
 
5
13
  * Performance improvements
data/lib/liquid/block.rb CHANGED
@@ -89,13 +89,27 @@ module Liquid
89
89
  end
90
90
 
91
91
  def render_all(list, context)
92
- list.collect do |token|
92
+ output = []
93
+ list.each do |token|
94
+ # Break out if we have any unhanded interrupts.
95
+ break if context.has_interrupt?
96
+
93
97
  begin
94
- token.respond_to?(:render) ? token.render(context) : token
98
+ # If we get an Interrupt that means the block must stop processing. An
99
+ # Interrupt is any command that stops block execution such as {% break %}
100
+ # or {% continue %}
101
+ if token.is_a? Continue or token.is_a? Break
102
+ context.push_interrupt(token.interrupt)
103
+ break
104
+ end
105
+
106
+ output << (token.respond_to?(:render) ? token.render(context) : token)
95
107
  rescue ::StandardError => e
96
- context.handle_error(e)
108
+ output << (context.handle_error(e))
97
109
  end
98
- end.join
110
+ end
111
+
112
+ output.join
99
113
  end
100
114
  end
101
115
  end
@@ -22,6 +22,8 @@ module Liquid
22
22
  @errors = []
23
23
  @rethrow_errors = rethrow_errors
24
24
  squash_instance_assigns_with_environments
25
+
26
+ @interrupts = []
25
27
  end
26
28
 
27
29
  def strainer
@@ -37,10 +39,26 @@ module Liquid
37
39
 
38
40
  filters.each do |f|
39
41
  raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module)
42
+ Strainer.add_known_filter(f)
40
43
  strainer.extend(f)
41
44
  end
42
45
  end
43
46
 
47
+ # are there any not handled interrupts?
48
+ def has_interrupt?
49
+ !@interrupts.empty?
50
+ end
51
+
52
+ # push an interrupt to the stack. this interrupt is considered not handled.
53
+ def push_interrupt(e)
54
+ @interrupts.push(e)
55
+ end
56
+
57
+ # pop an interrupt from the stack
58
+ def pop_interrupt
59
+ @interrupts.pop
60
+ end
61
+
44
62
  def handle_error(e)
45
63
  errors.push(e)
46
64
  raise if @rethrow_errors
@@ -54,11 +72,7 @@ module Liquid
54
72
  end
55
73
 
56
74
  def invoke(method, *args)
57
- if strainer.respond_to?(method)
58
- strainer.__send__(method, *args)
59
- else
60
- args.first
61
- end
75
+ strainer.invoke(method, *args)
62
76
  end
63
77
 
64
78
  # Push new local scope on the stack. use <tt>Context#stack</tt> instead
data/lib/liquid/drop.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'set'
2
+
1
3
  module Liquid
2
4
 
3
5
  # A drop in liquid is a class which allows you to export DOM like things to liquid.
@@ -22,6 +24,8 @@ module Liquid
22
24
  class Drop
23
25
  attr_writer :context
24
26
 
27
+ EMPTY_STRING = ''.freeze
28
+
25
29
  # Catch all for the method
26
30
  def before_method(method)
27
31
  nil
@@ -29,8 +33,8 @@ module Liquid
29
33
 
30
34
  # called by liquid to invoke a drop
31
35
  def invoke_drop(method_or_key)
32
- if method_or_key && method_or_key != '' && self.class.public_method_defined?(method_or_key.to_s.to_sym)
33
- send(method_or_key.to_s.to_sym)
36
+ if method_or_key && method_or_key != EMPTY_STRING && self.class.invokable?(method_or_key)
37
+ send(method_or_key)
34
38
  else
35
39
  before_method(method_or_key)
36
40
  end
@@ -45,5 +49,13 @@ module Liquid
45
49
  end
46
50
 
47
51
  alias :[] :invoke_drop
52
+
53
+ private
54
+
55
+ # Check for method existence without invoking respond_to?, which creates symbols
56
+ def self.invokable?(method_name)
57
+ @invokable_methods ||= Set.new((public_instance_methods - Liquid::Drop.public_instance_methods).map(&:to_s))
58
+ @invokable_methods.include?(method_name.to_s)
59
+ end
48
60
  end
49
61
  end
data/lib/liquid/errors.rb CHANGED
@@ -8,4 +8,4 @@ module Liquid
8
8
  class StandardError < Error; end
9
9
  class SyntaxError < Error; end
10
10
  class StackLevelError < Error; end
11
- end
11
+ end
@@ -0,0 +1,17 @@
1
+ module Liquid
2
+
3
+ # An interrupt is any command that breaks processing of a block (ex: a for loop).
4
+ class Interrupt
5
+ attr_reader :message
6
+
7
+ def initialize(message=nil)
8
+ @message = message || "interrupt"
9
+ end
10
+ end
11
+
12
+ # Interrupt that is thrown whenever a {% break %} is called.
13
+ class BreakInterrupt < Interrupt; end
14
+
15
+ # Interrupt that is thrown whenever a {% continue %} is called.
16
+ class ContinueInterrupt < Interrupt; end
17
+ end
@@ -2,24 +2,15 @@ require 'set'
2
2
 
3
3
  module Liquid
4
4
 
5
- parent_object = if defined? BlankObject
6
- BlankObject
7
- else
8
- Object
9
- end
10
-
11
5
  # Strainer is the parent class for the filters system.
12
- # New filters are mixed into the strainer class which is then instanciated for each liquid template render run.
6
+ # New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
13
7
  #
14
- # One of the strainer's responsibilities is to keep malicious method calls out
15
- class Strainer < parent_object #:nodoc:
16
- INTERNAL_METHOD = /^__/
17
- @@required_methods = Set.new([:__id__, :__send__, :respond_to?, :kind_of?, :extend, :methods, :singleton_methods, :class, :object_id])
18
-
19
- # Ruby 1.9.2 introduces Object#respond_to_missing?, which is invoked by Object#respond_to?
20
- @@required_methods << :respond_to_missing? if Object.respond_to? :respond_to_missing?
21
-
8
+ # The Strainer only allows method calls defined in filters given to it via Strainer.global_filter,
9
+ # Context#add_filters or Template.register_filter
10
+ class Strainer #:nodoc:
22
11
  @@filters = {}
12
+ @@known_filters = Set.new
13
+ @@known_methods = Set.new
23
14
 
24
15
  def initialize(context)
25
16
  @context = context
@@ -27,28 +18,36 @@ module Liquid
27
18
 
28
19
  def self.global_filter(filter)
29
20
  raise ArgumentError, "Passed filter is not a module" unless filter.is_a?(Module)
21
+ add_known_filter(filter)
30
22
  @@filters[filter.name] = filter
31
23
  end
32
24
 
25
+ def self.add_known_filter(filter)
26
+ unless @@known_filters.include?(filter)
27
+ @@method_blacklist ||= Set.new(Strainer.instance_methods.map(&:to_s))
28
+ new_methods = filter.instance_methods.map(&:to_s)
29
+ new_methods.reject!{ |m| @@method_blacklist.include?(m) }
30
+ @@known_methods.merge(new_methods)
31
+ @@known_filters.add(filter)
32
+ end
33
+ end
34
+
33
35
  def self.create(context)
34
36
  strainer = Strainer.new(context)
35
37
  @@filters.each { |k,m| strainer.extend(m) }
36
38
  strainer
37
39
  end
38
40
 
39
- def respond_to?(method, include_private = false)
40
- method_name = method.to_s
41
- return false if method_name =~ INTERNAL_METHOD
42
- return false if @@required_methods.include?(method_name)
43
- super
41
+ def invoke(method, *args)
42
+ if invokable?(method)
43
+ send(method, *args)
44
+ else
45
+ args.first
46
+ end
44
47
  end
45
48
 
46
- # remove all standard methods from the bucket so circumvent security
47
- # problems
48
- instance_methods.each do |m|
49
- unless @@required_methods.include?(m.to_sym)
50
- undef_method m
51
- end
49
+ def invokable?(method)
50
+ @@known_methods.include?(method.to_s) && respond_to?(method)
52
51
  end
53
52
  end
54
53
  end
@@ -0,0 +1,21 @@
1
+ module Liquid
2
+
3
+ # Break tag to be used to break out of a for loop.
4
+ #
5
+ # == Basic Usage:
6
+ # {% for item in collection %}
7
+ # {% if item.condition %}
8
+ # {% break %}
9
+ # {% endif %}
10
+ # {% endfor %}
11
+ #
12
+ class Break < Tag
13
+
14
+ def interrupt
15
+ BreakInterrupt.new
16
+ end
17
+
18
+ end
19
+
20
+ Template.register_tag('break', Break)
21
+ end
@@ -0,0 +1,21 @@
1
+ module Liquid
2
+
3
+ # Continue tag to be used to break out of a for loop.
4
+ #
5
+ # == Basic Usage:
6
+ # {% for item in collection %}
7
+ # {% if item.condition %}
8
+ # {% continue %}
9
+ # {% endif %}
10
+ # {% endfor %}
11
+ #
12
+ class Continue < Tag
13
+
14
+ def interrupt
15
+ ContinueInterrupt.new
16
+ end
17
+
18
+ end
19
+
20
+ Template.register_tag('continue', Continue)
21
+ end
@@ -69,7 +69,7 @@ module Liquid
69
69
  @nodelist = @else_block = []
70
70
  end
71
71
 
72
- def render(context)
72
+ def render(context)
73
73
  context.registers[:for] ||= Hash.new(0)
74
74
 
75
75
  collection = context[@collection_name]
@@ -101,8 +101,8 @@ module Liquid
101
101
  # Store our progress through the collection for the continue flag
102
102
  context.registers[:for][@name] = from + segment.length
103
103
 
104
- context.stack do
105
- segment.each_with_index do |item, index|
104
+ context.stack do
105
+ segment.each_with_index do |item, index|
106
106
  context[@variable_name] = item
107
107
  context['forloop'] = {
108
108
  'name' => @name,
@@ -115,6 +115,13 @@ module Liquid
115
115
  'last' => (index == length - 1) }
116
116
 
117
117
  result << render_all(@for_block, context)
118
+
119
+ # Handle any interrupts if they exist.
120
+ if context.has_interrupt?
121
+ interrupt = context.pop_interrupt
122
+ break if interrupt.is_a? BreakInterrupt
123
+ next if interrupt.is_a? ContinueInterrupt
124
+ end
118
125
  end
119
126
  end
120
127
  result
@@ -11,7 +11,7 @@ module Liquid
11
11
  # {{ user | link }}
12
12
  #
13
13
  class Variable
14
- FilterParser = /(?:#{FilterSeparator}|(?:\s*(?!(?:#{FilterSeparator}))(?:#{QuotedFragment}|\S+)\s*)+)/o
14
+ FilterParser = /(?:#{FilterSeparator}|(?:\s*(?:#{QuotedFragment}|#{ArgumentSeparator})\s*)+)/o
15
15
  attr_accessor :filters, :name
16
16
 
17
17
  def initialize(markup)
@@ -23,10 +23,10 @@ module Liquid
23
23
  if match[2].match(/#{FilterSeparator}\s*(.*)/o)
24
24
  filters = Regexp.last_match(1).scan(FilterParser)
25
25
  filters.each do |f|
26
- if matches = f.match(/\s*(\w+)/)
26
+ if matches = f.match(/\s*(\w+)(?:\s*#{FilterArgumentSeparator}(.*))?/)
27
27
  filtername = matches[1]
28
- filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*(#{QuotedFragment})/o).flatten
29
- @filters << [filtername.to_sym, filterargs]
28
+ filterargs = matches[2].to_s.scan(/(?:\A|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
29
+ @filters << [filtername, filterargs]
30
30
  end
31
31
  end
32
32
  end
@@ -36,9 +36,16 @@ module Liquid
36
36
  def render(context)
37
37
  return '' if @name.nil?
38
38
  @filters.inject(context[@name]) do |output, filter|
39
- filterargs = filter[1].to_a.collect do |a|
40
- context[a]
39
+ filterargs = []
40
+ keyword_args = {}
41
+ filter[1].to_a.each do |a|
42
+ if matches = a.match(/\A#{TagAttributes}\z/o)
43
+ keyword_args[matches[1]] = context[matches[2]]
44
+ else
45
+ filterargs << context[a]
46
+ end
41
47
  end
48
+ filterargs << keyword_args unless keyword_args.empty?
42
49
  begin
43
50
  output = context.invoke(filter[0], output, *filterargs)
44
51
  rescue FilterNotFound
data/lib/liquid.rb CHANGED
@@ -48,6 +48,7 @@ end
48
48
  require 'liquid/drop'
49
49
  require 'liquid/extensions'
50
50
  require 'liquid/errors'
51
+ require 'liquid/interrupts'
51
52
  require 'liquid/strainer'
52
53
  require 'liquid/context'
53
54
  require 'liquid/tag'
@@ -1,6 +1,6 @@
1
1
  require 'test_helper'
2
2
 
3
- class VariableTest < Test::Unit::TestCase
3
+ class BlockTest < Test::Unit::TestCase
4
4
  include Liquid
5
5
 
6
6
  def test_blankspace
@@ -189,10 +189,10 @@ class ContextTest < Test::Unit::TestCase
189
189
  end
190
190
 
191
191
  context = Context.new
192
- methods_before = context.strainer.methods.map { |method| method.to_s }
192
+ assert_equal "Wookie", context.invoke("hi", "Wookie")
193
+
193
194
  context.add_filters(filter)
194
- methods_after = context.strainer.methods.map { |method| method.to_s }
195
- assert_equal (methods_before + ["hi"]).sort, methods_after.sort
195
+ assert_equal "Wookie hi!", context.invoke("hi", "Wookie")
196
196
  end
197
197
 
198
198
  def test_add_item_in_outer_scope
@@ -115,6 +115,13 @@ class DropsTest < Test::Unit::TestCase
115
115
  assert_equal ' ', output
116
116
  end
117
117
 
118
+ def test_object_methods_not_allowed
119
+ [:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method|
120
+ output = Liquid::Template.parse(" {{ product.#{method} }} ").render('product' => ProductDrop.new)
121
+ assert_equal ' ', output
122
+ end
123
+ end
124
+
118
125
  def test_scope
119
126
  assert_equal '1', Liquid::Template.parse( '{{ context.scopes }}' ).render('context' => ContextDrop.new)
120
127
  assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ context.scopes }}{%endfor%}' ).render('context' => ContextDrop.new, 'dummy' => [1])
@@ -16,6 +16,12 @@ module CanadianMoneyFilter
16
16
  end
17
17
  end
18
18
 
19
+ module SubstituteFilter
20
+ def substitute(input, params={})
21
+ input.gsub(/%\{(\w+)\}/) { |match| params[$1] }
22
+ end
23
+ end
24
+
19
25
  class FiltersTest < Test::Unit::TestCase
20
26
  include Liquid
21
27
 
@@ -92,6 +98,13 @@ class FiltersTest < Test::Unit::TestCase
92
98
 
93
99
  assert_equal 1000, Variable.new("var | xyzzy").render(@context)
94
100
  end
101
+
102
+ def test_filter_with_keyword_arguments
103
+ @context['surname'] = 'john'
104
+ @context.add_filters(SubstituteFilter)
105
+ output = Variable.new(%! 'hello %{first_name}, %{last_name}' | substitute: first_name: surname, last_name: 'doe' !).render(@context)
106
+ assert_equal 'hello john, doe', output
107
+ end
95
108
  end
96
109
 
97
110
  class FiltersInTemplate < Test::Unit::TestCase
@@ -38,4 +38,27 @@ class SecurityTest < Test::Unit::TestCase
38
38
 
39
39
  assert_equal expected, Template.parse(text).render(@assigns, :filters => SecurityFilter)
40
40
  end
41
+
42
+ def test_does_not_add_filters_to_symbol_table
43
+ current_symbols = Symbol.all_symbols
44
+
45
+ test = %( {{ "some_string" | a_bad_filter }} )
46
+
47
+ template = Template.parse(test)
48
+ assert_equal [], (Symbol.all_symbols - current_symbols)
49
+
50
+ template.render
51
+ assert_equal [], (Symbol.all_symbols - current_symbols)
52
+ end
53
+
54
+ def test_does_not_add_drop_methods_to_symbol_table
55
+ current_symbols = Symbol.all_symbols
56
+
57
+ drop = Drop.new
58
+ drop.invoke_drop("custom_method_1")
59
+ drop.invoke_drop("custom_method_2")
60
+ drop.invoke_drop("custom_method_3")
61
+
62
+ assert_equal [], (Symbol.all_symbols - current_symbols)
63
+ end
41
64
  end # SecurityTest
@@ -3,23 +3,50 @@ require 'test_helper'
3
3
  class StrainerTest < Test::Unit::TestCase
4
4
  include Liquid
5
5
 
6
+ module AccessScopeFilters
7
+ def public_filter
8
+ "public"
9
+ end
10
+
11
+ def private_filter
12
+ "private"
13
+ end
14
+ private :private_filter
15
+ end
16
+
17
+ Strainer.global_filter(AccessScopeFilters)
18
+
6
19
  def test_strainer
7
20
  strainer = Strainer.create(nil)
8
- assert_equal false, strainer.respond_to?('__test__')
9
- assert_equal false, strainer.respond_to?('test')
10
- assert_equal false, strainer.respond_to?('instance_eval')
11
- assert_equal false, strainer.respond_to?('__send__')
12
- assert_equal true, strainer.respond_to?('size') # from the standard lib
21
+ assert_equal 5, strainer.invoke('size', 'input')
22
+ assert_equal "public", strainer.invoke("public_filter")
23
+ end
24
+
25
+ def test_strainer_only_invokes_public_filter_methods
26
+ strainer = Strainer.create(nil)
27
+ assert_equal false, strainer.invokable?('__test__')
28
+ assert_equal false, strainer.invokable?('test')
29
+ assert_equal false, strainer.invokable?('instance_eval')
30
+ assert_equal false, strainer.invokable?('__send__')
31
+ assert_equal true, strainer.invokable?('size') # from the standard lib
32
+ end
33
+
34
+ def test_strainer_returns_nil_if_no_filter_method_found
35
+ strainer = Strainer.create(nil)
36
+ assert_nil strainer.invoke("private_filter")
37
+ assert_nil strainer.invoke("undef_the_filter")
13
38
  end
14
39
 
15
- def test_should_respond_to_two_parameters
40
+ def test_strainer_returns_first_argument_if_no_method_and_arguments_given
16
41
  strainer = Strainer.create(nil)
17
- assert_equal true, strainer.respond_to?('size', false)
42
+ assert_equal "password", strainer.invoke("undef_the_method", "password")
18
43
  end
19
44
 
20
- # Asserts that Object#respond_to_missing? is not being undefined in Ruby versions where it has been implemented
21
- # Currently this method is only present in Ruby v1.9.2, or higher
22
- def test_object_respond_to_missing
23
- assert_equal Object.respond_to?(:respond_to_missing?), Strainer.create(nil).respond_to?(:respond_to_missing?)
45
+ def test_strainer_only_allows_methods_defined_in_filters
46
+ strainer = Strainer.create(nil)
47
+ assert_equal "1 + 1", strainer.invoke("instance_eval", "1 + 1")
48
+ assert_equal "puts", strainer.invoke("__send__", "puts", "Hi Mom")
49
+ assert_equal "has_method?", strainer.invoke("invoke", "has_method?", "invoke")
24
50
  end
51
+
25
52
  end # StrainerTest
@@ -0,0 +1,16 @@
1
+ require 'test_helper'
2
+
3
+ class BreakTagTest < Test::Unit::TestCase
4
+ include Liquid
5
+
6
+ # tests that no weird errors are raised if break is called outside of a
7
+ # block
8
+ def test_break_with_no_block
9
+ assigns = {'i' => 1}
10
+ markup = '{% break %}'
11
+ expected = ''
12
+
13
+ assert_template_result(expected, markup, assigns)
14
+ end
15
+
16
+ end
@@ -0,0 +1,16 @@
1
+ require 'test_helper'
2
+
3
+ class ContinueTagTest < Test::Unit::TestCase
4
+ include Liquid
5
+
6
+ # tests that no weird errors are raised if continue is called outside of a
7
+ # block
8
+ def test_continue_with_no_block
9
+ assigns = {}
10
+ markup = '{% continue %}'
11
+ expected = ''
12
+
13
+ assert_template_result(expected, markup, assigns)
14
+ end
15
+
16
+ end
@@ -168,6 +168,88 @@ HERE
168
168
  assert_template_result(expected,markup,assigns)
169
169
  end
170
170
 
171
+ def test_for_with_break
172
+ assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,10]}}
173
+
174
+ markup = '{% for i in array.items %}{% break %}{% endfor %}'
175
+ expected = ""
176
+ assert_template_result(expected,markup,assigns)
177
+
178
+ markup = '{% for i in array.items %}{{ i }}{% break %}{% endfor %}'
179
+ expected = "1"
180
+ assert_template_result(expected,markup,assigns)
181
+
182
+ markup = '{% for i in array.items %}{% break %}{{ i }}{% endfor %}'
183
+ expected = ""
184
+ assert_template_result(expected,markup,assigns)
185
+
186
+ markup = '{% for i in array.items %}{{ i }}{% if i > 3 %}{% break %}{% endif %}{% endfor %}'
187
+ expected = "1234"
188
+ assert_template_result(expected,markup,assigns)
189
+
190
+ # tests to ensure it only breaks out of the local for loop
191
+ # and not all of them.
192
+ assigns = {'array' => [[1,2],[3,4],[5,6]] }
193
+ markup = '{% for item in array %}' +
194
+ '{% for i in item %}' +
195
+ '{% if i == 1 %}' +
196
+ '{% break %}' +
197
+ '{% endif %}' +
198
+ '{{ i }}' +
199
+ '{% endfor %}' +
200
+ '{% endfor %}'
201
+ expected = '3456'
202
+ assert_template_result(expected, markup, assigns)
203
+
204
+ # test break does nothing when unreached
205
+ assigns = {'array' => {'items' => [1,2,3,4,5]}}
206
+ markup = '{% for i in array.items %}{% if i == 9999 %}{% break %}{% endif %}{{ i }}{% endfor %}'
207
+ expected = '12345'
208
+ assert_template_result(expected, markup, assigns)
209
+ end
210
+
211
+ def test_for_with_continue
212
+ assigns = {'array' => {'items' => [1,2,3,4,5]}}
213
+
214
+ markup = '{% for i in array.items %}{% continue %}{% endfor %}'
215
+ expected = ""
216
+ assert_template_result(expected,markup,assigns)
217
+
218
+ markup = '{% for i in array.items %}{{ i }}{% continue %}{% endfor %}'
219
+ expected = "12345"
220
+ assert_template_result(expected,markup,assigns)
221
+
222
+ markup = '{% for i in array.items %}{% continue %}{{ i }}{% endfor %}'
223
+ expected = ""
224
+ assert_template_result(expected,markup,assigns)
225
+
226
+ markup = '{% for i in array.items %}{% if i > 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}'
227
+ expected = "123"
228
+ assert_template_result(expected,markup,assigns)
229
+
230
+ markup = '{% for i in array.items %}{% if i == 3 %}{% continue %}{% else %}{{ i }}{% endif %}{% endfor %}'
231
+ expected = "1245"
232
+ assert_template_result(expected,markup,assigns)
233
+
234
+ # tests to ensure it only continues the local for loop and not all of them.
235
+ assigns = {'array' => [[1,2],[3,4],[5,6]] }
236
+ markup = '{% for item in array %}' +
237
+ '{% for i in item %}' +
238
+ '{% if i == 1 %}' +
239
+ '{% continue %}' +
240
+ '{% endif %}' +
241
+ '{{ i }}' +
242
+ '{% endfor %}' +
243
+ '{% endfor %}'
244
+ expected = '23456'
245
+ assert_template_result(expected, markup, assigns)
246
+
247
+ # test continue does nothing when unreached
248
+ assigns = {'array' => {'items' => [1,2,3,4,5]}}
249
+ markup = '{% for i in array.items %}{% if i == 9999 %}{% continue %}{% endif %}{{ i }}{% endfor %}'
250
+ expected = '12345'
251
+ assert_template_result(expected, markup, assigns)
252
+ end
171
253
 
172
254
  def test_for_tag_string
173
255
  # ruby 1.8.7 "String".each => Enumerator with single "String" element.
@@ -11,67 +11,71 @@ class VariableTest < Test::Unit::TestCase
11
11
  def test_filters
12
12
  var = Variable.new('hello | textileze')
13
13
  assert_equal 'hello', var.name
14
- assert_equal [[:textileze,[]]], var.filters
14
+ assert_equal [["textileze",[]]], var.filters
15
15
 
16
16
  var = Variable.new('hello | textileze | paragraph')
17
17
  assert_equal 'hello', var.name
18
- assert_equal [[:textileze,[]], [:paragraph,[]]], var.filters
18
+ assert_equal [["textileze",[]], ["paragraph",[]]], var.filters
19
19
 
20
20
  var = Variable.new(%! hello | strftime: '%Y'!)
21
21
  assert_equal 'hello', var.name
22
- assert_equal [[:strftime,["'%Y'"]]], var.filters
22
+ assert_equal [["strftime",["'%Y'"]]], var.filters
23
23
 
24
24
  var = Variable.new(%! 'typo' | link_to: 'Typo', true !)
25
25
  assert_equal %!'typo'!, var.name
26
- assert_equal [[:link_to,["'Typo'", "true"]]], var.filters
26
+ assert_equal [["link_to",["'Typo'", "true"]]], var.filters
27
27
 
28
28
  var = Variable.new(%! 'typo' | link_to: 'Typo', false !)
29
29
  assert_equal %!'typo'!, var.name
30
- assert_equal [[:link_to,["'Typo'", "false"]]], var.filters
30
+ assert_equal [["link_to",["'Typo'", "false"]]], var.filters
31
31
 
32
32
  var = Variable.new(%! 'foo' | repeat: 3 !)
33
33
  assert_equal %!'foo'!, var.name
34
- assert_equal [[:repeat,["3"]]], var.filters
34
+ assert_equal [["repeat",["3"]]], var.filters
35
35
 
36
36
  var = Variable.new(%! 'foo' | repeat: 3, 3 !)
37
37
  assert_equal %!'foo'!, var.name
38
- assert_equal [[:repeat,["3","3"]]], var.filters
38
+ assert_equal [["repeat",["3","3"]]], var.filters
39
39
 
40
40
  var = Variable.new(%! 'foo' | repeat: 3, 3, 3 !)
41
41
  assert_equal %!'foo'!, var.name
42
- assert_equal [[:repeat,["3","3","3"]]], var.filters
42
+ assert_equal [["repeat",["3","3","3"]]], var.filters
43
43
 
44
44
  var = Variable.new(%! hello | strftime: '%Y, okay?'!)
45
45
  assert_equal 'hello', var.name
46
- assert_equal [[:strftime,["'%Y, okay?'"]]], var.filters
46
+ assert_equal [["strftime",["'%Y, okay?'"]]], var.filters
47
47
 
48
48
  var = Variable.new(%! hello | things: "%Y, okay?", 'the other one'!)
49
49
  assert_equal 'hello', var.name
50
- assert_equal [[:things,["\"%Y, okay?\"","'the other one'"]]], var.filters
50
+ assert_equal [["things",["\"%Y, okay?\"","'the other one'"]]], var.filters
51
51
  end
52
52
 
53
53
  def test_filter_with_date_parameter
54
54
 
55
55
  var = Variable.new(%! '2006-06-06' | date: "%m/%d/%Y"!)
56
56
  assert_equal "'2006-06-06'", var.name
57
- assert_equal [[:date,["\"%m/%d/%Y\""]]], var.filters
57
+ assert_equal [["date",["\"%m/%d/%Y\""]]], var.filters
58
58
 
59
59
  end
60
60
 
61
61
  def test_filters_without_whitespace
62
62
  var = Variable.new('hello | textileze | paragraph')
63
63
  assert_equal 'hello', var.name
64
- assert_equal [[:textileze,[]], [:paragraph,[]]], var.filters
64
+ assert_equal [["textileze",[]], ["paragraph",[]]], var.filters
65
65
 
66
66
  var = Variable.new('hello|textileze|paragraph')
67
67
  assert_equal 'hello', var.name
68
- assert_equal [[:textileze,[]], [:paragraph,[]]], var.filters
68
+ assert_equal [["textileze",[]], ["paragraph",[]]], var.filters
69
+
70
+ var = Variable.new("hello|replace:'foo','bar'|textileze")
71
+ assert_equal 'hello', var.name
72
+ assert_equal [["replace", ["'foo'", "'bar'"]], ["textileze", []]], var.filters
69
73
  end
70
74
 
71
75
  def test_symbol
72
76
  var = Variable.new("http://disney.com/logo.gif | image: 'med' ")
73
77
  assert_equal 'http://disney.com/logo.gif', var.name
74
- assert_equal [[:image,["'med'"]]], var.filters
78
+ assert_equal [["image",["'med'"]]], var.filters
75
79
  end
76
80
 
77
81
  def test_string_single_quoted
@@ -103,6 +107,12 @@ class VariableTest < Test::Unit::TestCase
103
107
  var = Variable.new(%| test.test |)
104
108
  assert_equal 'test.test', var.name
105
109
  end
110
+
111
+ def test_filter_with_keyword_arguments
112
+ var = Variable.new(%! hello | things: greeting: "world", farewell: 'goodbye'!)
113
+ assert_equal 'hello', var.name
114
+ assert_equal [['things',["greeting: \"world\"","farewell: 'goodbye'"]]], var.filters
115
+ end
106
116
  end
107
117
 
108
118
 
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: liquid
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.1
5
4
  prerelease:
5
+ version: 2.5.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - Tobias Luetke
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-08-07 00:00:00.000000000 Z
12
+ date: 2013-03-06 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description:
15
15
  email:
@@ -30,14 +30,17 @@ files:
30
30
  - lib/liquid/extensions.rb
31
31
  - lib/liquid/file_system.rb
32
32
  - lib/liquid/htmltags.rb
33
+ - lib/liquid/interrupts.rb
33
34
  - lib/liquid/module_ex.rb
34
35
  - lib/liquid/standardfilters.rb
35
36
  - lib/liquid/strainer.rb
36
37
  - lib/liquid/tag.rb
37
38
  - lib/liquid/tags/assign.rb
39
+ - lib/liquid/tags/break.rb
38
40
  - lib/liquid/tags/capture.rb
39
41
  - lib/liquid/tags/case.rb
40
42
  - lib/liquid/tags/comment.rb
43
+ - lib/liquid/tags/continue.rb
41
44
  - lib/liquid/tags/cycle.rb
42
45
  - lib/liquid/tags/decrement.rb
43
46
  - lib/liquid/tags/for.rb
@@ -53,6 +56,7 @@ files:
53
56
  - lib/liquid.rb
54
57
  - MIT-LICENSE
55
58
  - README.md
59
+ - History.md
56
60
  - test/liquid/assign_test.rb
57
61
  - test/liquid/block_test.rb
58
62
  - test/liquid/capture_test.rb
@@ -69,6 +73,8 @@ files:
69
73
  - test/liquid/security_test.rb
70
74
  - test/liquid/standard_filter_test.rb
71
75
  - test/liquid/strainer_test.rb
76
+ - test/liquid/tags/break_tag_test.rb
77
+ - test/liquid/tags/continue_tag_test.rb
72
78
  - test/liquid/tags/for_tag_test.rb
73
79
  - test/liquid/tags/html_tag_test.rb
74
80
  - test/liquid/tags/if_else_tag_test.rb
@@ -81,7 +87,6 @@ files:
81
87
  - test/liquid/template_test.rb
82
88
  - test/liquid/variable_test.rb
83
89
  - test/test_helper.rb
84
- - History.md
85
90
  homepage: http://www.liquidmarkup.org
86
91
  licenses: []
87
92
  post_install_message:
@@ -89,20 +94,20 @@ rdoc_options: []
89
94
  require_paths:
90
95
  - lib
91
96
  required_ruby_version: !ruby/object:Gem::Requirement
92
- none: false
93
97
  requirements:
94
98
  - - ! '>='
95
99
  - !ruby/object:Gem::Version
96
100
  version: '0'
97
- required_rubygems_version: !ruby/object:Gem::Requirement
98
101
  none: false
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
103
  requirements:
100
104
  - - ! '>='
101
105
  - !ruby/object:Gem::Version
102
106
  version: 1.3.7
107
+ none: false
103
108
  requirements: []
104
109
  rubyforge_project:
105
- rubygems_version: 1.8.11
110
+ rubygems_version: 1.8.23
106
111
  signing_key:
107
112
  specification_version: 3
108
113
  summary: A secure, non-evaling end user template engine with aesthetic markup.
@@ -123,6 +128,8 @@ test_files:
123
128
  - test/liquid/security_test.rb
124
129
  - test/liquid/standard_filter_test.rb
125
130
  - test/liquid/strainer_test.rb
131
+ - test/liquid/tags/break_tag_test.rb
132
+ - test/liquid/tags/continue_tag_test.rb
126
133
  - test/liquid/tags/for_tag_test.rb
127
134
  - test/liquid/tags/html_tag_test.rb
128
135
  - test/liquid/tags/if_else_tag_test.rb