liquid 2.6.3 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +272 -26
  3. data/README.md +67 -3
  4. data/lib/liquid/block.rb +62 -94
  5. data/lib/liquid/block_body.rb +255 -0
  6. data/lib/liquid/condition.rb +96 -38
  7. data/lib/liquid/context.rb +172 -154
  8. data/lib/liquid/document.rb +57 -9
  9. data/lib/liquid/drop.rb +33 -14
  10. data/lib/liquid/errors.rb +56 -10
  11. data/lib/liquid/expression.rb +45 -0
  12. data/lib/liquid/extensions.rb +21 -7
  13. data/lib/liquid/file_system.rb +27 -14
  14. data/lib/liquid/forloop_drop.rb +92 -0
  15. data/lib/liquid/i18n.rb +41 -0
  16. data/lib/liquid/interrupts.rb +3 -2
  17. data/lib/liquid/lexer.rb +62 -0
  18. data/lib/liquid/locales/en.yml +29 -0
  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 +102 -0
  22. data/lib/liquid/parser_switching.rb +45 -0
  23. data/lib/liquid/partial_cache.rb +24 -0
  24. data/lib/liquid/profiler/hooks.rb +35 -0
  25. data/lib/liquid/profiler.rb +139 -0
  26. data/lib/liquid/range_lookup.rb +47 -0
  27. data/lib/liquid/registers.rb +51 -0
  28. data/lib/liquid/resource_limits.rb +62 -0
  29. data/lib/liquid/standardfilters.rb +789 -118
  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 +49 -10
  36. data/lib/liquid/tags/assign.rb +61 -19
  37. data/lib/liquid/tags/break.rb +14 -4
  38. data/lib/liquid/tags/capture.rb +29 -21
  39. data/lib/liquid/tags/case.rb +80 -31
  40. data/lib/liquid/tags/comment.rb +24 -2
  41. data/lib/liquid/tags/continue.rb +14 -13
  42. data/lib/liquid/tags/cycle.rb +50 -32
  43. data/lib/liquid/tags/decrement.rb +24 -26
  44. data/lib/liquid/tags/echo.rb +41 -0
  45. data/lib/liquid/tags/for.rb +164 -100
  46. data/lib/liquid/tags/if.rb +105 -44
  47. data/lib/liquid/tags/ifchanged.rb +10 -11
  48. data/lib/liquid/tags/include.rb +85 -65
  49. data/lib/liquid/tags/increment.rb +24 -22
  50. data/lib/liquid/tags/inline_comment.rb +43 -0
  51. data/lib/liquid/tags/raw.rb +50 -11
  52. data/lib/liquid/tags/render.rb +109 -0
  53. data/lib/liquid/tags/table_row.rb +88 -0
  54. data/lib/liquid/tags/unless.rb +37 -21
  55. data/lib/liquid/template.rb +124 -46
  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 +68 -5
  60. data/lib/liquid/variable.rb +128 -32
  61. data/lib/liquid/variable_lookup.rb +96 -0
  62. data/lib/liquid/version.rb +3 -1
  63. data/lib/liquid.rb +36 -13
  64. metadata +69 -77
  65. data/lib/extras/liquid_view.rb +0 -51
  66. data/lib/liquid/htmltags.rb +0 -73
  67. data/lib/liquid/module_ex.rb +0 -62
  68. data/lib/liquid/strainer.rb +0 -53
  69. data/test/liquid/assign_test.rb +0 -21
  70. data/test/liquid/block_test.rb +0 -58
  71. data/test/liquid/capture_test.rb +0 -40
  72. data/test/liquid/condition_test.rb +0 -127
  73. data/test/liquid/context_test.rb +0 -478
  74. data/test/liquid/drop_test.rb +0 -180
  75. data/test/liquid/error_handling_test.rb +0 -81
  76. data/test/liquid/file_system_test.rb +0 -29
  77. data/test/liquid/filter_test.rb +0 -125
  78. data/test/liquid/hash_ordering_test.rb +0 -25
  79. data/test/liquid/module_ex_test.rb +0 -87
  80. data/test/liquid/output_test.rb +0 -116
  81. data/test/liquid/parsing_quirks_test.rb +0 -52
  82. data/test/liquid/regexp_test.rb +0 -44
  83. data/test/liquid/security_test.rb +0 -64
  84. data/test/liquid/standard_filter_test.rb +0 -263
  85. data/test/liquid/strainer_test.rb +0 -52
  86. data/test/liquid/tags/break_tag_test.rb +0 -16
  87. data/test/liquid/tags/continue_tag_test.rb +0 -16
  88. data/test/liquid/tags/for_tag_test.rb +0 -297
  89. data/test/liquid/tags/html_tag_test.rb +0 -63
  90. data/test/liquid/tags/if_else_tag_test.rb +0 -166
  91. data/test/liquid/tags/include_tag_test.rb +0 -166
  92. data/test/liquid/tags/increment_tag_test.rb +0 -24
  93. data/test/liquid/tags/raw_tag_test.rb +0 -24
  94. data/test/liquid/tags/standard_tag_test.rb +0 -295
  95. data/test/liquid/tags/statements_test.rb +0 -134
  96. data/test/liquid/tags/unless_else_tag_test.rb +0 -26
  97. data/test/liquid/template_test.rb +0 -146
  98. data/test/liquid/variable_test.rb +0 -186
  99. data/test/test_helper.rb +0 -29
  100. /data/{MIT-LICENSE → LICENSE} +0 -0
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+
5
+ module Liquid
6
+ class BlockBody
7
+ LiquidTagToken = /\A\s*(#{TagName})\s*(.*?)\z/o
8
+ FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(#{TagName})(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
9
+ ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
10
+ WhitespaceOrNothing = /\A\s*\z/
11
+ TAGSTART = "{%"
12
+ VARSTART = "{{"
13
+
14
+ attr_reader :nodelist
15
+
16
+ def initialize
17
+ @nodelist = []
18
+ @blank = true
19
+ end
20
+
21
+ def parse(tokenizer, parse_context, &block)
22
+ raise FrozenError, "can't modify frozen Liquid::BlockBody" if frozen?
23
+
24
+ parse_context.line_number = tokenizer.line_number
25
+
26
+ if tokenizer.for_liquid_tag
27
+ parse_for_liquid_tag(tokenizer, parse_context, &block)
28
+ else
29
+ parse_for_document(tokenizer, parse_context, &block)
30
+ end
31
+ end
32
+
33
+ def freeze
34
+ @nodelist.freeze
35
+ super
36
+ end
37
+
38
+ private def parse_for_liquid_tag(tokenizer, parse_context)
39
+ while (token = tokenizer.shift)
40
+ unless token.empty? || token.match?(WhitespaceOrNothing)
41
+ unless token =~ LiquidTagToken
42
+ # line isn't empty but didn't match tag syntax, yield and let the
43
+ # caller raise a syntax error
44
+ return yield token, token
45
+ end
46
+ tag_name = Regexp.last_match(1)
47
+ markup = Regexp.last_match(2)
48
+ unless (tag = registered_tags[tag_name])
49
+ # end parsing if we reach an unknown tag and let the caller decide
50
+ # determine how to proceed
51
+ return yield tag_name, markup
52
+ end
53
+ new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
54
+ @blank &&= new_tag.blank?
55
+ @nodelist << new_tag
56
+ end
57
+ parse_context.line_number = tokenizer.line_number
58
+ end
59
+
60
+ yield nil, nil
61
+ end
62
+
63
+ # @api private
64
+ def self.unknown_tag_in_liquid_tag(tag, parse_context)
65
+ Block.raise_unknown_tag(tag, 'liquid', '%}', parse_context)
66
+ end
67
+
68
+ # @api private
69
+ def self.raise_missing_tag_terminator(token, parse_context)
70
+ raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect)
71
+ end
72
+
73
+ # @api private
74
+ def self.raise_missing_variable_terminator(token, parse_context)
75
+ raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect)
76
+ end
77
+
78
+ # @api private
79
+ def self.render_node(context, output, node)
80
+ node.render_to_output_buffer(context, output)
81
+ rescue => exc
82
+ blank_tag = !node.instance_of?(Variable) && node.blank?
83
+ rescue_render_node(context, output, node.line_number, exc, blank_tag)
84
+ end
85
+
86
+ # @api private
87
+ def self.rescue_render_node(context, output, line_number, exc, blank_tag)
88
+ case exc
89
+ when MemoryError
90
+ raise
91
+ when UndefinedVariable, UndefinedDropMethod, UndefinedFilter
92
+ context.handle_error(exc, line_number)
93
+ else
94
+ error_message = context.handle_error(exc, line_number)
95
+ unless blank_tag # conditional for backwards compatibility
96
+ output << error_message
97
+ end
98
+ end
99
+ end
100
+
101
+ private def parse_liquid_tag(markup, parse_context)
102
+ liquid_tag_tokenizer = parse_context.new_tokenizer(
103
+ markup, start_line_number: parse_context.line_number, for_liquid_tag: true
104
+ )
105
+ parse_for_liquid_tag(liquid_tag_tokenizer, parse_context) do |end_tag_name, _end_tag_markup|
106
+ if end_tag_name
107
+ BlockBody.unknown_tag_in_liquid_tag(end_tag_name, parse_context)
108
+ end
109
+ end
110
+ end
111
+
112
+ private def parse_for_document(tokenizer, parse_context)
113
+ while (token = tokenizer.shift)
114
+ next if token.empty?
115
+ case
116
+ when token.start_with?(TAGSTART)
117
+ whitespace_handler(token, parse_context)
118
+ unless token =~ FullToken
119
+ BlockBody.raise_missing_tag_terminator(token, parse_context)
120
+ end
121
+ tag_name = Regexp.last_match(2)
122
+ markup = Regexp.last_match(4)
123
+
124
+ if parse_context.line_number
125
+ # newlines inside the tag should increase the line number,
126
+ # particularly important for multiline {% liquid %} tags
127
+ parse_context.line_number += Regexp.last_match(1).count("\n") + Regexp.last_match(3).count("\n")
128
+ end
129
+
130
+ if tag_name == 'liquid'
131
+ parse_liquid_tag(markup, parse_context)
132
+ next
133
+ end
134
+
135
+ unless (tag = registered_tags[tag_name])
136
+ # end parsing if we reach an unknown tag and let the caller decide
137
+ # determine how to proceed
138
+ return yield tag_name, markup
139
+ end
140
+ new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
141
+ @blank &&= new_tag.blank?
142
+ @nodelist << new_tag
143
+ when token.start_with?(VARSTART)
144
+ whitespace_handler(token, parse_context)
145
+ @nodelist << create_variable(token, parse_context)
146
+ @blank = false
147
+ else
148
+ if parse_context.trim_whitespace
149
+ token.lstrip!
150
+ end
151
+ parse_context.trim_whitespace = false
152
+ @nodelist << token
153
+ @blank &&= token.match?(WhitespaceOrNothing)
154
+ end
155
+ parse_context.line_number = tokenizer.line_number
156
+ end
157
+
158
+ yield nil, nil
159
+ end
160
+
161
+ def whitespace_handler(token, parse_context)
162
+ if token[2] == WhitespaceControl
163
+ previous_token = @nodelist.last
164
+ if previous_token.is_a?(String)
165
+ first_byte = previous_token.getbyte(0)
166
+ previous_token.rstrip!
167
+ if previous_token.empty? && parse_context[:bug_compatible_whitespace_trimming] && first_byte
168
+ previous_token << first_byte
169
+ end
170
+ end
171
+ end
172
+ parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
173
+ end
174
+
175
+ def blank?
176
+ @blank
177
+ end
178
+
179
+ # Remove blank strings in the block body for a control flow tag (e.g. `if`, `for`, `case`, `unless`)
180
+ # with a blank body.
181
+ #
182
+ # For example, in a conditional assignment like the following
183
+ #
184
+ # ```
185
+ # {% if size > max_size %}
186
+ # {% assign size = max_size %}
187
+ # {% endif %}
188
+ # ```
189
+ #
190
+ # we assume the intention wasn't to output the blank spaces in the `if` tag's block body, so this method
191
+ # will remove them to reduce the render output size.
192
+ #
193
+ # Note that it is now preferred to use the `liquid` tag for this use case.
194
+ def remove_blank_strings
195
+ raise "remove_blank_strings only support being called on a blank block body" unless @blank
196
+ @nodelist.reject! { |node| node.instance_of?(String) }
197
+ end
198
+
199
+ def render(context)
200
+ render_to_output_buffer(context, +'')
201
+ end
202
+
203
+ def render_to_output_buffer(context, output)
204
+ freeze unless frozen?
205
+
206
+ context.resource_limits.increment_render_score(@nodelist.length)
207
+
208
+ idx = 0
209
+ while (node = @nodelist[idx])
210
+ if node.instance_of?(String)
211
+ output << node
212
+ else
213
+ render_node(context, output, node)
214
+ # If we get an Interrupt that means the block must stop processing. An
215
+ # Interrupt is any command that stops block execution such as {% break %}
216
+ # or {% continue %}. These tags may also occur through Block or Include tags.
217
+ break if context.interrupt? # might have happened in a for-block
218
+ end
219
+ idx += 1
220
+
221
+ context.resource_limits.increment_write_score(output)
222
+ end
223
+
224
+ output
225
+ end
226
+
227
+ private
228
+
229
+ def render_node(context, output, node)
230
+ BlockBody.render_node(context, output, node)
231
+ end
232
+
233
+ def create_variable(token, parse_context)
234
+ if token =~ ContentOfVariable
235
+ markup = Regexp.last_match(1)
236
+ return Variable.new(markup, parse_context)
237
+ end
238
+ BlockBody.raise_missing_variable_terminator(token, parse_context)
239
+ end
240
+
241
+ # @deprecated Use {.raise_missing_tag_terminator} instead
242
+ def raise_missing_tag_terminator(token, parse_context)
243
+ BlockBody.raise_missing_tag_terminator(token, parse_context)
244
+ end
245
+
246
+ # @deprecated Use {.raise_missing_variable_terminator} instead
247
+ def raise_missing_variable_terminator(token, parse_context)
248
+ BlockBody.raise_missing_variable_terminator(token, parse_context)
249
+ end
250
+
251
+ def registered_tags
252
+ Template.tags
253
+ end
254
+ end
255
+ end
@@ -1,55 +1,93 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  # Container for liquid nodes which conveniently wraps decision making logic
3
5
  #
4
6
  # Example:
5
7
  #
6
- # c = Condition.new('1', '==', '1')
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
- '==' => lambda { |cond, left, right| cond.send(:equal_variables, left, right) },
12
- '!=' => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
13
- '<>' => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
14
- '<' => :<,
15
- '>' => :>,
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
+ '>' => :>,
16
18
  '>=' => :>=,
17
19
  '<=' => :<=,
18
- 'contains' => lambda { |cond, left, right| left && right ? left.include?(right) : false }
20
+ 'contains' => lambda do |_cond, left, right|
21
+ if left && right && left.respond_to?(:include?)
22
+ right = right.to_s if left.is_a?(String)
23
+ left.include?(right)
24
+ else
25
+ false
26
+ end
27
+ end,
28
+ }
29
+
30
+ class MethodLiteral
31
+ attr_reader :method_name, :to_s
32
+
33
+ def initialize(method_name, to_s)
34
+ @method_name = method_name
35
+ @to_s = to_s
36
+ end
37
+ end
38
+
39
+ @@method_literals = {
40
+ 'blank' => MethodLiteral.new(:blank?, '').freeze,
41
+ 'empty' => MethodLiteral.new(:empty?, '').freeze,
19
42
  }
20
43
 
21
44
  def self.operators
22
45
  @@operators
23
46
  end
24
47
 
25
- attr_reader :attachment
48
+ def self.parse_expression(parse_context, markup)
49
+ @@method_literals[markup] || parse_context.parse_expression(markup)
50
+ end
51
+
52
+ attr_reader :attachment, :child_condition
26
53
  attr_accessor :left, :operator, :right
27
54
 
28
55
  def initialize(left = nil, operator = nil, right = nil)
29
- @left, @operator, @right = left, operator, right
56
+ @left = left
57
+ @operator = operator
58
+ @right = right
59
+
30
60
  @child_relation = nil
31
61
  @child_condition = nil
32
62
  end
33
63
 
34
- def evaluate(context = Context.new)
35
- result = interpret_condition(left, right, operator, context)
36
-
37
- case @child_relation
38
- when :or
39
- result || @child_condition.evaluate(context)
40
- when :and
41
- result && @child_condition.evaluate(context)
42
- else
43
- result
64
+ def evaluate(context = deprecated_default_context)
65
+ condition = self
66
+ result = nil
67
+ loop do
68
+ result = interpret_condition(condition.left, condition.right, condition.operator, context)
69
+
70
+ case condition.child_relation
71
+ when :or
72
+ break if result
73
+ when :and
74
+ break unless result
75
+ else
76
+ break
77
+ end
78
+ condition = condition.child_condition
44
79
  end
80
+ result
45
81
  end
46
82
 
47
83
  def or(condition)
48
- @child_relation, @child_condition = :or, condition
84
+ @child_relation = :or
85
+ @child_condition = condition
49
86
  end
50
87
 
51
88
  def and(condition)
52
- @child_relation, @child_condition = :and, condition
89
+ @child_relation = :and
90
+ @child_condition = condition
53
91
  end
54
92
 
55
93
  def attach(attachment)
@@ -64,20 +102,24 @@ module Liquid
64
102
  "#<Condition #{[@left, @operator, @right].compact.join(' ')}>"
65
103
  end
66
104
 
105
+ protected
106
+
107
+ attr_reader :child_relation
108
+
67
109
  private
68
110
 
69
111
  def equal_variables(left, right)
70
- if left.is_a?(Symbol)
71
- if right.respond_to?(left)
72
- return right.send(left.to_s)
112
+ if left.is_a?(MethodLiteral)
113
+ if right.respond_to?(left.method_name)
114
+ return right.send(left.method_name)
73
115
  else
74
116
  return nil
75
117
  end
76
118
  end
77
119
 
78
- if right.is_a?(Symbol)
79
- if left.respond_to?(right)
80
- return left.send(right.to_s)
120
+ if right.is_a?(MethodLiteral)
121
+ if left.respond_to?(right.method_name)
122
+ return left.send(right.method_name)
81
123
  else
82
124
  return nil
83
125
  end
@@ -90,31 +132,47 @@ module Liquid
90
132
  # If the operator is empty this means that the decision statement is just
91
133
  # a single variable. We can just poll this variable from the context and
92
134
  # return this as the result.
93
- return context[left] if op == nil
135
+ return context.evaluate(left) if op.nil?
94
136
 
95
- left, right = context[left], context[right]
137
+ left = Liquid::Utils.to_liquid_value(context.evaluate(left))
138
+ right = Liquid::Utils.to_liquid_value(context.evaluate(right))
96
139
 
97
- operation = self.class.operators[op] || raise(ArgumentError.new("Unknown operator #{op}"))
140
+ operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}")
98
141
 
99
142
  if operation.respond_to?(:call)
100
143
  operation.call(self, left, right)
101
- elsif left.respond_to?(operation) and right.respond_to?(operation)
102
- left.send(operation, right)
103
- else
104
- nil
144
+ elsif left.respond_to?(operation) && right.respond_to?(operation) && !left.is_a?(Hash) && !right.is_a?(Hash)
145
+ begin
146
+ left.send(operation, right)
147
+ rescue ::ArgumentError => e
148
+ raise Liquid::ArgumentError, e.message
149
+ end
105
150
  end
106
151
  end
107
- end
108
152
 
153
+ def deprecated_default_context
154
+ warn("DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \
155
+ " and will be removed from Liquid 6.0.0.")
156
+ Context.new
157
+ end
158
+
159
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
160
+ def children
161
+ [
162
+ @node.left, @node.right,
163
+ @node.child_condition, @node.attachment
164
+ ].compact
165
+ end
166
+ end
167
+ end
109
168
 
110
169
  class ElseCondition < Condition
111
170
  def else?
112
171
  true
113
172
  end
114
173
 
115
- def evaluate(context)
174
+ def evaluate(_context)
116
175
  true
117
176
  end
118
177
  end
119
-
120
178
  end