liquid 3.0.6 → 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 (113) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +243 -58
  3. data/README.md +43 -4
  4. data/lib/liquid/block.rb +57 -123
  5. data/lib/liquid/block_body.rb +217 -85
  6. data/lib/liquid/condition.rb +92 -45
  7. data/lib/liquid/context.rb +154 -89
  8. data/lib/liquid/document.rb +57 -9
  9. data/lib/liquid/drop.rb +20 -17
  10. data/lib/liquid/errors.rb +27 -29
  11. data/lib/liquid/expression.rb +32 -20
  12. data/lib/liquid/extensions.rb +21 -7
  13. data/lib/liquid/file_system.rb +17 -15
  14. data/lib/liquid/forloop_drop.rb +92 -0
  15. data/lib/liquid/i18n.rb +10 -8
  16. data/lib/liquid/interrupts.rb +4 -3
  17. data/lib/liquid/lexer.rb +37 -26
  18. data/lib/liquid/locales/en.yml +13 -6
  19. data/lib/liquid/parse_context.rb +54 -0
  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 +20 -6
  23. data/lib/liquid/partial_cache.rb +24 -0
  24. data/lib/liquid/profiler/hooks.rb +26 -14
  25. data/lib/liquid/profiler.rb +72 -92
  26. data/lib/liquid/range_lookup.rb +28 -3
  27. data/lib/liquid/registers.rb +51 -0
  28. data/lib/liquid/resource_limits.rb +62 -0
  29. data/lib/liquid/standardfilters.rb +715 -132
  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 +121 -0
  33. data/lib/liquid/tag/disableable.rb +22 -0
  34. data/lib/liquid/tag/disabler.rb +21 -0
  35. data/lib/liquid/tag.rb +35 -12
  36. data/lib/liquid/tags/assign.rb +57 -18
  37. data/lib/liquid/tags/break.rb +15 -5
  38. data/lib/liquid/tags/capture.rb +24 -18
  39. data/lib/liquid/tags/case.rb +79 -30
  40. data/lib/liquid/tags/comment.rb +19 -4
  41. data/lib/liquid/tags/continue.rb +16 -12
  42. data/lib/liquid/tags/cycle.rb +47 -27
  43. data/lib/liquid/tags/decrement.rb +23 -24
  44. data/lib/liquid/tags/echo.rb +41 -0
  45. data/lib/liquid/tags/for.rb +155 -124
  46. data/lib/liquid/tags/if.rb +97 -63
  47. data/lib/liquid/tags/ifchanged.rb +11 -12
  48. data/lib/liquid/tags/include.rb +82 -73
  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 +50 -8
  52. data/lib/liquid/tags/render.rb +109 -0
  53. data/lib/liquid/tags/table_row.rb +57 -41
  54. data/lib/liquid/tags/unless.rb +38 -20
  55. data/lib/liquid/template.rb +71 -103
  56. data/lib/liquid/template_factory.rb +9 -0
  57. data/lib/liquid/tokenizer.rb +39 -0
  58. data/lib/liquid/usage.rb +8 -0
  59. data/lib/liquid/utils.rb +63 -9
  60. data/lib/liquid/variable.rb +74 -56
  61. data/lib/liquid/variable_lookup.rb +31 -15
  62. data/lib/liquid/version.rb +3 -1
  63. data/lib/liquid.rb +27 -12
  64. metadata +30 -106
  65. data/lib/liquid/module_ex.rb +0 -62
  66. data/lib/liquid/strainer.rb +0 -59
  67. data/lib/liquid/token.rb +0 -18
  68. data/test/fixtures/en_locale.yml +0 -9
  69. data/test/integration/assign_test.rb +0 -48
  70. data/test/integration/blank_test.rb +0 -106
  71. data/test/integration/capture_test.rb +0 -50
  72. data/test/integration/context_test.rb +0 -32
  73. data/test/integration/drop_test.rb +0 -271
  74. data/test/integration/error_handling_test.rb +0 -207
  75. data/test/integration/filter_test.rb +0 -138
  76. data/test/integration/hash_ordering_test.rb +0 -23
  77. data/test/integration/output_test.rb +0 -124
  78. data/test/integration/parsing_quirks_test.rb +0 -116
  79. data/test/integration/render_profiling_test.rb +0 -154
  80. data/test/integration/security_test.rb +0 -64
  81. data/test/integration/standard_filter_test.rb +0 -396
  82. data/test/integration/tags/break_tag_test.rb +0 -16
  83. data/test/integration/tags/continue_tag_test.rb +0 -16
  84. data/test/integration/tags/for_tag_test.rb +0 -375
  85. data/test/integration/tags/if_else_tag_test.rb +0 -190
  86. data/test/integration/tags/include_tag_test.rb +0 -234
  87. data/test/integration/tags/increment_tag_test.rb +0 -24
  88. data/test/integration/tags/raw_tag_test.rb +0 -25
  89. data/test/integration/tags/standard_tag_test.rb +0 -297
  90. data/test/integration/tags/statements_test.rb +0 -113
  91. data/test/integration/tags/table_row_test.rb +0 -63
  92. data/test/integration/tags/unless_else_tag_test.rb +0 -26
  93. data/test/integration/template_test.rb +0 -182
  94. data/test/integration/variable_test.rb +0 -82
  95. data/test/test_helper.rb +0 -89
  96. data/test/unit/block_unit_test.rb +0 -55
  97. data/test/unit/condition_unit_test.rb +0 -149
  98. data/test/unit/context_unit_test.rb +0 -492
  99. data/test/unit/file_system_unit_test.rb +0 -35
  100. data/test/unit/i18n_unit_test.rb +0 -37
  101. data/test/unit/lexer_unit_test.rb +0 -48
  102. data/test/unit/module_ex_unit_test.rb +0 -87
  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 -69
  106. data/test/unit/tag_unit_test.rb +0 -16
  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 -69
  111. data/test/unit/tokenizer_unit_test.rb +0 -38
  112. data/test/unit/variable_unit_test.rb +0 -145
  113. /data/{MIT-LICENSE → LICENSE} +0 -0
@@ -1,10 +1,25 @@
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
- def unknown_tag(tag, markup, tokens)
22
+ def unknown_tag(_tag, _markup, _tokens)
8
23
  end
9
24
 
10
25
  def blank?
@@ -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 = $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)
33
- context.registers[:cycle] ||= Hash.new(0)
34
-
35
- context.stack do
36
- key = context[@name]
37
- iteration = context.registers[:cycle][key]
38
- result = context[@variables[iteration]]
39
- iteration += 1
40
- iteration = 0 if iteration >= @variables.size
41
- context.registers[:cycle][key] = iteration
42
- result
37
+ def render_to_output_buffer(context, output)
38
+ context.registers[:cycle] ||= {}
39
+
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 ? $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,38 +1,37 @@
1
- module Liquid
2
-
3
- # decrement is used in a place where one needs to insert a counter
4
- # into a template, and needs the counter to survive across
5
- # multiple instantiations of the template.
6
- # NOTE: decrement is a pre-decrement, --i,
7
- # while increment is post: i++.
8
- #
9
- # (To achieve the survival, the application must keep the context)
10
- #
11
- # if the variable does not exist, it is created with value 0.
1
+ # frozen_string_literal: true
12
2
 
13
- # Hello: {% decrement variable %}
14
- #
15
- # gives you:
16
- #
17
- # Hello: -1
18
- # Hello: -2
19
- # 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.
20
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.
21
21
  class Decrement < Tag
22
22
  def initialize(tag_name, markup, options)
23
23
  super
24
24
  @variable = markup.strip
25
25
  end
26
26
 
27
- def render(context)
27
+ def render_to_output_buffer(context, output)
28
28
  value = context.environments.first[@variable] ||= 0
29
- value = value - 1
29
+ value -= 1
30
30
  context.environments.first[@variable] = value
31
- value.to_s
31
+ output << value.to_s
32
+ output
32
33
  end
33
-
34
- private
35
34
  end
36
35
 
37
- Template.register_tag('decrement'.freeze, Decrement)
36
+ Template.register_tag('decrement', Decrement)
38
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,175 +1,206 @@
1
- module Liquid
1
+ # frozen_string_literal: true
2
2
 
3
- # "For" iterates over an array or collection.
4
- # Several useful variables are available to you within the loop.
5
- #
6
- # == Basic usage:
7
- # {% for item in collection %}
8
- # {{ forloop.index }}: {{ item.name }}
9
- # {% endfor %}
10
- #
11
- # == Advanced usage:
12
- # {% for item in collection %}
13
- # <div {% if forloop.first %}class="first"{% endif %}>
14
- # Item {{ forloop.index }}: {{ item.name }}
15
- # </div>
16
- # {% else %}
17
- # There is nothing in the collection.
18
- # {% endfor %}
19
- #
20
- # You can also define a limit and offset much like SQL. Remember
21
- # that offset starts at 0 for the first item.
22
- #
23
- # {% for item in collection limit:5 offset:10 %}
24
- # {{ item.name }}
25
- # {% end %}
26
- #
27
- # To reverse the for loop simply use {% for item in collection reversed %}
28
- #
29
- # == Available variables:
30
- #
31
- # forloop.name:: 'item-collection'
32
- # forloop.length:: Length of the loop
33
- # forloop.index:: The current item's position in the collection;
34
- # forloop.index starts at 1.
35
- # This is helpful for non-programmers who start believe
36
- # the first item in an array is 1, not 0.
37
- # forloop.index0:: The current item's position in the collection
38
- # where the first item is 0
39
- # forloop.rindex:: Number of items remaining in the loop
40
- # (length - index) where 1 is the last item.
41
- # forloop.rindex0:: Number of items remaining in the loop
42
- # where 0 is the last item.
43
- # forloop.first:: Returns true if the item is the first item.
44
- # forloop.last:: Returns true if the item is the last item.
3
+ module Liquid
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
 
30
+ attr_reader :collection_name, :variable_name, :limit, :from
31
+
49
32
  def initialize(tag_name, markup, options)
50
33
  super
34
+ @from = @limit = nil
51
35
  parse_with_selected_parser(markup)
52
- @nodelist = @for_block = []
36
+ @for_block = new_body
37
+ @else_block = nil
53
38
  end
54
39
 
55
- def nodelist
56
- if @else_block
57
- @for_block + @else_block
58
- else
59
- @for_block
40
+ def parse(tokens)
41
+ if parse_body(@for_block, tokens)
42
+ parse_body(@else_block, tokens)
60
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
61
50
  end
62
51
 
63
- def unknown_tag(tag, markup, tokens)
64
- return super unless tag == 'else'.freeze
65
- @nodelist = @else_block = []
52
+ def nodelist
53
+ @else_block ? [@for_block, @else_block] : [@for_block]
66
54
  end
67
55
 
68
- 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)
56
+ def unknown_tag(tag, markup, tokens)
57
+ return super unless tag == 'else'
58
+ @else_block = new_body
59
+ end
73
60
 
74
- # Maintains Ruby 1.8.7 String#each behaviour on 1.9
75
- return render_else(context) unless iterable?(collection)
61
+ def render_to_output_buffer(context, output)
62
+ segment = collection_segment(context)
76
63
 
77
- from = if @attributes['offset'.freeze] == 'continue'.freeze
78
- context.registers[:for][@name].to_i
64
+ if segment.empty?
65
+ render_else(context, output)
79
66
  else
80
- context[@attributes['offset'.freeze]].to_i
67
+ render_segment(context, output, segment)
81
68
  end
82
69
 
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
70
+ output
124
71
  end
125
72
 
126
73
  protected
127
74
 
128
75
  def lax_parse(markup)
129
76
  if markup =~ Syntax
130
- @variable_name = $1
131
- @collection_name = $2
132
- @name = "#{$1}-#{$2}"
133
- @reversed = $3
134
- @attributes = {}
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)
135
82
  markup.scan(TagAttributes) do |key, value|
136
- @attributes[key] = value
83
+ set_attribute(key, value)
137
84
  end
138
85
  else
139
- raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze))
86
+ raise SyntaxError, options[:locale].t("errors.syntax.for")
140
87
  end
141
88
  end
142
89
 
143
90
  def strict_parse(markup)
144
91
  p = Parser.new(markup)
145
92
  @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}"
149
- @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')
150
100
 
151
- @attributes = {}
152
101
  while p.look(:id) && p.look(:colon, 1)
153
- unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze)
154
- 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")
155
104
  end
156
105
  p.consume
157
- val = p.expression
158
- @attributes[attribute] = val
106
+ set_attribute(attribute, p.expression)
159
107
  end
160
108
  p.consume(:end_of_string)
161
109
  end
162
110
 
163
111
  private
164
112
 
165
- def render_else(context)
166
- return @else_block ? [render_all(@else_block, context)] : ''.freeze
113
+ def collection_segment(context)
114
+ offsets = context.registers[:for] ||= {}
115
+
116
+ from = if @from == :continue
117
+ offsets[@name].to_i
118
+ else
119
+ from_value = context.evaluate(@from)
120
+ if from_value.nil?
121
+ 0
122
+ else
123
+ Utils.to_integer(from_value)
124
+ end
125
+ end
126
+
127
+ collection = context.evaluate(@collection_name)
128
+ collection = collection.to_a if collection.is_a?(Range)
129
+
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
+
137
+ segment = Utils.slice_collection(collection, from, to)
138
+ segment.reverse! if @reversed
139
+
140
+ offsets[@name] = from + segment.length
141
+
142
+ segment
143
+ end
144
+
145
+ def render_segment(context, output, segment)
146
+ for_stack = context.registers[:for_stack] ||= []
147
+ length = segment.length
148
+
149
+ context.stack do
150
+ loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
151
+
152
+ for_stack.push(loop_vars)
153
+
154
+ begin
155
+ context['forloop'] = loop_vars
156
+
157
+ segment.each do |item|
158
+ context[@variable_name] = item
159
+ @for_block.render_to_output_buffer(context, output)
160
+ loop_vars.send(:increment!)
161
+
162
+ # Handle any interrupts if they exist.
163
+ next unless context.interrupt?
164
+ interrupt = context.pop_interrupt
165
+ break if interrupt.is_a?(BreakInterrupt)
166
+ next if interrupt.is_a?(ContinueInterrupt)
167
+ end
168
+ ensure
169
+ for_stack.pop
170
+ end
171
+ end
172
+
173
+ output
174
+ end
175
+
176
+ def set_attribute(key, expr)
177
+ case key
178
+ when 'offset'
179
+ @from = if expr == 'continue'
180
+ Usage.increment('for_offset_continue')
181
+ :continue
182
+ else
183
+ parse_expression(expr)
184
+ end
185
+ when 'limit'
186
+ @limit = parse_expression(expr)
187
+ end
167
188
  end
168
189
 
169
- def iterable?(collection)
170
- collection.respond_to?(:each) || Utils.non_blank_string?(collection)
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
171
202
  end
172
203
  end
173
204
 
174
- Template.register_tag('for'.freeze, For)
205
+ Template.register_tag('for', For)
175
206
  end