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,52 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+
1
5
  module Liquid
2
6
  class BlockBody
3
- FullToken = /\A#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
4
- ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
5
- TAGSTART = "{%".freeze
6
- VARSTART = "{{".freeze
7
+ LiquidTagToken = /\A\s*(#{TagName})\s*(.*?)\z/o
8
+ FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(#{TagName})(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
9
+ FullTokenPossiblyInvalid = /\A(.*)#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*)?#{WhitespaceControl}?#{TagEnd}\z/om
10
+ ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
11
+ WhitespaceOrNothing = /\A\s*\z/
12
+ TAGSTART = "{%"
13
+ VARSTART = "{{"
7
14
 
8
15
  attr_reader :nodelist
9
16
 
10
17
  def initialize
11
18
  @nodelist = []
12
- @blank = true
19
+ @blank = true
13
20
  end
14
21
 
15
- def parse(tokenizer, parse_context)
22
+ def parse(tokenizer, parse_context, &block)
23
+ raise FrozenError, "can't modify frozen Liquid::BlockBody" if frozen?
24
+
16
25
  parse_context.line_number = tokenizer.line_number
17
- while token = tokenizer.shift
18
- unless token.empty?
19
- case
20
- when token.start_with?(TAGSTART)
21
- whitespace_handler(token, parse_context)
22
- if token =~ FullToken
23
- tag_name = $1
24
- markup = $2
25
- # fetch the tag from registered blocks
26
- if tag = registered_tags[tag_name]
27
- new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
28
- @blank &&= new_tag.blank?
29
- @nodelist << new_tag
30
- else
31
- # end parsing if we reach an unknown tag and let the caller decide
32
- # determine how to proceed
33
- return yield tag_name, markup
34
- end
35
- else
36
- raise_missing_tag_terminator(token, parse_context)
37
- end
38
- when token.start_with?(VARSTART)
39
- whitespace_handler(token, parse_context)
40
- @nodelist << create_variable(token, parse_context)
41
- @blank = false
42
- else
43
- if parse_context.trim_whitespace
44
- token.lstrip!
45
- end
46
- parse_context.trim_whitespace = false
47
- @nodelist << token
48
- @blank &&= !!(token =~ /\A\s*\z/)
26
+
27
+ if tokenizer.for_liquid_tag
28
+ parse_for_liquid_tag(tokenizer, parse_context, &block)
29
+ else
30
+ parse_for_document(tokenizer, parse_context, &block)
31
+ end
32
+ end
33
+
34
+ def freeze
35
+ @nodelist.freeze
36
+ super
37
+ end
38
+
39
+ private def parse_for_liquid_tag(tokenizer, parse_context)
40
+ while (token = tokenizer.shift)
41
+ unless token.empty? || token.match?(WhitespaceOrNothing)
42
+ unless token =~ LiquidTagToken
43
+ # line isn't empty but didn't match tag syntax, yield and let the
44
+ # caller raise a syntax error
45
+ return yield token, token
46
+ end
47
+ tag_name = Regexp.last_match(1)
48
+ markup = Regexp.last_match(2)
49
+
50
+ if tag_name == 'liquid'
51
+ parse_context.line_number -= 1
52
+ next parse_liquid_tag(markup, parse_context)
53
+ end
54
+
55
+ unless (tag = parse_context.environment.tag_for_name(tag_name))
56
+ # end parsing if we reach an unknown tag and let the caller decide
57
+ # determine how to proceed
58
+ return yield tag_name, markup
49
59
  end
60
+ new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
61
+ @blank &&= new_tag.blank?
62
+ @nodelist << new_tag
63
+ end
64
+ parse_context.line_number = tokenizer.line_number
65
+ end
66
+
67
+ yield nil, nil
68
+ end
69
+
70
+ # @api private
71
+ def self.unknown_tag_in_liquid_tag(tag, parse_context)
72
+ Block.raise_unknown_tag(tag, 'liquid', '%}', parse_context)
73
+ end
74
+
75
+ # @api private
76
+ def self.raise_missing_tag_terminator(token, parse_context)
77
+ raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect)
78
+ end
79
+
80
+ # @api private
81
+ def self.raise_missing_variable_terminator(token, parse_context)
82
+ raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect)
83
+ end
84
+
85
+ # @api private
86
+ def self.render_node(context, output, node)
87
+ node.render_to_output_buffer(context, output)
88
+ rescue => exc
89
+ blank_tag = !node.instance_of?(Variable) && node.blank?
90
+ rescue_render_node(context, output, node.line_number, exc, blank_tag)
91
+ end
92
+
93
+ # @api private
94
+ def self.rescue_render_node(context, output, line_number, exc, blank_tag)
95
+ case exc
96
+ when MemoryError
97
+ raise
98
+ when UndefinedVariable, UndefinedDropMethod, UndefinedFilter
99
+ context.handle_error(exc, line_number)
100
+ else
101
+ error_message = context.handle_error(exc, line_number)
102
+ unless blank_tag # conditional for backwards compatibility
103
+ output << error_message
104
+ end
105
+ end
106
+ end
107
+
108
+ private def parse_liquid_tag(markup, parse_context)
109
+ liquid_tag_tokenizer = parse_context.new_tokenizer(
110
+ markup, start_line_number: parse_context.line_number, for_liquid_tag: true
111
+ )
112
+ parse_for_liquid_tag(liquid_tag_tokenizer, parse_context) do |end_tag_name, _end_tag_markup|
113
+ if end_tag_name
114
+ BlockBody.unknown_tag_in_liquid_tag(end_tag_name, parse_context)
115
+ end
116
+ end
117
+ end
118
+
119
+ private def handle_invalid_tag_token(token, parse_context)
120
+ if token.end_with?('%}')
121
+ yield token, token
122
+ else
123
+ BlockBody.raise_missing_tag_terminator(token, parse_context)
124
+ end
125
+ end
126
+
127
+ private def parse_for_document(tokenizer, parse_context, &block)
128
+ while (token = tokenizer.shift)
129
+ next if token.empty?
130
+ case
131
+ when token.start_with?(TAGSTART)
132
+ whitespace_handler(token, parse_context)
133
+ unless token =~ FullToken
134
+ return handle_invalid_tag_token(token, parse_context, &block)
135
+ end
136
+ tag_name = Regexp.last_match(2)
137
+ markup = Regexp.last_match(4)
138
+
139
+ if parse_context.line_number
140
+ # newlines inside the tag should increase the line number,
141
+ # particularly important for multiline {% liquid %} tags
142
+ parse_context.line_number += Regexp.last_match(1).count("\n") + Regexp.last_match(3).count("\n")
143
+ end
144
+
145
+ if tag_name == 'liquid'
146
+ parse_liquid_tag(markup, parse_context)
147
+ next
148
+ end
149
+
150
+ unless (tag = parse_context.environment.tag_for_name(tag_name))
151
+ # end parsing if we reach an unknown tag and let the caller decide
152
+ # determine how to proceed
153
+ return yield tag_name, markup
154
+ end
155
+ new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
156
+ @blank &&= new_tag.blank?
157
+ @nodelist << new_tag
158
+ when token.start_with?(VARSTART)
159
+ whitespace_handler(token, parse_context)
160
+ @nodelist << create_variable(token, parse_context)
161
+ @blank = false
162
+ else
163
+ if parse_context.trim_whitespace
164
+ token.lstrip!
165
+ end
166
+ parse_context.trim_whitespace = false
167
+ @nodelist << token
168
+ @blank &&= token.match?(WhitespaceOrNothing)
50
169
  end
51
170
  parse_context.line_number = tokenizer.line_number
52
171
  end
@@ -57,8 +176,12 @@ module Liquid
57
176
  def whitespace_handler(token, parse_context)
58
177
  if token[2] == WhitespaceControl
59
178
  previous_token = @nodelist.last
60
- if previous_token.is_a? String
179
+ if previous_token.is_a?(String)
180
+ first_byte = previous_token.getbyte(0)
61
181
  previous_token.rstrip!
182
+ if previous_token.empty? && parse_context[:bug_compatible_whitespace_trimming] && first_byte
183
+ previous_token << first_byte
184
+ end
62
185
  end
63
186
  end
64
187
  parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
@@ -68,72 +191,83 @@ module Liquid
68
191
  @blank
69
192
  end
70
193
 
194
+ # Remove blank strings in the block body for a control flow tag (e.g. `if`, `for`, `case`, `unless`)
195
+ # with a blank body.
196
+ #
197
+ # For example, in a conditional assignment like the following
198
+ #
199
+ # ```
200
+ # {% if size > max_size %}
201
+ # {% assign size = max_size %}
202
+ # {% endif %}
203
+ # ```
204
+ #
205
+ # we assume the intention wasn't to output the blank spaces in the `if` tag's block body, so this method
206
+ # will remove them to reduce the render output size.
207
+ #
208
+ # Note that it is now preferred to use the `liquid` tag for this use case.
209
+ def remove_blank_strings
210
+ raise "remove_blank_strings only support being called on a blank block body" unless @blank
211
+ @nodelist.reject! { |node| node.instance_of?(String) }
212
+ end
213
+
71
214
  def render(context)
72
- output = []
73
- context.resource_limits.render_score += @nodelist.length
215
+ render_to_output_buffer(context, +'')
216
+ end
217
+
218
+ def render_to_output_buffer(context, output)
219
+ freeze unless frozen?
74
220
 
75
- @nodelist.each do |token|
76
- # Break out if we have any unhanded interrupts.
77
- break if context.interrupt?
221
+ context.resource_limits.increment_render_score(@nodelist.length)
78
222
 
79
- begin
223
+ idx = 0
224
+ while (node = @nodelist[idx])
225
+ if node.instance_of?(String)
226
+ output << node
227
+ else
228
+ render_node(context, output, node)
80
229
  # If we get an Interrupt that means the block must stop processing. An
81
230
  # Interrupt is any command that stops block execution such as {% break %}
82
- # or {% continue %}
83
- if token.is_a?(Continue) || token.is_a?(Break)
84
- context.push_interrupt(token.interrupt)
85
- break
86
- end
87
-
88
- node_output = render_node(token, context)
89
-
90
- unless token.is_a?(Block) && token.blank?
91
- output << node_output
92
- end
93
- rescue MemoryError => e
94
- raise e
95
- rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
96
- context.handle_error(e, token.line_number, token.raw)
97
- output << nil
98
- rescue ::StandardError => e
99
- output << context.handle_error(e, token.line_number, token.raw)
231
+ # or {% continue %}. These tags may also occur through Block or Include tags.
232
+ break if context.interrupt? # might have happened in a for-block
100
233
  end
234
+ idx += 1
235
+
236
+ context.resource_limits.increment_write_score(output)
101
237
  end
102
238
 
103
- output.join
239
+ output
104
240
  end
105
241
 
106
242
  private
107
243
 
108
- def render_node(node, context)
109
- node_output = (node.respond_to?(:render) ? node.render(context) : node)
110
- node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
111
-
112
- context.resource_limits.render_length += node_output.length
113
- if context.resource_limits.reached?
114
- raise MemoryError.new("Memory limits exceeded".freeze)
115
- end
116
- node_output
244
+ def render_node(context, output, node)
245
+ BlockBody.render_node(context, output, node)
117
246
  end
118
247
 
119
248
  def create_variable(token, parse_context)
120
- token.scan(ContentOfVariable) do |content|
121
- markup = content.first
249
+ if token.end_with?("}}")
250
+ i = 2
251
+ i = 3 if token[i] == "-"
252
+ parse_end = token.length - 3
253
+ parse_end -= 1 if token[parse_end] == "-"
254
+ markup_end = parse_end - i + 1
255
+ markup = markup_end <= 0 ? "" : token.slice(i, markup_end)
256
+
122
257
  return Variable.new(markup, parse_context)
123
258
  end
124
- raise_missing_variable_terminator(token, parse_context)
259
+
260
+ BlockBody.raise_missing_variable_terminator(token, parse_context)
125
261
  end
126
262
 
263
+ # @deprecated Use {.raise_missing_tag_terminator} instead
127
264
  def raise_missing_tag_terminator(token, parse_context)
128
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_termination".freeze, token: token, tag_end: TagEnd.inspect))
265
+ BlockBody.raise_missing_tag_terminator(token, parse_context)
129
266
  end
130
267
 
268
+ # @deprecated Use {.raise_missing_variable_terminator} instead
131
269
  def raise_missing_variable_terminator(token, parse_context)
132
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.variable_termination".freeze, token: token, tag_end: VariableEnd.inspect))
133
- end
134
-
135
- def registered_tags
136
- Template.tags
270
+ BlockBody.raise_missing_variable_terminator(token, parse_context)
137
271
  end
138
272
  end
139
273
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  # Container for liquid nodes which conveniently wraps decision making logic
3
5
  #
@@ -6,60 +8,88 @@ module Liquid
6
8
  # c = Condition.new(1, '==', 1)
7
9
  # c.evaluate #=> true
8
10
  #
9
- class Condition #:nodoc:
11
+ class Condition # :nodoc:
10
12
  @@operators = {
11
- '=='.freeze => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
12
- '!='.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
13
- '<>'.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
14
- '<'.freeze => :<,
15
- '>'.freeze => :>,
16
- '>='.freeze => :>=,
17
- '<='.freeze => :<=,
18
- 'contains'.freeze => lambda do |cond, left, right|
13
+ '==' => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
14
+ '!=' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
15
+ '<>' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
16
+ '<' => :<,
17
+ '>' => :>,
18
+ '>=' => :>=,
19
+ '<=' => :<=,
20
+ 'contains' => lambda do |_cond, left, right|
19
21
  if left && right && left.respond_to?(:include?)
20
22
  right = right.to_s if left.is_a?(String)
21
23
  left.include?(right)
22
24
  else
23
25
  false
24
26
  end
27
+ rescue Encoding::CompatibilityError
28
+ # "✅".b.include?("✅") raises Encoding::CompatibilityError despite being materially equal
29
+ left.b.include?(right.b)
30
+ end,
31
+ }
32
+
33
+ class MethodLiteral
34
+ attr_reader :method_name, :to_s
35
+
36
+ def initialize(method_name, to_s)
37
+ @method_name = method_name
38
+ @to_s = to_s
25
39
  end
40
+ end
41
+
42
+ @@method_literals = {
43
+ 'blank' => MethodLiteral.new(:blank?, '').freeze,
44
+ 'empty' => MethodLiteral.new(:empty?, '').freeze,
26
45
  }
27
46
 
28
47
  def self.operators
29
48
  @@operators
30
49
  end
31
50
 
32
- attr_reader :attachment
51
+ def self.parse_expression(parse_context, markup, safe: false)
52
+ @@method_literals[markup] || parse_context.parse_expression(markup, safe: safe)
53
+ end
54
+
55
+ attr_reader :attachment, :child_condition
33
56
  attr_accessor :left, :operator, :right
34
57
 
35
58
  def initialize(left = nil, operator = nil, right = nil)
36
- @left = left
59
+ @left = left
37
60
  @operator = operator
38
- @right = right
61
+ @right = right
62
+
39
63
  @child_relation = nil
40
64
  @child_condition = nil
41
65
  end
42
66
 
43
- def evaluate(context = Context.new)
44
- result = interpret_condition(left, right, operator, context)
45
-
46
- case @child_relation
47
- when :or
48
- result || @child_condition.evaluate(context)
49
- when :and
50
- result && @child_condition.evaluate(context)
51
- else
52
- result
67
+ def evaluate(context = deprecated_default_context)
68
+ condition = self
69
+ result = nil
70
+ loop do
71
+ result = interpret_condition(condition.left, condition.right, condition.operator, context)
72
+
73
+ case condition.child_relation
74
+ when :or
75
+ break if Liquid::Utils.to_liquid_value(result)
76
+ when :and
77
+ break unless Liquid::Utils.to_liquid_value(result)
78
+ else
79
+ break
80
+ end
81
+ condition = condition.child_condition
53
82
  end
83
+ result
54
84
  end
55
85
 
56
86
  def or(condition)
57
- @child_relation = :or
87
+ @child_relation = :or
58
88
  @child_condition = condition
59
89
  end
60
90
 
61
91
  def and(condition)
62
- @child_relation = :and
92
+ @child_relation = :and
63
93
  @child_condition = condition
64
94
  end
65
95
 
@@ -72,13 +102,17 @@ module Liquid
72
102
  end
73
103
 
74
104
  def inspect
75
- "#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
105
+ "#<Condition #{[@left, @operator, @right].compact.join(' ')}>"
76
106
  end
77
107
 
108
+ protected
109
+
110
+ attr_reader :child_relation
111
+
78
112
  private
79
113
 
80
114
  def equal_variables(left, right)
81
- if left.is_a?(Liquid::Expression::MethodLiteral)
115
+ if left.is_a?(MethodLiteral)
82
116
  if right.respond_to?(left.method_name)
83
117
  return right.send(left.method_name)
84
118
  else
@@ -86,7 +120,7 @@ module Liquid
86
120
  end
87
121
  end
88
122
 
89
- if right.is_a?(Liquid::Expression::MethodLiteral)
123
+ if right.is_a?(MethodLiteral)
90
124
  if left.respond_to?(right.method_name)
91
125
  return left.send(right.method_name)
92
126
  else
@@ -103,21 +137,38 @@ module Liquid
103
137
  # return this as the result.
104
138
  return context.evaluate(left) if op.nil?
105
139
 
106
- left = context.evaluate(left)
107
- right = context.evaluate(right)
140
+ left = Liquid::Utils.to_liquid_value(context.evaluate(left))
141
+ right = Liquid::Utils.to_liquid_value(context.evaluate(right))
108
142
 
109
- operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}"))
143
+ operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}")
110
144
 
111
145
  if operation.respond_to?(:call)
112
146
  operation.call(self, left, right)
113
- elsif left.respond_to?(operation) && right.respond_to?(operation)
147
+ elsif left.respond_to?(operation) && right.respond_to?(operation) && !left.is_a?(Hash) && !right.is_a?(Hash)
114
148
  begin
115
149
  left.send(operation, right)
116
150
  rescue ::ArgumentError => e
117
- raise Liquid::ArgumentError.new(e.message)
151
+ raise Liquid::ArgumentError, e.message
118
152
  end
119
153
  end
120
154
  end
155
+
156
+ def deprecated_default_context
157
+ warn("DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \
158
+ " and will be removed from Liquid 6.0.0.")
159
+ Context.new
160
+ end
161
+
162
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
163
+ def children
164
+ [
165
+ @node.left,
166
+ @node.right,
167
+ @node.child_condition,
168
+ @node.attachment
169
+ ].compact
170
+ end
171
+ end
121
172
  end
122
173
 
123
174
  class ElseCondition < Condition
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ module Const
5
+ EMPTY_HASH = {}.freeze
6
+ EMPTY_ARRAY = [].freeze
7
+ end
8
+ end