liquid 4.0.0 → 5.0.1

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 (123) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +101 -2
  3. data/README.md +8 -0
  4. data/lib/liquid.rb +18 -5
  5. data/lib/liquid/block.rb +47 -20
  6. data/lib/liquid/block_body.rb +192 -76
  7. data/lib/liquid/condition.rb +69 -29
  8. data/lib/liquid/context.rb +110 -53
  9. data/lib/liquid/document.rb +47 -9
  10. data/lib/liquid/drop.rb +4 -2
  11. data/lib/liquid/errors.rb +20 -18
  12. data/lib/liquid/expression.rb +30 -31
  13. data/lib/liquid/extensions.rb +8 -0
  14. data/lib/liquid/file_system.rb +6 -4
  15. data/lib/liquid/forloop_drop.rb +11 -4
  16. data/lib/liquid/i18n.rb +5 -3
  17. data/lib/liquid/interrupts.rb +3 -1
  18. data/lib/liquid/lexer.rb +35 -26
  19. data/lib/liquid/locales/en.yml +4 -2
  20. data/lib/liquid/parse_context.rb +21 -4
  21. data/lib/liquid/parse_tree_visitor.rb +42 -0
  22. data/lib/liquid/parser.rb +30 -18
  23. data/lib/liquid/parser_switching.rb +17 -3
  24. data/lib/liquid/partial_cache.rb +24 -0
  25. data/lib/liquid/profiler.rb +67 -86
  26. data/lib/liquid/profiler/hooks.rb +26 -14
  27. data/lib/liquid/range_lookup.rb +5 -3
  28. data/lib/liquid/register.rb +6 -0
  29. data/lib/liquid/resource_limits.rb +47 -8
  30. data/lib/liquid/standardfilters.rb +170 -63
  31. data/lib/liquid/static_registers.rb +44 -0
  32. data/lib/liquid/strainer_factory.rb +36 -0
  33. data/lib/liquid/strainer_template.rb +53 -0
  34. data/lib/liquid/tablerowloop_drop.rb +6 -4
  35. data/lib/liquid/tag.rb +28 -6
  36. data/lib/liquid/tag/disableable.rb +22 -0
  37. data/lib/liquid/tag/disabler.rb +21 -0
  38. data/lib/liquid/tags/assign.rb +32 -10
  39. data/lib/liquid/tags/break.rb +8 -3
  40. data/lib/liquid/tags/capture.rb +11 -8
  41. data/lib/liquid/tags/case.rb +41 -27
  42. data/lib/liquid/tags/comment.rb +5 -3
  43. data/lib/liquid/tags/continue.rb +8 -3
  44. data/lib/liquid/tags/cycle.rb +35 -16
  45. data/lib/liquid/tags/decrement.rb +6 -3
  46. data/lib/liquid/tags/echo.rb +34 -0
  47. data/lib/liquid/tags/for.rb +79 -47
  48. data/lib/liquid/tags/if.rb +53 -30
  49. data/lib/liquid/tags/ifchanged.rb +11 -10
  50. data/lib/liquid/tags/include.rb +42 -44
  51. data/lib/liquid/tags/increment.rb +7 -3
  52. data/lib/liquid/tags/raw.rb +14 -11
  53. data/lib/liquid/tags/render.rb +84 -0
  54. data/lib/liquid/tags/table_row.rb +32 -20
  55. data/lib/liquid/tags/unless.rb +15 -15
  56. data/lib/liquid/template.rb +53 -72
  57. data/lib/liquid/template_factory.rb +9 -0
  58. data/lib/liquid/tokenizer.rb +17 -9
  59. data/lib/liquid/usage.rb +8 -0
  60. data/lib/liquid/utils.rb +6 -4
  61. data/lib/liquid/variable.rb +55 -38
  62. data/lib/liquid/variable_lookup.rb +14 -6
  63. data/lib/liquid/version.rb +3 -1
  64. data/test/integration/assign_test.rb +74 -5
  65. data/test/integration/blank_test.rb +11 -8
  66. data/test/integration/block_test.rb +58 -0
  67. data/test/integration/capture_test.rb +18 -10
  68. data/test/integration/context_test.rb +609 -5
  69. data/test/integration/document_test.rb +4 -2
  70. data/test/integration/drop_test.rb +67 -83
  71. data/test/integration/error_handling_test.rb +73 -61
  72. data/test/integration/expression_test.rb +46 -0
  73. data/test/integration/filter_test.rb +53 -42
  74. data/test/integration/hash_ordering_test.rb +5 -3
  75. data/test/integration/output_test.rb +26 -24
  76. data/test/integration/parsing_quirks_test.rb +24 -8
  77. data/test/integration/{render_profiling_test.rb → profiler_test.rb} +84 -25
  78. data/test/integration/security_test.rb +41 -18
  79. data/test/integration/standard_filter_test.rb +513 -210
  80. data/test/integration/tag/disableable_test.rb +59 -0
  81. data/test/integration/tag_test.rb +45 -0
  82. data/test/integration/tags/break_tag_test.rb +4 -2
  83. data/test/integration/tags/continue_tag_test.rb +4 -2
  84. data/test/integration/tags/echo_test.rb +13 -0
  85. data/test/integration/tags/for_tag_test.rb +109 -53
  86. data/test/integration/tags/if_else_tag_test.rb +5 -3
  87. data/test/integration/tags/include_tag_test.rb +83 -52
  88. data/test/integration/tags/increment_tag_test.rb +4 -2
  89. data/test/integration/tags/liquid_tag_test.rb +116 -0
  90. data/test/integration/tags/raw_tag_test.rb +14 -11
  91. data/test/integration/tags/render_tag_test.rb +213 -0
  92. data/test/integration/tags/standard_tag_test.rb +38 -31
  93. data/test/integration/tags/statements_test.rb +23 -21
  94. data/test/integration/tags/table_row_test.rb +2 -0
  95. data/test/integration/tags/unless_else_tag_test.rb +4 -2
  96. data/test/integration/template_test.rb +123 -120
  97. data/test/integration/trim_mode_test.rb +82 -44
  98. data/test/integration/variable_test.rb +46 -31
  99. data/test/test_helper.rb +75 -23
  100. data/test/unit/block_unit_test.rb +19 -24
  101. data/test/unit/condition_unit_test.rb +82 -72
  102. data/test/unit/file_system_unit_test.rb +6 -4
  103. data/test/unit/i18n_unit_test.rb +7 -5
  104. data/test/unit/lexer_unit_test.rb +12 -10
  105. data/test/unit/parse_tree_visitor_test.rb +254 -0
  106. data/test/unit/parser_unit_test.rb +37 -35
  107. data/test/unit/partial_cache_unit_test.rb +128 -0
  108. data/test/unit/regexp_unit_test.rb +17 -15
  109. data/test/unit/static_registers_unit_test.rb +156 -0
  110. data/test/unit/strainer_factory_unit_test.rb +100 -0
  111. data/test/unit/strainer_template_unit_test.rb +82 -0
  112. data/test/unit/tag_unit_test.rb +5 -3
  113. data/test/unit/tags/case_tag_unit_test.rb +3 -1
  114. data/test/unit/tags/for_tag_unit_test.rb +4 -2
  115. data/test/unit/tags/if_tag_unit_test.rb +3 -1
  116. data/test/unit/template_factory_unit_test.rb +12 -0
  117. data/test/unit/template_unit_test.rb +19 -10
  118. data/test/unit/tokenizer_unit_test.rb +26 -19
  119. data/test/unit/variable_unit_test.rb +51 -49
  120. metadata +79 -46
  121. data/lib/liquid/strainer.rb +0 -66
  122. data/test/unit/context_unit_test.rb +0 -483
  123. data/test/unit/strainer_unit_test.rb +0 -148
@@ -1,16 +1,24 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'cgi'
2
4
  require 'bigdecimal'
3
5
 
4
6
  module Liquid
5
7
  module StandardFilters
6
8
  HTML_ESCAPE = {
7
- '&'.freeze => '&'.freeze,
8
- '>'.freeze => '>'.freeze,
9
- '<'.freeze => '&lt;'.freeze,
10
- '"'.freeze => '&quot;'.freeze,
11
- "'".freeze => '&#39;'.freeze
12
- }
9
+ '&' => '&amp;',
10
+ '>' => '&gt;',
11
+ '<' => '&lt;',
12
+ '"' => '&quot;',
13
+ "'" => '&#39;',
14
+ }.freeze
13
15
  HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
16
+ STRIP_HTML_BLOCKS = Regexp.union(
17
+ %r{<script.*?</script>}m,
18
+ /<!--.*?-->/m,
19
+ %r{<style.*?</style>}m
20
+ )
21
+ STRIP_HTML_TAGS = /<.*?>/m
14
22
 
15
23
  # Return the size of an array or of an string
16
24
  def size(input)
@@ -33,7 +41,7 @@ module Liquid
33
41
  end
34
42
 
35
43
  def escape(input)
36
- CGI.escapeHTML(input).untaint unless input.nil?
44
+ CGI.escapeHTML(input.to_s) unless input.nil?
37
45
  end
38
46
  alias_method :h, :escape
39
47
 
@@ -42,11 +50,16 @@ module Liquid
42
50
  end
43
51
 
44
52
  def url_encode(input)
45
- CGI.escape(input) unless input.nil?
53
+ CGI.escape(input.to_s) unless input.nil?
46
54
  end
47
55
 
48
56
  def url_decode(input)
49
- CGI.unescape(input) unless input.nil?
57
+ return if input.nil?
58
+
59
+ result = CGI.unescape(input.to_s)
60
+ raise Liquid::ArgumentError, "invalid byte sequence in #{result.encoding}" unless result.valid_encoding?
61
+
62
+ result
50
63
  end
51
64
 
52
65
  def slice(input, offset, length = nil)
@@ -61,23 +74,30 @@ module Liquid
61
74
  end
62
75
 
63
76
  # Truncate a string down to x characters
64
- def truncate(input, length = 50, truncate_string = "...".freeze)
77
+ def truncate(input, length = 50, truncate_string = "...")
65
78
  return if input.nil?
66
79
  input_str = input.to_s
67
- length = Utils.to_integer(length)
80
+ length = Utils.to_integer(length)
81
+
68
82
  truncate_string_str = truncate_string.to_s
83
+
69
84
  l = length - truncate_string_str.length
70
85
  l = 0 if l < 0
71
- input_str.length > length ? input_str[0...l] + truncate_string_str : input_str
86
+
87
+ input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str
72
88
  end
73
89
 
74
- def truncatewords(input, words = 15, truncate_string = "...".freeze)
90
+ def truncatewords(input, words = 15, truncate_string = "...")
75
91
  return if input.nil?
76
- wordlist = input.to_s.split
92
+ input = input.to_s
77
93
  words = Utils.to_integer(words)
78
- l = words - 1
79
- l = 0 if l < 0
80
- wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string.to_s : input
94
+ words = 1 if words <= 0
95
+
96
+ wordlist = input.split(" ", words + 1)
97
+ return input if wordlist.length <= words
98
+
99
+ wordlist.pop
100
+ wordlist.join(" ").concat(truncate_string.to_s)
81
101
  end
82
102
 
83
103
  # Split input string into an array of substrings separated by given pattern.
@@ -102,37 +122,38 @@ module Liquid
102
122
  end
103
123
 
104
124
  def strip_html(input)
105
- empty = ''.freeze
106
- input.to_s.gsub(/<script.*?<\/script>/m, empty).gsub(/<!--.*?-->/m, empty).gsub(/<style.*?<\/style>/m, empty).gsub(/<.*?>/m, empty)
125
+ empty = ''
126
+ result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
127
+ result.gsub!(STRIP_HTML_TAGS, empty)
128
+ result
107
129
  end
108
130
 
109
131
  # Remove all newlines from the string
110
132
  def strip_newlines(input)
111
- input.to_s.gsub(/\r?\n/, ''.freeze)
133
+ input.to_s.gsub(/\r?\n/, '')
112
134
  end
113
135
 
114
136
  # Join elements of the array with certain character between them
115
- def join(input, glue = ' '.freeze)
116
- InputIterator.new(input).join(glue)
137
+ def join(input, glue = ' ')
138
+ InputIterator.new(input, context).join(glue)
117
139
  end
118
140
 
119
141
  # Sort elements of the array
120
142
  # provide optional property with which to sort an array of hashes or drops
121
143
  def sort(input, property = nil)
122
- ary = InputIterator.new(input)
144
+ ary = InputIterator.new(input, context)
145
+
146
+ return [] if ary.empty?
147
+
123
148
  if property.nil?
124
- ary.sort
125
- elsif ary.empty? # The next two cases assume a non-empty array.
126
- []
127
- elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
128
149
  ary.sort do |a, b|
129
- a = a[property]
130
- b = b[property]
131
- if a && b
132
- a <=> b
133
- else
134
- a ? -1 : 1
135
- end
150
+ nil_safe_compare(a, b)
151
+ end
152
+ elsif ary.all? { |el| el.respond_to?(:[]) }
153
+ begin
154
+ ary.sort { |a, b| nil_safe_compare(a[property], b[property]) }
155
+ rescue TypeError
156
+ raise_property_error(property)
136
157
  end
137
158
  end
138
159
  end
@@ -140,83 +161,121 @@ module Liquid
140
161
  # Sort elements of an array ignoring case if strings
141
162
  # provide optional property with which to sort an array of hashes or drops
142
163
  def sort_natural(input, property = nil)
143
- ary = InputIterator.new(input)
164
+ ary = InputIterator.new(input, context)
165
+
166
+ return [] if ary.empty?
144
167
 
145
168
  if property.nil?
146
- ary.sort { |a, b| a.casecmp(b) }
147
- elsif ary.empty? # The next two cases assume a non-empty array.
169
+ ary.sort do |a, b|
170
+ nil_safe_casecmp(a, b)
171
+ end
172
+ elsif ary.all? { |el| el.respond_to?(:[]) }
173
+ begin
174
+ ary.sort { |a, b| nil_safe_casecmp(a[property], b[property]) }
175
+ rescue TypeError
176
+ raise_property_error(property)
177
+ end
178
+ end
179
+ end
180
+
181
+ # Filter the elements of an array to those with a certain property value.
182
+ # By default the target is any truthy value.
183
+ def where(input, property, target_value = nil)
184
+ ary = InputIterator.new(input, context)
185
+
186
+ if ary.empty?
148
187
  []
149
- elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
150
- ary.sort { |a, b| a[property].casecmp(b[property]) }
188
+ elsif ary.first.respond_to?(:[]) && target_value.nil?
189
+ begin
190
+ ary.select { |item| item[property] }
191
+ rescue TypeError
192
+ raise_property_error(property)
193
+ end
194
+ elsif ary.first.respond_to?(:[])
195
+ begin
196
+ ary.select { |item| item[property] == target_value }
197
+ rescue TypeError
198
+ raise_property_error(property)
199
+ end
151
200
  end
152
201
  end
153
202
 
154
203
  # Remove duplicate elements from an array
155
204
  # provide optional property with which to determine uniqueness
156
205
  def uniq(input, property = nil)
157
- ary = InputIterator.new(input)
206
+ ary = InputIterator.new(input, context)
158
207
 
159
208
  if property.nil?
160
209
  ary.uniq
161
210
  elsif ary.empty? # The next two cases assume a non-empty array.
162
211
  []
163
212
  elsif ary.first.respond_to?(:[])
164
- ary.uniq{ |a| a[property] }
213
+ begin
214
+ ary.uniq { |a| a[property] }
215
+ rescue TypeError
216
+ raise_property_error(property)
217
+ end
165
218
  end
166
219
  end
167
220
 
168
221
  # Reverse the elements of an array
169
222
  def reverse(input)
170
- ary = InputIterator.new(input)
223
+ ary = InputIterator.new(input, context)
171
224
  ary.reverse
172
225
  end
173
226
 
174
227
  # map/collect on a given property
175
228
  def map(input, property)
176
- InputIterator.new(input).map do |e|
229
+ InputIterator.new(input, context).map do |e|
177
230
  e = e.call if e.is_a?(Proc)
178
231
 
179
- if property == "to_liquid".freeze
232
+ if property == "to_liquid"
180
233
  e
181
234
  elsif e.respond_to?(:[])
182
235
  r = e[property]
183
236
  r.is_a?(Proc) ? r.call : r
184
237
  end
185
238
  end
239
+ rescue TypeError
240
+ raise_property_error(property)
186
241
  end
187
242
 
188
243
  # Remove nils within an array
189
244
  # provide optional property with which to check for nil
190
245
  def compact(input, property = nil)
191
- ary = InputIterator.new(input)
246
+ ary = InputIterator.new(input, context)
192
247
 
193
248
  if property.nil?
194
249
  ary.compact
195
250
  elsif ary.empty? # The next two cases assume a non-empty array.
196
251
  []
197
252
  elsif ary.first.respond_to?(:[])
198
- ary.reject{ |a| a[property].nil? }
253
+ begin
254
+ ary.reject { |a| a[property].nil? }
255
+ rescue TypeError
256
+ raise_property_error(property)
257
+ end
199
258
  end
200
259
  end
201
260
 
202
261
  # Replace occurrences of a string with another
203
- def replace(input, string, replacement = ''.freeze)
262
+ def replace(input, string, replacement = '')
204
263
  input.to_s.gsub(string.to_s, replacement.to_s)
205
264
  end
206
265
 
207
266
  # Replace the first occurrences of a string with another
208
- def replace_first(input, string, replacement = ''.freeze)
267
+ def replace_first(input, string, replacement = '')
209
268
  input.to_s.sub(string.to_s, replacement.to_s)
210
269
  end
211
270
 
212
271
  # remove a substring
213
272
  def remove(input, string)
214
- input.to_s.gsub(string.to_s, ''.freeze)
273
+ input.to_s.gsub(string.to_s, '')
215
274
  end
216
275
 
217
276
  # remove the first occurrences of a substring
218
277
  def remove_first(input, string)
219
- input.to_s.sub(string.to_s, ''.freeze)
278
+ input.to_s.sub(string.to_s, '')
220
279
  end
221
280
 
222
281
  # add one string to another
@@ -226,9 +285,9 @@ module Liquid
226
285
 
227
286
  def concat(input, array)
228
287
  unless array.respond_to?(:to_ary)
229
- raise ArgumentError.new("concat filter requires an array argument")
288
+ raise ArgumentError, "concat filter requires an array argument"
230
289
  end
231
- InputIterator.new(input).concat(array)
290
+ InputIterator.new(input, context).concat(array)
232
291
  end
233
292
 
234
293
  # prepend a string to another
@@ -238,7 +297,7 @@ module Liquid
238
297
 
239
298
  # Add <br /> tags in front of all newlines in input string
240
299
  def newline_to_br(input)
241
- input.to_s.gsub(/\n/, "<br />\n".freeze)
300
+ input.to_s.gsub(/\r?\n/, "<br />\n")
242
301
  end
243
302
 
244
303
  # Reformat a date using Ruby's core Time#strftime( string ) -> string
@@ -275,7 +334,7 @@ module Liquid
275
334
  def date(input, format)
276
335
  return input if format.to_s.empty?
277
336
 
278
- return input unless date = Utils.to_date(input)
337
+ return input unless (date = Utils.to_date(input))
279
338
 
280
339
  date.strftime(format.to_s)
281
340
  end
@@ -353,26 +412,73 @@ module Liquid
353
412
  raise Liquid::FloatDomainError, e.message
354
413
  end
355
414
 
356
- def default(input, default_value = ''.freeze)
357
- if !input || input.respond_to?(:empty?) && input.empty?
358
- default_value
359
- else
360
- input
361
- end
415
+ def at_least(input, n)
416
+ min_value = Utils.to_number(n)
417
+
418
+ result = Utils.to_number(input)
419
+ result = min_value if min_value > result
420
+ result.is_a?(BigDecimal) ? result.to_f : result
421
+ end
422
+
423
+ def at_most(input, n)
424
+ max_value = Utils.to_number(n)
425
+
426
+ result = Utils.to_number(input)
427
+ result = max_value if max_value < result
428
+ result.is_a?(BigDecimal) ? result.to_f : result
429
+ end
430
+
431
+ # Set a default value when the input is nil, false or empty
432
+ #
433
+ # Example:
434
+ # {{ product.title | default: "No Title" }}
435
+ #
436
+ # Use `allow_false` when an input should only be tested against nil or empty and not false.
437
+ #
438
+ # Example:
439
+ # {{ product.title | default: "No Title", allow_false: true }}
440
+ #
441
+ def default(input, default_value = '', options = {})
442
+ options = {} unless options.is_a?(Hash)
443
+ false_check = options['allow_false'] ? input.nil? : !input
444
+ false_check || (input.respond_to?(:empty?) && input.empty?) ? default_value : input
362
445
  end
363
446
 
364
447
  private
365
448
 
449
+ attr_reader :context
450
+
451
+ def raise_property_error(property)
452
+ raise Liquid::ArgumentError, "cannot select the property '#{property}'"
453
+ end
454
+
366
455
  def apply_operation(input, operand, operation)
367
456
  result = Utils.to_number(input).send(operation, Utils.to_number(operand))
368
457
  result.is_a?(BigDecimal) ? result.to_f : result
369
458
  end
370
459
 
460
+ def nil_safe_compare(a, b)
461
+ if !a.nil? && !b.nil?
462
+ a <=> b
463
+ else
464
+ a.nil? ? 1 : -1
465
+ end
466
+ end
467
+
468
+ def nil_safe_casecmp(a, b)
469
+ if !a.nil? && !b.nil?
470
+ a.to_s.casecmp(b.to_s)
471
+ else
472
+ a.nil? ? 1 : -1
473
+ end
474
+ end
475
+
371
476
  class InputIterator
372
477
  include Enumerable
373
478
 
374
- def initialize(input)
375
- @input = if input.is_a?(Array)
479
+ def initialize(input, context)
480
+ @context = context
481
+ @input = if input.is_a?(Array)
376
482
  input.flatten
377
483
  elsif input.is_a?(Hash)
378
484
  [input]
@@ -384,7 +490,7 @@ module Liquid
384
490
  end
385
491
 
386
492
  def join(glue)
387
- to_a.join(glue)
493
+ to_a.join(glue.to_s)
388
494
  end
389
495
 
390
496
  def concat(args)
@@ -410,6 +516,7 @@ module Liquid
410
516
 
411
517
  def each
412
518
  @input.each do |e|
519
+ e.context = @context if e.respond_to?(:context=)
413
520
  yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
414
521
  end
415
522
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ class StaticRegisters
5
+ attr_reader :static
6
+
7
+ def initialize(registers = {})
8
+ @static = registers.is_a?(StaticRegisters) ? registers.static : registers
9
+ @registers = {}
10
+ end
11
+
12
+ def []=(key, value)
13
+ @registers[key] = value
14
+ end
15
+
16
+ def [](key)
17
+ if @registers.key?(key)
18
+ @registers[key]
19
+ else
20
+ @static[key]
21
+ end
22
+ end
23
+
24
+ def delete(key)
25
+ @registers.delete(key)
26
+ end
27
+
28
+ UNDEFINED = Object.new
29
+
30
+ def fetch(key, default = UNDEFINED, &block)
31
+ if @registers.key?(key)
32
+ @registers.fetch(key)
33
+ elsif default != UNDEFINED
34
+ @static.fetch(key, default, &block)
35
+ else
36
+ @static.fetch(key, &block)
37
+ end
38
+ end
39
+
40
+ def key?(key)
41
+ @registers.key?(key) || @static.key?(key)
42
+ end
43
+ end
44
+ end