liquid 3.0.6 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +98 -58
  3. data/README.md +31 -0
  4. data/lib/liquid/block.rb +31 -124
  5. data/lib/liquid/block_body.rb +75 -59
  6. data/lib/liquid/condition.rb +23 -22
  7. data/lib/liquid/context.rb +50 -46
  8. data/lib/liquid/document.rb +19 -9
  9. data/lib/liquid/drop.rb +17 -16
  10. data/lib/liquid/errors.rb +20 -24
  11. data/lib/liquid/expression.rb +15 -3
  12. data/lib/liquid/extensions.rb +13 -7
  13. data/lib/liquid/file_system.rb +11 -11
  14. data/lib/liquid/forloop_drop.rb +42 -0
  15. data/lib/liquid/i18n.rb +5 -5
  16. data/lib/liquid/interrupts.rb +1 -2
  17. data/lib/liquid/lexer.rb +6 -4
  18. data/lib/liquid/locales/en.yml +5 -1
  19. data/lib/liquid/parse_context.rb +37 -0
  20. data/lib/liquid/parser_switching.rb +4 -4
  21. data/lib/liquid/profiler/hooks.rb +7 -7
  22. data/lib/liquid/profiler.rb +18 -19
  23. data/lib/liquid/range_lookup.rb +16 -1
  24. data/lib/liquid/resource_limits.rb +23 -0
  25. data/lib/liquid/standardfilters.rb +121 -61
  26. data/lib/liquid/strainer.rb +14 -7
  27. data/lib/liquid/tablerowloop_drop.rb +62 -0
  28. data/lib/liquid/tag.rb +9 -8
  29. data/lib/liquid/tags/assign.rb +17 -4
  30. data/lib/liquid/tags/break.rb +0 -3
  31. data/lib/liquid/tags/capture.rb +1 -1
  32. data/lib/liquid/tags/case.rb +19 -12
  33. data/lib/liquid/tags/comment.rb +2 -2
  34. data/lib/liquid/tags/cycle.rb +6 -6
  35. data/lib/liquid/tags/decrement.rb +1 -4
  36. data/lib/liquid/tags/for.rb +95 -75
  37. data/lib/liquid/tags/if.rb +49 -44
  38. data/lib/liquid/tags/ifchanged.rb +0 -2
  39. data/lib/liquid/tags/include.rb +61 -52
  40. data/lib/liquid/tags/raw.rb +32 -4
  41. data/lib/liquid/tags/table_row.rb +12 -30
  42. data/lib/liquid/tags/unless.rb +3 -4
  43. data/lib/liquid/template.rb +42 -54
  44. data/lib/liquid/tokenizer.rb +31 -0
  45. data/lib/liquid/utils.rb +52 -8
  46. data/lib/liquid/variable.rb +46 -45
  47. data/lib/liquid/variable_lookup.rb +7 -5
  48. data/lib/liquid/version.rb +1 -1
  49. data/lib/liquid.rb +9 -7
  50. data/test/integration/assign_test.rb +8 -8
  51. data/test/integration/blank_test.rb +14 -14
  52. data/test/integration/context_test.rb +2 -2
  53. data/test/integration/document_test.rb +19 -0
  54. data/test/integration/drop_test.rb +42 -40
  55. data/test/integration/error_handling_test.rb +99 -46
  56. data/test/integration/filter_test.rb +60 -20
  57. data/test/integration/hash_ordering_test.rb +9 -9
  58. data/test/integration/output_test.rb +26 -27
  59. data/test/integration/parsing_quirks_test.rb +15 -13
  60. data/test/integration/render_profiling_test.rb +20 -20
  61. data/test/integration/security_test.rb +9 -7
  62. data/test/integration/standard_filter_test.rb +179 -40
  63. data/test/integration/tags/break_tag_test.rb +1 -2
  64. data/test/integration/tags/continue_tag_test.rb +0 -1
  65. data/test/integration/tags/for_tag_test.rb +133 -98
  66. data/test/integration/tags/if_else_tag_test.rb +75 -77
  67. data/test/integration/tags/include_tag_test.rb +34 -30
  68. data/test/integration/tags/increment_tag_test.rb +10 -11
  69. data/test/integration/tags/raw_tag_test.rb +7 -1
  70. data/test/integration/tags/standard_tag_test.rb +121 -122
  71. data/test/integration/tags/statements_test.rb +3 -5
  72. data/test/integration/tags/table_row_test.rb +20 -19
  73. data/test/integration/tags/unless_else_tag_test.rb +6 -6
  74. data/test/integration/template_test.rb +190 -49
  75. data/test/integration/trim_mode_test.rb +525 -0
  76. data/test/integration/variable_test.rb +23 -13
  77. data/test/test_helper.rb +33 -5
  78. data/test/unit/block_unit_test.rb +8 -5
  79. data/test/unit/condition_unit_test.rb +86 -77
  80. data/test/unit/context_unit_test.rb +48 -57
  81. data/test/unit/file_system_unit_test.rb +3 -3
  82. data/test/unit/i18n_unit_test.rb +2 -2
  83. data/test/unit/lexer_unit_test.rb +11 -8
  84. data/test/unit/parser_unit_test.rb +2 -2
  85. data/test/unit/regexp_unit_test.rb +1 -1
  86. data/test/unit/strainer_unit_test.rb +80 -1
  87. data/test/unit/tag_unit_test.rb +7 -2
  88. data/test/unit/tags/case_tag_unit_test.rb +1 -1
  89. data/test/unit/tags/for_tag_unit_test.rb +2 -2
  90. data/test/unit/tags/if_tag_unit_test.rb +1 -1
  91. data/test/unit/template_unit_test.rb +14 -5
  92. data/test/unit/tokenizer_unit_test.rb +24 -7
  93. data/test/unit/variable_unit_test.rb +60 -43
  94. metadata +19 -14
  95. data/lib/liquid/module_ex.rb +0 -62
  96. data/lib/liquid/token.rb +0 -18
  97. data/test/unit/module_ex_unit_test.rb +0 -87
  98. /data/{MIT-LICENSE → LICENSE} +0 -0
@@ -1,7 +1,6 @@
1
1
  require 'set'
2
2
 
3
3
  module Liquid
4
-
5
4
  # Strainer is the parent class for the filters system.
6
5
  # New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
7
6
  #
@@ -22,19 +21,25 @@ module Liquid
22
21
  @context = context
23
22
  end
24
23
 
25
- def self.filter_methods
26
- @filter_methods
24
+ class << self
25
+ attr_reader :filter_methods
27
26
  end
28
27
 
29
28
  def self.add_filter(filter)
30
- raise ArgumentError, "Expected module but got: #{f.class}" unless filter.is_a?(Module)
29
+ raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
31
30
  unless self.class.include?(filter)
32
- self.send(:include, filter)
33
- @filter_methods.merge(filter.public_instance_methods.map(&:to_s))
31
+ invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
32
+ if invokable_non_public_methods.any?
33
+ raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
34
+ else
35
+ send(:include, filter)
36
+ @filter_methods.merge(filter.public_instance_methods.map(&:to_s))
37
+ end
34
38
  end
35
39
  end
36
40
 
37
41
  def self.global_filter(filter)
42
+ @@strainer_class_cache.clear
38
43
  @@global_strainer.add_filter(filter)
39
44
  end
40
45
 
@@ -49,11 +54,13 @@ module Liquid
49
54
  def invoke(method, *args)
50
55
  if self.class.invokable?(method)
51
56
  send(method, *args)
57
+ elsif @context && @context.strict_filters
58
+ raise Liquid::UndefinedFilter, "undefined filter #{method}"
52
59
  else
53
60
  args.first
54
61
  end
55
62
  rescue ::ArgumentError => e
56
- raise Liquid::ArgumentError.new(e.message)
63
+ raise Liquid::ArgumentError, e.message, e.backtrace
57
64
  end
58
65
  end
59
66
  end
@@ -0,0 +1,62 @@
1
+ module Liquid
2
+ class TablerowloopDrop < Drop
3
+ def initialize(length, cols)
4
+ @length = length
5
+ @row = 1
6
+ @col = 1
7
+ @cols = cols
8
+ @index = 0
9
+ end
10
+
11
+ attr_reader :length, :col, :row
12
+
13
+ def index
14
+ @index + 1
15
+ end
16
+
17
+ def index0
18
+ @index
19
+ end
20
+
21
+ def col0
22
+ @col - 1
23
+ end
24
+
25
+ def rindex
26
+ @length - @index
27
+ end
28
+
29
+ def rindex0
30
+ @length - @index - 1
31
+ end
32
+
33
+ def first
34
+ @index == 0
35
+ end
36
+
37
+ def last
38
+ @index == @length - 1
39
+ end
40
+
41
+ def col_first
42
+ @col == 1
43
+ end
44
+
45
+ def col_last
46
+ @col == @cols
47
+ end
48
+
49
+ protected
50
+
51
+ def increment!
52
+ @index += 1
53
+
54
+ if @col == @cols
55
+ @col = 1
56
+ @row += 1
57
+ else
58
+ @col += 1
59
+ end
60
+ end
61
+ end
62
+ end
data/lib/liquid/tag.rb CHANGED
@@ -1,26 +1,27 @@
1
1
  module Liquid
2
2
  class Tag
3
- attr_accessor :options, :line_number
4
- attr_reader :nodelist, :warnings
3
+ attr_reader :nodelist, :tag_name, :line_number, :parse_context
4
+ alias_method :options, :parse_context
5
5
  include ParserSwitching
6
6
 
7
7
  class << self
8
- def parse(tag_name, markup, tokens, options)
8
+ def parse(tag_name, markup, tokenizer, options)
9
9
  tag = new(tag_name, markup, options)
10
- tag.parse(tokens)
10
+ tag.parse(tokenizer)
11
11
  tag
12
12
  end
13
13
 
14
14
  private :new
15
15
  end
16
16
 
17
- def initialize(tag_name, markup, options)
17
+ def initialize(tag_name, markup, parse_context)
18
18
  @tag_name = tag_name
19
19
  @markup = markup
20
- @options = options
20
+ @parse_context = parse_context
21
+ @line_number = parse_context.line_number
21
22
  end
22
23
 
23
- def parse(tokens)
24
+ def parse(_tokens)
24
25
  end
25
26
 
26
27
  def raw
@@ -31,7 +32,7 @@ module Liquid
31
32
  self.class.name.downcase
32
33
  end
33
34
 
34
- def render(context)
35
+ def render(_context)
35
36
  ''.freeze
36
37
  end
37
38
 
@@ -1,5 +1,4 @@
1
1
  module Liquid
2
-
3
2
  # Assign sets a variable in your template.
4
3
  #
5
4
  # {% assign foo = 'monkey' %}
@@ -15,8 +14,7 @@ module Liquid
15
14
  super
16
15
  if markup =~ Syntax
17
16
  @to = $1
18
- @from = Variable.new($2,options)
19
- @from.line_number = line_number
17
+ @from = Variable.new($2, options)
20
18
  else
21
19
  raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze)
22
20
  end
@@ -25,13 +23,28 @@ module Liquid
25
23
  def render(context)
26
24
  val = @from.render(context)
27
25
  context.scopes.last[@to] = val
28
- context.increment_used_resources(:assign_score_current, val)
26
+ context.resource_limits.assign_score += assign_score_of(val)
29
27
  ''.freeze
30
28
  end
31
29
 
32
30
  def blank?
33
31
  true
34
32
  end
33
+
34
+ private
35
+
36
+ def assign_score_of(val)
37
+ if val.instance_of?(String)
38
+ val.length
39
+ elsif val.instance_of?(Array) || val.instance_of?(Hash)
40
+ sum = 1
41
+ # Uses #each to avoid extra allocations.
42
+ val.each { |child| sum += assign_score_of(child) }
43
+ sum
44
+ else
45
+ 1
46
+ end
47
+ end
35
48
  end
36
49
 
37
50
  Template.register_tag('assign'.freeze, Assign)
@@ -1,5 +1,4 @@
1
1
  module Liquid
2
-
3
2
  # Break tag to be used to break out of a for loop.
4
3
  #
5
4
  # == Basic Usage:
@@ -10,11 +9,9 @@ module Liquid
10
9
  # {% endfor %}
11
10
  #
12
11
  class Break < Tag
13
-
14
12
  def interrupt
15
13
  BreakInterrupt.new
16
14
  end
17
-
18
15
  end
19
16
 
20
17
  Template.register_tag('break'.freeze, Break)
@@ -25,7 +25,7 @@ module Liquid
25
25
  def render(context)
26
26
  output = super
27
27
  context.scopes.last[@to] = output
28
- context.increment_used_resources(:assign_score_current, output)
28
+ context.resource_limits.assign_score += output.length
29
29
  ''.freeze
30
30
  end
31
31
 
@@ -8,18 +8,24 @@ module Liquid
8
8
  @blocks = []
9
9
 
10
10
  if markup =~ Syntax
11
- @left = $1
11
+ @left = Expression.parse($1)
12
12
  else
13
13
  raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze))
14
14
  end
15
15
  end
16
16
 
17
+ def parse(tokens)
18
+ body = BlockBody.new
19
+ while parse_body(body, tokens)
20
+ body = @blocks.last.attachment
21
+ end
22
+ end
23
+
17
24
  def nodelist
18
- @blocks.flat_map(&:attachment)
25
+ @blocks.map(&:attachment)
19
26
  end
20
27
 
21
28
  def unknown_tag(tag, markup, tokens)
22
- @nodelist = []
23
29
  case tag
24
30
  when 'when'.freeze
25
31
  record_when_condition(markup)
@@ -37,10 +43,10 @@ module Liquid
37
43
  output = ''
38
44
  @blocks.each do |block|
39
45
  if block.else?
40
- return render_all(block.attachment, context) if execute_else_block
46
+ return block.attachment.render(context) if execute_else_block
41
47
  elsif block.evaluate(context)
42
48
  execute_else_block = false
43
- output << render_all(block.attachment, context)
49
+ output << block.attachment.render(context)
44
50
  end
45
51
  end
46
52
  output
@@ -50,27 +56,28 @@ module Liquid
50
56
  private
51
57
 
52
58
  def record_when_condition(markup)
59
+ body = BlockBody.new
60
+
53
61
  while markup
54
- # Create a new nodelist and assign it to the new block
55
- if not markup =~ WhenSyntax
62
+ unless markup =~ WhenSyntax
56
63
  raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze))
57
64
  end
58
65
 
59
66
  markup = $2
60
67
 
61
- block = Condition.new(@left, '=='.freeze, $1)
62
- block.attach(@nodelist)
63
- @blocks.push(block)
68
+ block = Condition.new(@left, '=='.freeze, Expression.parse($1))
69
+ block.attach(body)
70
+ @blocks << block
64
71
  end
65
72
  end
66
73
 
67
74
  def record_else_condition(markup)
68
- if not markup.strip.empty?
75
+ unless markup.strip.empty?
69
76
  raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else".freeze))
70
77
  end
71
78
 
72
79
  block = ElseCondition.new
73
- block.attach(@nodelist)
80
+ block.attach(BlockBody.new)
74
81
  @blocks << block
75
82
  end
76
83
  end
@@ -1,10 +1,10 @@
1
1
  module Liquid
2
2
  class Comment < Block
3
- def render(context)
3
+ def render(_context)
4
4
  ''.freeze
5
5
  end
6
6
 
7
- def unknown_tag(tag, markup, tokens)
7
+ def unknown_tag(_tag, _markup, _tokens)
8
8
  end
9
9
 
10
10
  def blank?
@@ -20,10 +20,10 @@ module Liquid
20
20
  case markup
21
21
  when NamedSyntax
22
22
  @variables = variables_from_string($2)
23
- @name = $1
23
+ @name = Expression.parse($1)
24
24
  when SimpleSyntax
25
25
  @variables = variables_from_string(markup)
26
- @name = "'#{@variables.to_s}'"
26
+ @name = @variables.to_s
27
27
  else
28
28
  raise SyntaxError.new(options[:locale].t("errors.syntax.cycle".freeze))
29
29
  end
@@ -33,11 +33,11 @@ module Liquid
33
33
  context.registers[:cycle] ||= Hash.new(0)
34
34
 
35
35
  context.stack do
36
- key = context[@name]
36
+ key = context.evaluate(@name)
37
37
  iteration = context.registers[:cycle][key]
38
- result = context[@variables[iteration]]
38
+ result = context.evaluate(@variables[iteration])
39
39
  iteration += 1
40
- iteration = 0 if iteration >= @variables.size
40
+ iteration = 0 if iteration >= @variables.size
41
41
  context.registers[:cycle][key] = iteration
42
42
  result
43
43
  end
@@ -48,7 +48,7 @@ module Liquid
48
48
  def variables_from_string(markup)
49
49
  markup.split(',').collect do |var|
50
50
  var =~ /\s*(#{QuotedFragment})\s*/o
51
- $1 ? $1 : nil
51
+ $1 ? Expression.parse($1) : nil
52
52
  end.compact
53
53
  end
54
54
  end
@@ -1,5 +1,4 @@
1
1
  module Liquid
2
-
3
2
  # decrement is used in a place where one needs to insert a counter
4
3
  # into a template, and needs the counter to survive across
5
4
  # multiple instantiations of the template.
@@ -26,12 +25,10 @@ module Liquid
26
25
 
27
26
  def render(context)
28
27
  value = context.environments.first[@variable] ||= 0
29
- value = value - 1
28
+ value -= 1
30
29
  context.environments.first[@variable] = value
31
30
  value.to_s
32
31
  end
33
-
34
- private
35
32
  end
36
33
 
37
34
  Template.register_tag('decrement'.freeze, Decrement)
@@ -1,5 +1,4 @@
1
1
  module Liquid
2
-
3
2
  # "For" iterates over an array or collection.
4
3
  # Several useful variables are available to you within the loop.
5
4
  #
@@ -42,85 +41,41 @@ module Liquid
42
41
  # where 0 is the last item.
43
42
  # forloop.first:: Returns true if the item is the first item.
44
43
  # forloop.last:: Returns true if the item is the last item.
44
+ # forloop.parentloop:: Provides access to the parent loop, if present.
45
45
  #
46
46
  class For < Block
47
47
  Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
48
48
 
49
49
  def initialize(tag_name, markup, options)
50
50
  super
51
+ @from = @limit = nil
51
52
  parse_with_selected_parser(markup)
52
- @nodelist = @for_block = []
53
+ @for_block = BlockBody.new
54
+ @else_block = nil
55
+ end
56
+
57
+ def parse(tokens)
58
+ return unless parse_body(@for_block, tokens)
59
+ parse_body(@else_block, tokens)
53
60
  end
54
61
 
55
62
  def nodelist
56
- if @else_block
57
- @for_block + @else_block
58
- else
59
- @for_block
60
- end
63
+ @else_block ? [@for_block, @else_block] : [@for_block]
61
64
  end
62
65
 
63
66
  def unknown_tag(tag, markup, tokens)
64
67
  return super unless tag == 'else'.freeze
65
- @nodelist = @else_block = []
68
+ @else_block = BlockBody.new
66
69
  end
67
70
 
68
71
  def render(context)
69
- context.registers[:for] ||= Hash.new(0)
70
-
71
- collection = context[@collection_name]
72
- collection = collection.to_a if collection.is_a?(Range)
73
-
74
- # Maintains Ruby 1.8.7 String#each behaviour on 1.9
75
- return render_else(context) unless iterable?(collection)
72
+ segment = collection_segment(context)
76
73
 
77
- from = if @attributes['offset'.freeze] == 'continue'.freeze
78
- context.registers[:for][@name].to_i
74
+ if segment.empty?
75
+ render_else(context)
79
76
  else
80
- context[@attributes['offset'.freeze]].to_i
77
+ render_segment(context, segment)
81
78
  end
82
-
83
- limit = context[@attributes['limit'.freeze]]
84
- to = limit ? limit.to_i + from : nil
85
-
86
- segment = Utils.slice_collection(collection, from, to)
87
-
88
- return render_else(context) if segment.empty?
89
-
90
- segment.reverse! if @reversed
91
-
92
- result = ''
93
-
94
- length = segment.length
95
-
96
- # Store our progress through the collection for the continue flag
97
- context.registers[:for][@name] = from + segment.length
98
-
99
- context.stack do
100
- segment.each_with_index do |item, index|
101
- context[@variable_name] = item
102
- context['forloop'.freeze] = {
103
- 'name'.freeze => @name,
104
- 'length'.freeze => length,
105
- 'index'.freeze => index + 1,
106
- 'index0'.freeze => index,
107
- 'rindex'.freeze => length - index,
108
- 'rindex0'.freeze => length - index - 1,
109
- 'first'.freeze => (index == 0),
110
- 'last'.freeze => (index == length - 1)
111
- }
112
-
113
- result << render_all(@for_block, context)
114
-
115
- # Handle any interrupts if they exist.
116
- if context.has_interrupt?
117
- interrupt = context.pop_interrupt
118
- break if interrupt.is_a? BreakInterrupt
119
- next if interrupt.is_a? ContinueInterrupt
120
- end
121
- end
122
- end
123
- result
124
79
  end
125
80
 
126
81
  protected
@@ -128,12 +83,12 @@ module Liquid
128
83
  def lax_parse(markup)
129
84
  if markup =~ Syntax
130
85
  @variable_name = $1
131
- @collection_name = $2
132
- @name = "#{$1}-#{$2}"
133
- @reversed = $3
134
- @attributes = {}
86
+ collection_name = $2
87
+ @reversed = !!$3
88
+ @name = "#{@variable_name}-#{collection_name}"
89
+ @collection_name = Expression.parse(collection_name)
135
90
  markup.scan(TagAttributes) do |key, value|
136
- @attributes[key] = value
91
+ set_attribute(key, value)
137
92
  end
138
93
  else
139
94
  raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze))
@@ -143,31 +98,96 @@ module Liquid
143
98
  def strict_parse(markup)
144
99
  p = Parser.new(markup)
145
100
  @variable_name = p.consume(:id)
146
- raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze)
147
- @collection_name = p.expression
148
- @name = "#{@variable_name}-#{@collection_name}"
101
+ raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze)
102
+ collection_name = p.expression
103
+ @name = "#{@variable_name}-#{collection_name}"
104
+ @collection_name = Expression.parse(collection_name)
149
105
  @reversed = p.id?('reversed'.freeze)
150
106
 
151
- @attributes = {}
152
107
  while p.look(:id) && p.look(:colon, 1)
153
108
  unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze)
154
109
  raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".freeze))
155
110
  end
156
111
  p.consume
157
- val = p.expression
158
- @attributes[attribute] = val
112
+ set_attribute(attribute, p.expression)
159
113
  end
160
114
  p.consume(:end_of_string)
161
115
  end
162
116
 
163
117
  private
164
118
 
165
- def render_else(context)
166
- return @else_block ? [render_all(@else_block, context)] : ''.freeze
119
+ def collection_segment(context)
120
+ offsets = context.registers[:for] ||= Hash.new(0)
121
+
122
+ from = if @from == :continue
123
+ offsets[@name].to_i
124
+ else
125
+ context.evaluate(@from).to_i
126
+ end
127
+
128
+ collection = context.evaluate(@collection_name)
129
+ collection = collection.to_a if collection.is_a?(Range)
130
+
131
+ limit = context.evaluate(@limit)
132
+ to = limit ? limit.to_i + from : nil
133
+
134
+ segment = Utils.slice_collection(collection, from, to)
135
+ segment.reverse! if @reversed
136
+
137
+ offsets[@name] = from + segment.length
138
+
139
+ segment
167
140
  end
168
141
 
169
- def iterable?(collection)
170
- collection.respond_to?(:each) || Utils.non_blank_string?(collection)
142
+ def render_segment(context, segment)
143
+ for_stack = context.registers[:for_stack] ||= []
144
+ length = segment.length
145
+
146
+ result = ''
147
+
148
+ context.stack do
149
+ loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
150
+
151
+ for_stack.push(loop_vars)
152
+
153
+ begin
154
+ context['forloop'.freeze] = loop_vars
155
+
156
+ segment.each_with_index do |item, index|
157
+ context[@variable_name] = item
158
+ result << @for_block.render(context)
159
+ loop_vars.send(:increment!)
160
+
161
+ # Handle any interrupts if they exist.
162
+ if context.interrupt?
163
+ interrupt = context.pop_interrupt
164
+ break if interrupt.is_a? BreakInterrupt
165
+ next if interrupt.is_a? ContinueInterrupt
166
+ end
167
+ end
168
+ ensure
169
+ for_stack.pop
170
+ end
171
+ end
172
+
173
+ result
174
+ end
175
+
176
+ def set_attribute(key, expr)
177
+ case key
178
+ when 'offset'.freeze
179
+ @from = if expr == 'continue'.freeze
180
+ :continue
181
+ else
182
+ Expression.parse(expr)
183
+ end
184
+ when 'limit'.freeze
185
+ @limit = Expression.parse(expr)
186
+ end
187
+ end
188
+
189
+ def render_else(context)
190
+ @else_block ? @else_block.render(context) : ''.freeze
171
191
  end
172
192
  end
173
193