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.
- checksums.yaml +5 -5
- data/History.md +235 -2
- data/README.md +58 -8
- data/lib/liquid/block.rb +51 -20
- data/lib/liquid/block_body.rb +216 -82
- data/lib/liquid/condition.rb +83 -32
- data/lib/liquid/const.rb +8 -0
- data/lib/liquid/context.rb +130 -59
- data/lib/liquid/deprecations.rb +22 -0
- data/lib/liquid/document.rb +47 -9
- data/lib/liquid/drop.rb +8 -2
- data/lib/liquid/environment.rb +159 -0
- data/lib/liquid/errors.rb +23 -20
- data/lib/liquid/expression.rb +114 -31
- data/lib/liquid/extensions.rb +8 -0
- data/lib/liquid/file_system.rb +6 -4
- data/lib/liquid/forloop_drop.rb +51 -4
- data/lib/liquid/i18n.rb +5 -3
- data/lib/liquid/interrupts.rb +3 -1
- data/lib/liquid/lexer.rb +165 -39
- data/lib/liquid/locales/en.yml +16 -6
- data/lib/liquid/parse_context.rb +62 -7
- data/lib/liquid/parse_tree_visitor.rb +42 -0
- data/lib/liquid/parser.rb +31 -19
- data/lib/liquid/parser_switching.rb +42 -3
- data/lib/liquid/partial_cache.rb +33 -0
- data/lib/liquid/profiler/hooks.rb +26 -14
- data/lib/liquid/profiler.rb +67 -86
- data/lib/liquid/range_lookup.rb +26 -6
- data/lib/liquid/registers.rb +51 -0
- data/lib/liquid/resource_limits.rb +47 -8
- data/lib/liquid/snippet_drop.rb +22 -0
- data/lib/liquid/standardfilters.rb +813 -137
- data/lib/liquid/strainer_template.rb +62 -0
- data/lib/liquid/tablerowloop_drop.rb +64 -5
- data/lib/liquid/tag/disableable.rb +22 -0
- data/lib/liquid/tag/disabler.rb +13 -0
- data/lib/liquid/tag.rb +42 -6
- data/lib/liquid/tags/assign.rb +46 -18
- data/lib/liquid/tags/break.rb +15 -4
- data/lib/liquid/tags/capture.rb +26 -18
- data/lib/liquid/tags/case.rb +108 -32
- data/lib/liquid/tags/comment.rb +76 -4
- data/lib/liquid/tags/continue.rb +15 -13
- data/lib/liquid/tags/cycle.rb +117 -34
- data/lib/liquid/tags/decrement.rb +30 -23
- data/lib/liquid/tags/doc.rb +81 -0
- data/lib/liquid/tags/echo.rb +39 -0
- data/lib/liquid/tags/for.rb +109 -96
- data/lib/liquid/tags/if.rb +72 -41
- data/lib/liquid/tags/ifchanged.rb +10 -11
- data/lib/liquid/tags/include.rb +89 -63
- data/lib/liquid/tags/increment.rb +31 -20
- data/lib/liquid/tags/inline_comment.rb +28 -0
- data/lib/liquid/tags/raw.rb +25 -13
- data/lib/liquid/tags/render.rb +151 -0
- data/lib/liquid/tags/snippet.rb +45 -0
- data/lib/liquid/tags/table_row.rb +104 -21
- data/lib/liquid/tags/unless.rb +37 -20
- data/lib/liquid/tags.rb +51 -0
- data/lib/liquid/template.rb +90 -106
- data/lib/liquid/template_factory.rb +9 -0
- data/lib/liquid/tokenizer.rb +143 -13
- data/lib/liquid/usage.rb +8 -0
- data/lib/liquid/utils.rb +114 -5
- data/lib/liquid/variable.rb +119 -45
- data/lib/liquid/variable_lookup.rb +35 -13
- data/lib/liquid/version.rb +3 -1
- data/lib/liquid.rb +31 -18
- metadata +56 -107
- data/lib/liquid/strainer.rb +0 -66
- data/test/fixtures/en_locale.yml +0 -9
- data/test/integration/assign_test.rb +0 -48
- data/test/integration/blank_test.rb +0 -106
- data/test/integration/capture_test.rb +0 -50
- data/test/integration/context_test.rb +0 -32
- data/test/integration/document_test.rb +0 -19
- data/test/integration/drop_test.rb +0 -273
- data/test/integration/error_handling_test.rb +0 -260
- data/test/integration/filter_test.rb +0 -178
- data/test/integration/hash_ordering_test.rb +0 -23
- data/test/integration/output_test.rb +0 -123
- data/test/integration/parsing_quirks_test.rb +0 -118
- data/test/integration/render_profiling_test.rb +0 -154
- data/test/integration/security_test.rb +0 -66
- data/test/integration/standard_filter_test.rb +0 -535
- data/test/integration/tags/break_tag_test.rb +0 -15
- data/test/integration/tags/continue_tag_test.rb +0 -15
- data/test/integration/tags/for_tag_test.rb +0 -410
- data/test/integration/tags/if_else_tag_test.rb +0 -188
- data/test/integration/tags/include_tag_test.rb +0 -238
- data/test/integration/tags/increment_tag_test.rb +0 -23
- data/test/integration/tags/raw_tag_test.rb +0 -31
- data/test/integration/tags/standard_tag_test.rb +0 -296
- data/test/integration/tags/statements_test.rb +0 -111
- data/test/integration/tags/table_row_test.rb +0 -64
- data/test/integration/tags/unless_else_tag_test.rb +0 -26
- data/test/integration/template_test.rb +0 -323
- data/test/integration/trim_mode_test.rb +0 -525
- data/test/integration/variable_test.rb +0 -92
- data/test/test_helper.rb +0 -117
- data/test/unit/block_unit_test.rb +0 -58
- data/test/unit/condition_unit_test.rb +0 -158
- data/test/unit/context_unit_test.rb +0 -483
- data/test/unit/file_system_unit_test.rb +0 -35
- data/test/unit/i18n_unit_test.rb +0 -37
- data/test/unit/lexer_unit_test.rb +0 -51
- data/test/unit/parser_unit_test.rb +0 -82
- data/test/unit/regexp_unit_test.rb +0 -44
- data/test/unit/strainer_unit_test.rb +0 -148
- data/test/unit/tag_unit_test.rb +0 -21
- data/test/unit/tags/case_tag_unit_test.rb +0 -10
- data/test/unit/tags/for_tag_unit_test.rb +0 -13
- data/test/unit/tags/if_tag_unit_test.rb +0 -8
- data/test/unit/template_unit_test.rb +0 -78
- data/test/unit/tokenizer_unit_test.rb +0 -55
- data/test/unit/variable_unit_test.rb +0 -162
data/lib/liquid/block_body.rb
CHANGED
|
@@ -1,52 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'English'
|
|
4
|
+
|
|
1
5
|
module Liquid
|
|
2
6
|
class BlockBody
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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?
|
|
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
|
-
|
|
73
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
239
|
+
output
|
|
104
240
|
end
|
|
105
241
|
|
|
106
242
|
private
|
|
107
243
|
|
|
108
|
-
def render_node(
|
|
109
|
-
|
|
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.
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/liquid/condition.rb
CHANGED
|
@@ -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
|
|
11
|
+
class Condition # :nodoc:
|
|
10
12
|
@@operators = {
|
|
11
|
-
'=='
|
|
12
|
-
'!='
|
|
13
|
-
'<>'
|
|
14
|
-
'<'
|
|
15
|
-
'>'
|
|
16
|
-
'>='
|
|
17
|
-
'<='
|
|
18
|
-
'contains'
|
|
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
|
-
|
|
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
|
|
59
|
+
@left = left
|
|
37
60
|
@operator = operator
|
|
38
|
-
@right
|
|
61
|
+
@right = right
|
|
62
|
+
|
|
39
63
|
@child_relation = nil
|
|
40
64
|
@child_condition = nil
|
|
41
65
|
end
|
|
42
66
|
|
|
43
|
-
def evaluate(context =
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
87
|
+
@child_relation = :or
|
|
58
88
|
@child_condition = condition
|
|
59
89
|
end
|
|
60
90
|
|
|
61
91
|
def and(condition)
|
|
62
|
-
@child_relation
|
|
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(' '
|
|
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?(
|
|
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?(
|
|
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
|
|
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
|
|
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
|
|
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
|