liquid 3.0.6 → 5.4.0

Sign up to get free protection for your applications and to get access to all the features.
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