liquid 4.0.0 → 5.10.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 (117) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +235 -2
  3. data/README.md +58 -8
  4. data/lib/liquid/block.rb +51 -20
  5. data/lib/liquid/block_body.rb +216 -82
  6. data/lib/liquid/condition.rb +83 -32
  7. data/lib/liquid/const.rb +8 -0
  8. data/lib/liquid/context.rb +130 -59
  9. data/lib/liquid/deprecations.rb +22 -0
  10. data/lib/liquid/document.rb +47 -9
  11. data/lib/liquid/drop.rb +8 -2
  12. data/lib/liquid/environment.rb +159 -0
  13. data/lib/liquid/errors.rb +23 -20
  14. data/lib/liquid/expression.rb +114 -31
  15. data/lib/liquid/extensions.rb +8 -0
  16. data/lib/liquid/file_system.rb +6 -4
  17. data/lib/liquid/forloop_drop.rb +51 -4
  18. data/lib/liquid/i18n.rb +5 -3
  19. data/lib/liquid/interrupts.rb +3 -1
  20. data/lib/liquid/lexer.rb +165 -39
  21. data/lib/liquid/locales/en.yml +16 -6
  22. data/lib/liquid/parse_context.rb +62 -7
  23. data/lib/liquid/parse_tree_visitor.rb +42 -0
  24. data/lib/liquid/parser.rb +31 -19
  25. data/lib/liquid/parser_switching.rb +42 -3
  26. data/lib/liquid/partial_cache.rb +33 -0
  27. data/lib/liquid/profiler/hooks.rb +26 -14
  28. data/lib/liquid/profiler.rb +67 -86
  29. data/lib/liquid/range_lookup.rb +26 -6
  30. data/lib/liquid/registers.rb +51 -0
  31. data/lib/liquid/resource_limits.rb +47 -8
  32. data/lib/liquid/snippet_drop.rb +22 -0
  33. data/lib/liquid/standardfilters.rb +813 -137
  34. data/lib/liquid/strainer_template.rb +62 -0
  35. data/lib/liquid/tablerowloop_drop.rb +64 -5
  36. data/lib/liquid/tag/disableable.rb +22 -0
  37. data/lib/liquid/tag/disabler.rb +13 -0
  38. data/lib/liquid/tag.rb +42 -6
  39. data/lib/liquid/tags/assign.rb +46 -18
  40. data/lib/liquid/tags/break.rb +15 -4
  41. data/lib/liquid/tags/capture.rb +26 -18
  42. data/lib/liquid/tags/case.rb +108 -32
  43. data/lib/liquid/tags/comment.rb +76 -4
  44. data/lib/liquid/tags/continue.rb +15 -13
  45. data/lib/liquid/tags/cycle.rb +117 -34
  46. data/lib/liquid/tags/decrement.rb +30 -23
  47. data/lib/liquid/tags/doc.rb +81 -0
  48. data/lib/liquid/tags/echo.rb +39 -0
  49. data/lib/liquid/tags/for.rb +109 -96
  50. data/lib/liquid/tags/if.rb +72 -41
  51. data/lib/liquid/tags/ifchanged.rb +10 -11
  52. data/lib/liquid/tags/include.rb +89 -63
  53. data/lib/liquid/tags/increment.rb +31 -20
  54. data/lib/liquid/tags/inline_comment.rb +28 -0
  55. data/lib/liquid/tags/raw.rb +25 -13
  56. data/lib/liquid/tags/render.rb +151 -0
  57. data/lib/liquid/tags/snippet.rb +45 -0
  58. data/lib/liquid/tags/table_row.rb +104 -21
  59. data/lib/liquid/tags/unless.rb +37 -20
  60. data/lib/liquid/tags.rb +51 -0
  61. data/lib/liquid/template.rb +90 -106
  62. data/lib/liquid/template_factory.rb +9 -0
  63. data/lib/liquid/tokenizer.rb +143 -13
  64. data/lib/liquid/usage.rb +8 -0
  65. data/lib/liquid/utils.rb +114 -5
  66. data/lib/liquid/variable.rb +119 -45
  67. data/lib/liquid/variable_lookup.rb +35 -13
  68. data/lib/liquid/version.rb +3 -1
  69. data/lib/liquid.rb +31 -18
  70. metadata +56 -107
  71. data/lib/liquid/strainer.rb +0 -66
  72. data/test/fixtures/en_locale.yml +0 -9
  73. data/test/integration/assign_test.rb +0 -48
  74. data/test/integration/blank_test.rb +0 -106
  75. data/test/integration/capture_test.rb +0 -50
  76. data/test/integration/context_test.rb +0 -32
  77. data/test/integration/document_test.rb +0 -19
  78. data/test/integration/drop_test.rb +0 -273
  79. data/test/integration/error_handling_test.rb +0 -260
  80. data/test/integration/filter_test.rb +0 -178
  81. data/test/integration/hash_ordering_test.rb +0 -23
  82. data/test/integration/output_test.rb +0 -123
  83. data/test/integration/parsing_quirks_test.rb +0 -118
  84. data/test/integration/render_profiling_test.rb +0 -154
  85. data/test/integration/security_test.rb +0 -66
  86. data/test/integration/standard_filter_test.rb +0 -535
  87. data/test/integration/tags/break_tag_test.rb +0 -15
  88. data/test/integration/tags/continue_tag_test.rb +0 -15
  89. data/test/integration/tags/for_tag_test.rb +0 -410
  90. data/test/integration/tags/if_else_tag_test.rb +0 -188
  91. data/test/integration/tags/include_tag_test.rb +0 -238
  92. data/test/integration/tags/increment_tag_test.rb +0 -23
  93. data/test/integration/tags/raw_tag_test.rb +0 -31
  94. data/test/integration/tags/standard_tag_test.rb +0 -296
  95. data/test/integration/tags/statements_test.rb +0 -111
  96. data/test/integration/tags/table_row_test.rb +0 -64
  97. data/test/integration/tags/unless_else_tag_test.rb +0 -26
  98. data/test/integration/template_test.rb +0 -323
  99. data/test/integration/trim_mode_test.rb +0 -525
  100. data/test/integration/variable_test.rb +0 -92
  101. data/test/test_helper.rb +0 -117
  102. data/test/unit/block_unit_test.rb +0 -58
  103. data/test/unit/condition_unit_test.rb +0 -158
  104. data/test/unit/context_unit_test.rb +0 -483
  105. data/test/unit/file_system_unit_test.rb +0 -35
  106. data/test/unit/i18n_unit_test.rb +0 -37
  107. data/test/unit/lexer_unit_test.rb +0 -51
  108. data/test/unit/parser_unit_test.rb +0 -82
  109. data/test/unit/regexp_unit_test.rb +0 -44
  110. data/test/unit/strainer_unit_test.rb +0 -148
  111. data/test/unit/tag_unit_test.rb +0 -21
  112. data/test/unit/tags/case_tag_unit_test.rb +0 -10
  113. data/test/unit/tags/for_tag_unit_test.rb +0 -13
  114. data/test/unit/tags/if_tag_unit_test.rb +0 -8
  115. data/test/unit/template_unit_test.rb +0 -78
  116. data/test/unit/tokenizer_unit_test.rb +0 -55
  117. data/test/unit/variable_unit_test.rb +0 -162
@@ -1,62 +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 %}
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](/docs/api/liquid/tags/paginate) to split the items over multiple pages.
45
13
  #
14
+ # > Tip:
15
+ # > Every `for` loop has an associated [`forloop` object](/docs/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
51
34
  @from = @limit = nil
52
35
  parse_with_selected_parser(markup)
53
- @for_block = BlockBody.new
36
+ @for_block = new_body
54
37
  @else_block = nil
55
38
  end
56
39
 
57
40
  def parse(tokens)
58
- return unless parse_body(@for_block, tokens)
59
- 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
60
50
  end
61
51
 
62
52
  def nodelist
@@ -64,72 +54,90 @@ module Liquid
64
54
  end
65
55
 
66
56
  def unknown_tag(tag, markup, tokens)
67
- return super unless tag == 'else'.freeze
68
- @else_block = BlockBody.new
57
+ return super unless tag == 'else'
58
+ @else_block = new_body
69
59
  end
70
60
 
71
- def render(context)
61
+ def render_to_output_buffer(context, output)
72
62
  segment = collection_segment(context)
73
63
 
74
64
  if segment.empty?
75
- render_else(context)
65
+ render_else(context, output)
76
66
  else
77
- render_segment(context, segment)
67
+ render_segment(context, output, segment)
78
68
  end
69
+
70
+ output
79
71
  end
80
72
 
81
73
  protected
82
74
 
83
75
  def lax_parse(markup)
84
76
  if markup =~ Syntax
85
- @variable_name = $1
86
- collection_name = $2
87
- @reversed = !!$3
88
- @name = "#{@variable_name}-#{collection_name}"
89
- @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)
90
82
  markup.scan(TagAttributes) do |key, value|
91
83
  set_attribute(key, value)
92
84
  end
93
85
  else
94
- raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze))
86
+ raise SyntaxError, options[:locale].t("errors.syntax.for")
95
87
  end
96
88
  end
97
89
 
98
90
  def strict_parse(markup)
99
- p = Parser.new(markup)
91
+ p = @parse_context.new_parser(markup)
100
92
  @variable_name = p.consume(:id)
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)
105
- @reversed = p.id?('reversed'.freeze)
106
-
107
- while p.look(:id) && p.look(:colon, 1)
108
- unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze)
109
- raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".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, safe: true)
97
+
98
+ @name = "#{@variable_name}-#{collection_name}"
99
+ @reversed = p.id?('reversed')
100
+
101
+ while p.look(:comma) || p.look(:id)
102
+ p.consume?(:comma)
103
+ unless (attribute = p.id?('limit') || p.id?('offset'))
104
+ raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
110
105
  end
111
- p.consume
112
- set_attribute(attribute, p.expression)
106
+ p.consume(:colon)
107
+ set_attribute(attribute, p.expression, safe: true)
113
108
  end
114
109
  p.consume(:end_of_string)
115
110
  end
116
111
 
117
112
  private
118
113
 
114
+ def rigid_parse(markup)
115
+ strict_parse(markup)
116
+ end
117
+
119
118
  def collection_segment(context)
120
- offsets = context.registers[:for] ||= Hash.new(0)
119
+ offsets = context.registers[:for] ||= {}
121
120
 
122
121
  from = if @from == :continue
123
122
  offsets[@name].to_i
124
123
  else
125
- context.evaluate(@from).to_i
124
+ from_value = context.evaluate(@from)
125
+ if from_value.nil?
126
+ 0
127
+ else
128
+ Utils.to_integer(from_value)
129
+ end
126
130
  end
127
131
 
128
132
  collection = context.evaluate(@collection_name)
129
133
  collection = collection.to_a if collection.is_a?(Range)
130
134
 
131
- limit = context.evaluate(@limit)
132
- to = limit ? limit.to_i + from : nil
135
+ limit_value = context.evaluate(@limit)
136
+ to = if limit_value.nil?
137
+ nil
138
+ else
139
+ Utils.to_integer(limit_value) + from
140
+ end
133
141
 
134
142
  segment = Utils.slice_collection(collection, from, to)
135
143
  segment.reverse! if @reversed
@@ -139,11 +147,9 @@ module Liquid
139
147
  segment
140
148
  end
141
149
 
142
- def render_segment(context, segment)
150
+ def render_segment(context, output, segment)
143
151
  for_stack = context.registers[:for_stack] ||= []
144
- length = segment.length
145
-
146
- result = ''
152
+ length = segment.length
147
153
 
148
154
  context.stack do
149
155
  loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
@@ -151,45 +157,52 @@ module Liquid
151
157
  for_stack.push(loop_vars)
152
158
 
153
159
  begin
154
- context['forloop'.freeze] = loop_vars
160
+ context['forloop'] = loop_vars
155
161
 
156
- segment.each_with_index do |item, index|
162
+ segment.each do |item|
157
163
  context[@variable_name] = item
158
- result << @for_block.render(context)
164
+ @for_block.render_to_output_buffer(context, output)
159
165
  loop_vars.send(:increment!)
160
166
 
161
167
  # 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
168
+ next unless context.interrupt?
169
+ interrupt = context.pop_interrupt
170
+ break if interrupt.is_a?(BreakInterrupt)
171
+ next if interrupt.is_a?(ContinueInterrupt)
167
172
  end
168
173
  ensure
169
174
  for_stack.pop
170
175
  end
171
176
  end
172
177
 
173
- result
178
+ output
174
179
  end
175
180
 
176
- def set_attribute(key, expr)
181
+ def set_attribute(key, expr, safe: false)
177
182
  case key
178
- when 'offset'.freeze
179
- @from = if expr == 'continue'.freeze
183
+ when 'offset'
184
+ @from = if expr == 'continue'
180
185
  :continue
181
186
  else
182
- Expression.parse(expr)
187
+ parse_expression(expr, safe: safe)
183
188
  end
184
- when 'limit'.freeze
185
- @limit = Expression.parse(expr)
189
+ when 'limit'
190
+ @limit = parse_expression(expr, safe: safe)
186
191
  end
187
192
  end
188
193
 
189
- def render_else(context)
190
- @else_block ? @else_block.render(context) : ''.freeze
194
+ def render_else(context, output)
195
+ if @else_block
196
+ @else_block.render_to_output_buffer(context, output)
197
+ else
198
+ output
199
+ end
191
200
  end
192
- end
193
201
 
194
- Template.register_tag('for'.freeze, For)
202
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
203
+ def children
204
+ (super + [@node.limit, @node.from, @node.collection_name]).compact
205
+ end
206
+ end
207
+ end
195
208
  end
@@ -1,79 +1,103 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
- # If is the conditional block
3
- #
4
- # {% if user.admin %}
5
- # Admin user!
6
- # {% else %}
7
- # Not admin user
4
+ # @liquid_public_docs
5
+ # @liquid_type tag
6
+ # @liquid_category conditional
7
+ # @liquid_name if
8
+ # @liquid_summary
9
+ # Renders an expression if a specific condition is `true`.
10
+ # @liquid_syntax
11
+ # {% if condition %}
12
+ # expression
8
13
  # {% endif %}
9
- #
10
- # There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
11
- #
14
+ # @liquid_syntax_keyword condition The condition to evaluate.
15
+ # @liquid_syntax_keyword expression The expression to render if the condition is met.
12
16
  class If < Block
13
- Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
17
+ Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
14
18
  ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
15
- BOOLEAN_OPERATORS = %w(and or)
19
+ BOOLEAN_OPERATORS = %w(and or).freeze
20
+
21
+ attr_reader :blocks
16
22
 
17
23
  def initialize(tag_name, markup, options)
18
24
  super
19
25
  @blocks = []
20
- push_block('if'.freeze, markup)
26
+ push_block('if', markup)
27
+ end
28
+
29
+ def nodelist
30
+ @blocks.map(&:attachment)
21
31
  end
22
32
 
23
33
  def parse(tokens)
24
34
  while parse_body(@blocks.last.attachment, tokens)
25
35
  end
36
+ @blocks.reverse_each do |block|
37
+ block.attachment.remove_blank_strings if blank?
38
+ block.attachment.freeze
39
+ end
26
40
  end
27
41
 
28
- def nodelist
29
- @blocks.map(&:attachment)
30
- end
42
+ ELSE_TAG_NAMES = ['elsif', 'else'].freeze
43
+ private_constant :ELSE_TAG_NAMES
31
44
 
32
45
  def unknown_tag(tag, markup, tokens)
33
- if ['elsif'.freeze, 'else'.freeze].include?(tag)
46
+ if ELSE_TAG_NAMES.include?(tag)
34
47
  push_block(tag, markup)
35
48
  else
36
49
  super
37
50
  end
38
51
  end
39
52
 
40
- def render(context)
41
- context.stack do
42
- @blocks.each do |block|
43
- if block.evaluate(context)
44
- return block.attachment.render(context)
45
- end
53
+ def render_to_output_buffer(context, output)
54
+ @blocks.each do |block|
55
+ result = Liquid::Utils.to_liquid_value(
56
+ block.evaluate(context),
57
+ )
58
+
59
+ if result
60
+ return block.attachment.render_to_output_buffer(context, output)
46
61
  end
47
- ''.freeze
48
62
  end
63
+
64
+ output
49
65
  end
50
66
 
51
67
  private
52
68
 
69
+ def rigid_parse(markup)
70
+ strict_parse(markup)
71
+ end
72
+
53
73
  def push_block(tag, markup)
54
- block = if tag == 'else'.freeze
74
+ block = if tag == 'else'
55
75
  ElseCondition.new
56
76
  else
57
77
  parse_with_selected_parser(markup)
58
78
  end
59
79
 
60
80
  @blocks.push(block)
61
- block.attach(BlockBody.new)
81
+ block.attach(new_body)
82
+ end
83
+
84
+ def parse_expression(markup, safe: false)
85
+ Condition.parse_expression(parse_context, markup, safe: safe)
62
86
  end
63
87
 
64
88
  def lax_parse(markup)
65
89
  expressions = markup.scan(ExpressionsAndOperators)
66
- raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop =~ Syntax
90
+ raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop =~ Syntax
67
91
 
68
- condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
92
+ condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3)))
69
93
 
70
94
  until expressions.empty?
71
95
  operator = expressions.pop.to_s.strip
72
96
 
73
- raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop.to_s =~ Syntax
97
+ raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop.to_s =~ Syntax
74
98
 
75
- new_condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
76
- raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator)
99
+ new_condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3)))
100
+ raise SyntaxError, options[:locale].t("errors.syntax.if") unless BOOLEAN_OPERATORS.include?(operator)
77
101
  new_condition.send(operator, condition)
78
102
  condition = new_condition
79
103
  end
@@ -82,30 +106,37 @@ module Liquid
82
106
  end
83
107
 
84
108
  def strict_parse(markup)
85
- p = Parser.new(markup)
86
- condition = parse_binary_comparison(p)
109
+ p = @parse_context.new_parser(markup)
110
+ condition = parse_binary_comparisons(p)
87
111
  p.consume(:end_of_string)
88
112
  condition
89
113
  end
90
114
 
91
- def parse_binary_comparison(p)
115
+ def parse_binary_comparisons(p)
92
116
  condition = parse_comparison(p)
93
- if op = (p.id?('and'.freeze) || p.id?('or'.freeze))
94
- condition.send(op, parse_binary_comparison(p))
117
+ first_condition = condition
118
+ while (op = p.id?('and') || p.id?('or'))
119
+ child_condition = parse_comparison(p)
120
+ condition.send(op, child_condition)
121
+ condition = child_condition
95
122
  end
96
- condition
123
+ first_condition
97
124
  end
98
125
 
99
126
  def parse_comparison(p)
100
- a = Expression.parse(p.expression)
101
- if op = p.consume?(:comparison)
102
- b = Expression.parse(p.expression)
127
+ a = parse_expression(p.expression, safe: true)
128
+ if (op = p.consume?(:comparison))
129
+ b = parse_expression(p.expression, safe: true)
103
130
  Condition.new(a, op, b)
104
131
  else
105
132
  Condition.new(a)
106
133
  end
107
134
  end
108
- end
109
135
 
110
- Template.register_tag('if'.freeze, If)
136
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
137
+ def children
138
+ @node.blocks
139
+ end
140
+ end
141
+ end
111
142
  end
@@ -1,18 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class Ifchanged < Block
3
- def render(context)
4
- context.stack do
5
- output = super
5
+ def render_to_output_buffer(context, output)
6
+ block_output = +''
7
+ super(context, block_output)
6
8
 
7
- if output != context.registers[:ifchanged]
8
- context.registers[:ifchanged] = output
9
- output
10
- else
11
- ''.freeze
12
- end
9
+ if block_output != context.registers[:ifchanged]
10
+ context.registers[:ifchanged] = block_output
11
+ output << block_output
13
12
  end
13
+
14
+ output
14
15
  end
15
16
  end
16
-
17
- Template.register_tag('ifchanged'.freeze, Ifchanged)
18
17
  end