liquid 2.4.1 → 2.5.0

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.
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