liquid 4.0.1 → 5.4.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.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +142 -0
  3. data/README.md +10 -4
  4. data/lib/liquid/block.rb +31 -14
  5. data/lib/liquid/block_body.rb +169 -56
  6. data/lib/liquid/condition.rb +59 -23
  7. data/lib/liquid/context.rb +111 -52
  8. data/lib/liquid/document.rb +47 -9
  9. data/lib/liquid/drop.rb +4 -2
  10. data/lib/liquid/errors.rb +20 -18
  11. data/lib/liquid/expression.rb +29 -33
  12. data/lib/liquid/extensions.rb +2 -0
  13. data/lib/liquid/file_system.rb +6 -4
  14. data/lib/liquid/forloop_drop.rb +54 -4
  15. data/lib/liquid/i18n.rb +5 -3
  16. data/lib/liquid/interrupts.rb +3 -1
  17. data/lib/liquid/lexer.rb +31 -24
  18. data/lib/liquid/locales/en.yml +8 -5
  19. data/lib/liquid/parse_context.rb +20 -4
  20. data/lib/liquid/parse_tree_visitor.rb +42 -0
  21. data/lib/liquid/parser.rb +30 -18
  22. data/lib/liquid/parser_switching.rb +17 -3
  23. data/lib/liquid/partial_cache.rb +24 -0
  24. data/lib/liquid/profiler/hooks.rb +26 -14
  25. data/lib/liquid/profiler.rb +67 -86
  26. data/lib/liquid/range_lookup.rb +13 -3
  27. data/lib/liquid/registers.rb +51 -0
  28. data/lib/liquid/resource_limits.rb +47 -8
  29. data/lib/liquid/standardfilters.rb +616 -129
  30. data/lib/liquid/strainer_factory.rb +41 -0
  31. data/lib/liquid/strainer_template.rb +62 -0
  32. data/lib/liquid/tablerowloop_drop.rb +64 -5
  33. data/lib/liquid/tag/disableable.rb +22 -0
  34. data/lib/liquid/tag/disabler.rb +21 -0
  35. data/lib/liquid/tag.rb +28 -6
  36. data/lib/liquid/tags/assign.rb +44 -18
  37. data/lib/liquid/tags/break.rb +16 -3
  38. data/lib/liquid/tags/capture.rb +24 -18
  39. data/lib/liquid/tags/case.rb +69 -27
  40. data/lib/liquid/tags/comment.rb +18 -3
  41. data/lib/liquid/tags/continue.rb +16 -12
  42. data/lib/liquid/tags/cycle.rb +45 -25
  43. data/lib/liquid/tags/decrement.rb +22 -20
  44. data/lib/liquid/tags/echo.rb +41 -0
  45. data/lib/liquid/tags/for.rb +97 -89
  46. data/lib/liquid/tags/if.rb +61 -35
  47. data/lib/liquid/tags/ifchanged.rb +11 -10
  48. data/lib/liquid/tags/include.rb +56 -56
  49. data/lib/liquid/tags/increment.rb +23 -17
  50. data/lib/liquid/tags/inline_comment.rb +43 -0
  51. data/lib/liquid/tags/raw.rb +25 -11
  52. data/lib/liquid/tags/render.rb +109 -0
  53. data/lib/liquid/tags/table_row.rb +53 -19
  54. data/lib/liquid/tags/unless.rb +38 -19
  55. data/lib/liquid/template.rb +52 -72
  56. data/lib/liquid/template_factory.rb +9 -0
  57. data/lib/liquid/tokenizer.rb +18 -10
  58. data/lib/liquid/usage.rb +8 -0
  59. data/lib/liquid/utils.rb +13 -3
  60. data/lib/liquid/variable.rb +52 -41
  61. data/lib/liquid/variable_lookup.rb +24 -10
  62. data/lib/liquid/version.rb +3 -1
  63. data/lib/liquid.rb +19 -6
  64. metadata +21 -104
  65. data/lib/liquid/strainer.rb +0 -66
  66. data/test/fixtures/en_locale.yml +0 -9
  67. data/test/integration/assign_test.rb +0 -48
  68. data/test/integration/blank_test.rb +0 -106
  69. data/test/integration/block_test.rb +0 -12
  70. data/test/integration/capture_test.rb +0 -50
  71. data/test/integration/context_test.rb +0 -32
  72. data/test/integration/document_test.rb +0 -19
  73. data/test/integration/drop_test.rb +0 -273
  74. data/test/integration/error_handling_test.rb +0 -260
  75. data/test/integration/filter_test.rb +0 -178
  76. data/test/integration/hash_ordering_test.rb +0 -23
  77. data/test/integration/output_test.rb +0 -123
  78. data/test/integration/parsing_quirks_test.rb +0 -122
  79. data/test/integration/render_profiling_test.rb +0 -154
  80. data/test/integration/security_test.rb +0 -80
  81. data/test/integration/standard_filter_test.rb +0 -626
  82. data/test/integration/tags/break_tag_test.rb +0 -15
  83. data/test/integration/tags/continue_tag_test.rb +0 -15
  84. data/test/integration/tags/for_tag_test.rb +0 -410
  85. data/test/integration/tags/if_else_tag_test.rb +0 -188
  86. data/test/integration/tags/include_tag_test.rb +0 -245
  87. data/test/integration/tags/increment_tag_test.rb +0 -23
  88. data/test/integration/tags/raw_tag_test.rb +0 -31
  89. data/test/integration/tags/standard_tag_test.rb +0 -296
  90. data/test/integration/tags/statements_test.rb +0 -111
  91. data/test/integration/tags/table_row_test.rb +0 -64
  92. data/test/integration/tags/unless_else_tag_test.rb +0 -26
  93. data/test/integration/template_test.rb +0 -332
  94. data/test/integration/trim_mode_test.rb +0 -529
  95. data/test/integration/variable_test.rb +0 -96
  96. data/test/test_helper.rb +0 -116
  97. data/test/unit/block_unit_test.rb +0 -58
  98. data/test/unit/condition_unit_test.rb +0 -166
  99. data/test/unit/context_unit_test.rb +0 -489
  100. data/test/unit/file_system_unit_test.rb +0 -35
  101. data/test/unit/i18n_unit_test.rb +0 -37
  102. data/test/unit/lexer_unit_test.rb +0 -51
  103. data/test/unit/parser_unit_test.rb +0 -82
  104. data/test/unit/regexp_unit_test.rb +0 -44
  105. data/test/unit/strainer_unit_test.rb +0 -164
  106. data/test/unit/tag_unit_test.rb +0 -21
  107. data/test/unit/tags/case_tag_unit_test.rb +0 -10
  108. data/test/unit/tags/for_tag_unit_test.rb +0 -13
  109. data/test/unit/tags/if_tag_unit_test.rb +0 -8
  110. data/test/unit/template_unit_test.rb +0 -78
  111. data/test/unit/tokenizer_unit_test.rb +0 -55
  112. data/test/unit/variable_unit_test.rb +0 -162
@@ -1,7 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
4
+ # @liquid_public_docs
5
+ # @liquid_type tag
6
+ # @liquid_category syntax
7
+ # @liquid_name comment
8
+ # @liquid_summary
9
+ # Prevents an expression from being rendered or output.
10
+ # @liquid_description
11
+ # Any text inside `comment` tags won't be output, and any Liquid code won't be rendered.
12
+ # @liquid_syntax
13
+ # {% comment %}
14
+ # content
15
+ # {% endcomment %}
16
+ # @liquid_syntax_keyword content The content of the comment.
2
17
  class Comment < Block
3
- def render(_context)
4
- ''.freeze
18
+ def render_to_output_buffer(_context, output)
19
+ output
5
20
  end
6
21
 
7
22
  def unknown_tag(_tag, _markup, _tokens)
@@ -12,5 +27,5 @@ module Liquid
12
27
  end
13
28
  end
14
29
 
15
- Template.register_tag('comment'.freeze, Comment)
30
+ Template.register_tag('comment', Comment)
16
31
  end
@@ -1,18 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
- # Continue tag to be used to break out of a for loop.
3
- #
4
- # == Basic Usage:
5
- # {% for item in collection %}
6
- # {% if item.condition %}
7
- # {% continue %}
8
- # {% endif %}
9
- # {% endfor %}
10
- #
4
+ # @liquid_public_docs
5
+ # @liquid_type tag
6
+ # @liquid_category iteration
7
+ # @liquid_name continue
8
+ # @liquid_summary
9
+ # Causes a [`for` loop](/api/liquid/tags#for) to skip to the next iteration.
10
+ # @liquid_syntax
11
+ # {% continue %}
11
12
  class Continue < Tag
12
- def interrupt
13
- ContinueInterrupt.new
13
+ INTERRUPT = ContinueInterrupt.new.freeze
14
+
15
+ def render_to_output_buffer(context, output)
16
+ context.push_interrupt(INTERRUPT)
17
+ output
14
18
  end
15
19
  end
16
20
 
17
- Template.register_tag('continue'.freeze, Continue)
21
+ Template.register_tag('continue', Continue)
18
22
  end
@@ -1,46 +1,60 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
- # Cycle is usually used within a loop to alternate between values, like colors or DOM classes.
3
- #
4
- # {% for item in items %}
5
- # <div class="{% cycle 'red', 'green', 'blue' %}"> {{ item }} </div>
6
- # {% end %}
7
- #
8
- # <div class="red"> Item one </div>
9
- # <div class="green"> Item two </div>
10
- # <div class="blue"> Item three </div>
11
- # <div class="red"> Item four </div>
12
- # <div class="green"> Item five</div>
4
+ # @liquid_public_docs
5
+ # @liquid_type tag
6
+ # @liquid_category iteration
7
+ # @liquid_name cycle
8
+ # @liquid_summary
9
+ # Loops through a group of strings and outputs them one at a time for each iteration of a [`for` loop](/api/liquid/tags#for).
10
+ # @liquid_description
11
+ # The `cycle` tag must be used inside a `for` loop.
13
12
  #
13
+ # > Tip:
14
+ # > Use the `cycle` tag to output text in a predictable pattern. For example, to apply odd/even classes to rows in a table.
15
+ # @liquid_syntax
16
+ # {% cycle string, string, ... %}
14
17
  class Cycle < Tag
15
18
  SimpleSyntax = /\A#{QuotedFragment}+/o
16
19
  NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
17
20
 
21
+ attr_reader :variables
22
+
18
23
  def initialize(tag_name, markup, options)
19
24
  super
20
25
  case markup
21
26
  when NamedSyntax
22
- @variables = variables_from_string($2)
23
- @name = Expression.parse($1)
27
+ @variables = variables_from_string(Regexp.last_match(2))
28
+ @name = parse_expression(Regexp.last_match(1))
24
29
  when SimpleSyntax
25
30
  @variables = variables_from_string(markup)
26
- @name = @variables.to_s
31
+ @name = @variables.to_s
27
32
  else
28
- raise SyntaxError.new(options[:locale].t("errors.syntax.cycle".freeze))
33
+ raise SyntaxError, options[:locale].t("errors.syntax.cycle")
29
34
  end
30
35
  end
31
36
 
32
- def render(context)
37
+ def render_to_output_buffer(context, output)
33
38
  context.registers[:cycle] ||= {}
34
39
 
35
- context.stack do
36
- key = context.evaluate(@name)
37
- iteration = context.registers[:cycle][key].to_i
38
- result = context.evaluate(@variables[iteration])
39
- iteration += 1
40
- iteration = 0 if iteration >= @variables.size
41
- context.registers[:cycle][key] = iteration
42
- result
40
+ key = context.evaluate(@name)
41
+ iteration = context.registers[:cycle][key].to_i
42
+
43
+ val = context.evaluate(@variables[iteration])
44
+
45
+ if val.is_a?(Array)
46
+ val = val.join
47
+ elsif !val.is_a?(String)
48
+ val = val.to_s
43
49
  end
50
+
51
+ output << val
52
+
53
+ iteration += 1
54
+ iteration = 0 if iteration >= @variables.size
55
+
56
+ context.registers[:cycle][key] = iteration
57
+ output
44
58
  end
45
59
 
46
60
  private
@@ -48,9 +62,15 @@ module Liquid
48
62
  def variables_from_string(markup)
49
63
  markup.split(',').collect do |var|
50
64
  var =~ /\s*(#{QuotedFragment})\s*/o
51
- $1 ? Expression.parse($1) : nil
65
+ Regexp.last_match(1) ? parse_expression(Regexp.last_match(1)) : nil
52
66
  end.compact
53
67
  end
68
+
69
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
70
+ def children
71
+ Array(@node.variables)
72
+ end
73
+ end
54
74
  end
55
75
 
56
76
  Template.register_tag('cycle', Cycle)
@@ -1,35 +1,37 @@
1
- module Liquid
2
- # decrement is used in a place where one needs to insert a counter
3
- # into a template, and needs the counter to survive across
4
- # multiple instantiations of the template.
5
- # NOTE: decrement is a pre-decrement, --i,
6
- # while increment is post: i++.
7
- #
8
- # (To achieve the survival, the application must keep the context)
9
- #
10
- # if the variable does not exist, it is created with value 0.
1
+ # frozen_string_literal: true
11
2
 
12
- # Hello: {% decrement variable %}
13
- #
14
- # gives you:
15
- #
16
- # Hello: -1
17
- # Hello: -2
18
- # Hello: -3
3
+ module Liquid
4
+ # @liquid_public_docs
5
+ # @liquid_type tag
6
+ # @liquid_category variable
7
+ # @liquid_name decrement
8
+ # @liquid_summary
9
+ # Creates a new variable, with a default value of -1, that's decreased by 1 with each subsequent call.
10
+ # @liquid_description
11
+ # Variables that are declared with `decrement` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
12
+ # or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
13
+ # [snippets](/themes/architecture#snippets) included in the file.
19
14
  #
15
+ # Similarly, variables that are created with `decrement` are independent from those created with [`assign`](/api/liquid/tags#assign)
16
+ # and [`capture`](/api/liquid/tags#capture). However, `decrement` and [`increment`](/api/liquid/tags#increment) share
17
+ # variables.
18
+ # @liquid_syntax
19
+ # {% decrement variable_name %}
20
+ # @liquid_syntax_keyword variable_name The name of the variable being decremented.
20
21
  class Decrement < Tag
21
22
  def initialize(tag_name, markup, options)
22
23
  super
23
24
  @variable = markup.strip
24
25
  end
25
26
 
26
- def render(context)
27
+ def render_to_output_buffer(context, output)
27
28
  value = context.environments.first[@variable] ||= 0
28
29
  value -= 1
29
30
  context.environments.first[@variable] = value
30
- value.to_s
31
+ output << value.to_s
32
+ output
31
33
  end
32
34
  end
33
35
 
34
- Template.register_tag('decrement'.freeze, Decrement)
36
+ Template.register_tag('decrement', Decrement)
35
37
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ # @liquid_public_docs
5
+ # @liquid_type tag
6
+ # @liquid_category syntax
7
+ # @liquid_name echo
8
+ # @liquid_summary
9
+ # Outputs an expression.
10
+ # @liquid_description
11
+ # Using the `echo` tag is the same as wrapping an expression in curly brackets (`{{` and `}}`). However, unlike the curly
12
+ # bracket method, you can use the `echo` tag inside [`liquid` tags](/api/liquid/tags#liquid).
13
+ #
14
+ # > Tip:
15
+ # > You can use [filters](/api/liquid/filters) on expressions inside `echo` tags.
16
+ # @liquid_syntax
17
+ # {% liquid
18
+ # echo expression
19
+ # %}
20
+ # @liquid_syntax_keyword expression The expression to be output.
21
+ class Echo < Tag
22
+ attr_reader :variable
23
+
24
+ def initialize(tag_name, markup, parse_context)
25
+ super
26
+ @variable = Variable.new(markup, parse_context)
27
+ end
28
+
29
+ def render(context)
30
+ @variable.render_to_output_buffer(context, +'')
31
+ end
32
+
33
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
34
+ def children
35
+ [@node.variable]
36
+ end
37
+ end
38
+ end
39
+
40
+ Template.register_tag('echo', Echo)
41
+ end
@@ -1,65 +1,52 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
- # "For" iterates over an array or collection.
3
- # Several useful variables are available to you within the loop.
4
- #
5
- # == Basic usage:
6
- # {% for item in collection %}
7
- # {{ forloop.index }}: {{ item.name }}
8
- # {% endfor %}
9
- #
10
- # == Advanced usage:
11
- # {% for item in collection %}
12
- # <div {% if forloop.first %}class="first"{% endif %}>
13
- # Item {{ forloop.index }}: {{ item.name }}
14
- # </div>
15
- # {% else %}
16
- # There is nothing in the collection.
17
- # {% endfor %}
18
- #
19
- # You can also define a limit and offset much like SQL. Remember
20
- # that offset starts at 0 for the first item.
21
- #
22
- # {% for item in collection limit:5 offset:10 %}
23
- # {{ item.name }}
24
- # {% end %}
25
- #
26
- # To reverse the for loop simply use {% for item in collection reversed %} (note that the flag's spelling is different to the filter `reverse`)
27
- #
28
- # == Available variables:
29
- #
30
- # forloop.name:: 'item-collection'
31
- # forloop.length:: Length of the loop
32
- # forloop.index:: The current item's position in the collection;
33
- # forloop.index starts at 1.
34
- # This is helpful for non-programmers who start believe
35
- # the first item in an array is 1, not 0.
36
- # forloop.index0:: The current item's position in the collection
37
- # where the first item is 0
38
- # forloop.rindex:: Number of items remaining in the loop
39
- # (length - index) where 1 is the last item.
40
- # forloop.rindex0:: Number of items remaining in the loop
41
- # where 0 is the last item.
42
- # forloop.first:: Returns true if the item is the first item.
43
- # forloop.last:: Returns true if the item is the last item.
44
- # forloop.parentloop:: Provides access to the parent loop, if present.
4
+ # @liquid_public_docs
5
+ # @liquid_type tag
6
+ # @liquid_category iteration
7
+ # @liquid_name for
8
+ # @liquid_summary
9
+ # Renders an expression for every item in an array.
10
+ # @liquid_description
11
+ # You can do a maximum of 50 iterations with a `for` loop. If you need to iterate over more than 50 items, then use the
12
+ # [`paginate` tag](/api/liquid/tags#paginate) to split the items over multiple pages.
45
13
  #
14
+ # > Tip:
15
+ # > Every `for` loop has an associated [`forloop` object](/api/liquid/objects#forloop) with information about the loop.
16
+ # @liquid_syntax
17
+ # {% for variable in array %}
18
+ # expression
19
+ # {% endfor %}
20
+ # @liquid_syntax_keyword variable The current item in the array.
21
+ # @liquid_syntax_keyword array The array to iterate over.
22
+ # @liquid_syntax_keyword expression The expression to render for each iteration.
23
+ # @liquid_optional_param limit [number] The number of iterations to perform.
24
+ # @liquid_optional_param offset [number] The 1-based index to start iterating at.
25
+ # @liquid_optional_param range [untyped] A custom numeric range to iterate over.
26
+ # @liquid_optional_param reversed [untyped] Iterate in reverse order.
46
27
  class For < Block
47
28
  Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
48
29
 
49
- attr_reader :collection_name
50
- attr_reader :variable_name
30
+ attr_reader :collection_name, :variable_name, :limit, :from
51
31
 
52
32
  def initialize(tag_name, markup, options)
53
33
  super
54
34
  @from = @limit = nil
55
35
  parse_with_selected_parser(markup)
56
- @for_block = BlockBody.new
36
+ @for_block = new_body
57
37
  @else_block = nil
58
38
  end
59
39
 
60
40
  def parse(tokens)
61
- return unless parse_body(@for_block, tokens)
62
- parse_body(@else_block, tokens)
41
+ if parse_body(@for_block, tokens)
42
+ parse_body(@else_block, tokens)
43
+ end
44
+ if blank?
45
+ @else_block&.remove_blank_strings
46
+ @for_block.remove_blank_strings
47
+ end
48
+ @else_block&.freeze
49
+ @for_block.freeze
63
50
  end
64
51
 
65
52
  def nodelist
@@ -67,49 +54,53 @@ module Liquid
67
54
  end
68
55
 
69
56
  def unknown_tag(tag, markup, tokens)
70
- return super unless tag == 'else'.freeze
71
- @else_block = BlockBody.new
57
+ return super unless tag == 'else'
58
+ @else_block = new_body
72
59
  end
73
60
 
74
- def render(context)
61
+ def render_to_output_buffer(context, output)
75
62
  segment = collection_segment(context)
76
63
 
77
64
  if segment.empty?
78
- render_else(context)
65
+ render_else(context, output)
79
66
  else
80
- render_segment(context, segment)
67
+ render_segment(context, output, segment)
81
68
  end
69
+
70
+ output
82
71
  end
83
72
 
84
73
  protected
85
74
 
86
75
  def lax_parse(markup)
87
76
  if markup =~ Syntax
88
- @variable_name = $1
89
- collection_name = $2
90
- @reversed = !!$3
91
- @name = "#{@variable_name}-#{collection_name}"
92
- @collection_name = Expression.parse(collection_name)
77
+ @variable_name = Regexp.last_match(1)
78
+ collection_name = Regexp.last_match(2)
79
+ @reversed = !!Regexp.last_match(3)
80
+ @name = "#{@variable_name}-#{collection_name}"
81
+ @collection_name = parse_expression(collection_name)
93
82
  markup.scan(TagAttributes) do |key, value|
94
83
  set_attribute(key, value)
95
84
  end
96
85
  else
97
- raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze))
86
+ raise SyntaxError, options[:locale].t("errors.syntax.for")
98
87
  end
99
88
  end
100
89
 
101
90
  def strict_parse(markup)
102
91
  p = Parser.new(markup)
103
92
  @variable_name = p.consume(:id)
104
- raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze)
105
- collection_name = p.expression
106
- @name = "#{@variable_name}-#{collection_name}"
107
- @collection_name = Expression.parse(collection_name)
108
- @reversed = p.id?('reversed'.freeze)
93
+ raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
94
+
95
+ collection_name = p.expression
96
+ @collection_name = parse_expression(collection_name)
97
+
98
+ @name = "#{@variable_name}-#{collection_name}"
99
+ @reversed = p.id?('reversed')
109
100
 
110
101
  while p.look(:id) && p.look(:colon, 1)
111
- unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze)
112
- raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".freeze))
102
+ unless (attribute = p.id?('limit') || p.id?('offset'))
103
+ raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
113
104
  end
114
105
  p.consume
115
106
  set_attribute(attribute, p.expression)
@@ -125,14 +116,23 @@ module Liquid
125
116
  from = if @from == :continue
126
117
  offsets[@name].to_i
127
118
  else
128
- context.evaluate(@from).to_i
119
+ from_value = context.evaluate(@from)
120
+ if from_value.nil?
121
+ 0
122
+ else
123
+ Utils.to_integer(from_value)
124
+ end
129
125
  end
130
126
 
131
127
  collection = context.evaluate(@collection_name)
132
128
  collection = collection.to_a if collection.is_a?(Range)
133
129
 
134
- limit = context.evaluate(@limit)
135
- to = limit ? limit.to_i + from : nil
130
+ limit_value = context.evaluate(@limit)
131
+ to = if limit_value.nil?
132
+ nil
133
+ else
134
+ Utils.to_integer(limit_value) + from
135
+ end
136
136
 
137
137
  segment = Utils.slice_collection(collection, from, to)
138
138
  segment.reverse! if @reversed
@@ -142,11 +142,9 @@ module Liquid
142
142
  segment
143
143
  end
144
144
 
145
- def render_segment(context, segment)
145
+ def render_segment(context, output, segment)
146
146
  for_stack = context.registers[:for_stack] ||= []
147
- length = segment.length
148
-
149
- result = ''
147
+ length = segment.length
150
148
 
151
149
  context.stack do
152
150
  loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
@@ -154,45 +152,55 @@ module Liquid
154
152
  for_stack.push(loop_vars)
155
153
 
156
154
  begin
157
- context['forloop'.freeze] = loop_vars
155
+ context['forloop'] = loop_vars
158
156
 
159
157
  segment.each do |item|
160
158
  context[@variable_name] = item
161
- result << @for_block.render(context)
159
+ @for_block.render_to_output_buffer(context, output)
162
160
  loop_vars.send(:increment!)
163
161
 
164
162
  # Handle any interrupts if they exist.
165
- if context.interrupt?
166
- interrupt = context.pop_interrupt
167
- break if interrupt.is_a? BreakInterrupt
168
- next if interrupt.is_a? ContinueInterrupt
169
- end
163
+ next unless context.interrupt?
164
+ interrupt = context.pop_interrupt
165
+ break if interrupt.is_a?(BreakInterrupt)
166
+ next if interrupt.is_a?(ContinueInterrupt)
170
167
  end
171
168
  ensure
172
169
  for_stack.pop
173
170
  end
174
171
  end
175
172
 
176
- result
173
+ output
177
174
  end
178
175
 
179
176
  def set_attribute(key, expr)
180
177
  case key
181
- when 'offset'.freeze
182
- @from = if expr == 'continue'.freeze
178
+ when 'offset'
179
+ @from = if expr == 'continue'
180
+ Usage.increment('for_offset_continue')
183
181
  :continue
184
182
  else
185
- Expression.parse(expr)
183
+ parse_expression(expr)
186
184
  end
187
- when 'limit'.freeze
188
- @limit = Expression.parse(expr)
185
+ when 'limit'
186
+ @limit = parse_expression(expr)
189
187
  end
190
188
  end
191
189
 
192
- def render_else(context)
193
- @else_block ? @else_block.render(context) : ''.freeze
190
+ def render_else(context, output)
191
+ if @else_block
192
+ @else_block.render_to_output_buffer(context, output)
193
+ else
194
+ output
195
+ end
196
+ end
197
+
198
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
199
+ def children
200
+ (super + [@node.limit, @node.from, @node.collection_name]).compact
201
+ end
194
202
  end
195
203
  end
196
204
 
197
- Template.register_tag('for'.freeze, For)
205
+ Template.register_tag('for', For)
198
206
  end