liquid 5.6.1 → 5.8.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f56c8d1a7b6c038267d7e838aae2d8dba5c99e5fbb563d1163966cbbc9b60673
4
- data.tar.gz: c26e7a9288ee25b325ff6f2db4d21b8ed4f9ad97eb64f1aebeefd971e5c5bc46
3
+ metadata.gz: b3b75c321445b52cb025c14359defc6f1c5c38662f4cc49fdb25665578d7e0bd
4
+ data.tar.gz: c8b444e6a3848fbe278c8208feca605b788417aba56d2091e39cc6cf427f703b
5
5
  SHA512:
6
- metadata.gz: a81801d7e2976bd5825e19349a410c5800e85c30d31502da013cab9b95fff5cdeb7debcd9e7ccedee4a3056456ed3433a3c8751258f89c9a24c8b46a223e2f19
7
- data.tar.gz: b81704d1039698ce2e0391ad2bfbb6d69daba677dfbc1d4180bb7b6a4018981ed7984af44b64de84d8c745561cacf86999a1f326d1045296d696c5b62edd8849
6
+ metadata.gz: 44360766b328bd38396bf8a0fa0a37f6808f955804fb0208d415d01ef5370ad3504adb8fa204e1df16fb28917e96588a0d51bcb21a46607a0c350b1434f7ea35
7
+ data.tar.gz: 59de1d715d6a9dcff97600e160c0827bd83a6e4f36f92d546c8f6d7734103ffcbe0477d829a8a3c397db8bde5ca56fab13b981997e1222f734794c23303e29e4
data/History.md CHANGED
@@ -1,11 +1,76 @@
1
1
  # Liquid Change Log
2
2
 
3
- ## 5.6.0 (unreleased)
3
+ ## 5.8.7
4
+ * Expose body content in the `Doc` tag [James Meng]
5
+
6
+ ## 5.8.1
7
+
8
+ * Fix `{% doc %}` tag to be visitable [Guilherme Carreiro]
9
+
10
+ ## 5.8.0
11
+
12
+ * Introduce the new `{% doc %}` tag [Guilherme Carreiro]
13
+
14
+ ## 5.7.3
15
+
16
+ * Raise Liquid::SyntaxError when parsing invalidly encoded strings [Chris AtLee]
17
+
18
+ ## 5.7.2 2025-01-31
19
+
20
+ * Fix array filters to not support nested properties [Guilherme Carreiro]
21
+
22
+ ## 5.7.1 2025-01-24
23
+
24
+ * Fix the `find` and `find_index`filters to return `nil` when filtering empty arrays [Guilherme Carreiro]
25
+ * Fix the `has` filter to return `false` when filtering empty arrays [Guilherme Carreiro]
26
+
27
+ ## 5.7.0 2025-01-16
28
+
29
+ ### Features
30
+
31
+ * Add `find`, `find_index`, `has`, and `reject` filters to arrays [Guilherme Carreiro]
32
+ * Compatibility with Ruby 3.4 [Ian Ker-Seymer]
33
+
34
+ ## 5.6.4 2025-01-14
4
35
 
5
36
  ### Fixes
37
+ * Add a default `string_scanner` to avoid errors with `Liquid::VariableLookup.parse("foo.bar")` [Ian Ker-Seymer]
6
38
 
7
- * Fix Tokenizer to handle null source value (#1873) [Bahar Pourazar]
39
+ ## 5.6.3 2025-01-13
40
+ * Remove `lru_redux` dependency [Michael Go]
41
+
42
+ ## 5.6.2 2025-01-13
43
+
44
+ ### Fixes
45
+ * Preserve the old behavior of requiring floats to start with a digit [Michael Go]
46
+
47
+ ## 5.6.1 2025-01-13
48
+
49
+ ### Performance improvements
50
+ * Faster Expression parser / Tokenizer with StringScanner [Michael Go]
8
51
 
52
+ ## 5.6.0 2024-12-19
53
+
54
+ ### Architectural changes
55
+ * Added new `Environment` class to manage configuration and state that was previously stored in `Template` [Ian Ker-Seymer]
56
+ * Moved tag registration from `Template` to `Environment` [Ian Ker-Seymer]
57
+ * Removed `StrainerFactory` in favor of `Environment`-based strainer creation [Ian Ker-Seymer]
58
+ * Consolidated standard tags into a new `Tags` module with `STANDARD_TAGS` constant [Ian Ker-Seymer]
59
+
60
+ ### Performance improvements
61
+ * Optimized `Lexer` with a new `Lexer2` implementation using jump tables for faster tokenization, requires Ruby 3.4 [Ian Ker-Seymer]
62
+ * Improved variable rendering with specialized handling for different types [Michael Go]
63
+ * Reduced array allocations by using frozen empty constants [Michael Go]
64
+
65
+ ### API changes
66
+ * Deprecated several `Template` class methods in favor of `Environment` methods [Ian Ker-Seymer]
67
+ * Added deprecation warnings system [Ian Ker-Seymer]
68
+ * Changed how filters and tags are registered to use Environment [Ian Ker-Seymer]
69
+
70
+ ### Fixes
71
+ * Fixed table row handling of break interrupts [Alex Coco]
72
+ * Improved variable output handling for arrays [Ian Ker-Seymer]
73
+ * Fix Tokenizer to handle null source value (#1873) [Bahar Pourazar]
9
74
 
10
75
  ## 5.5.0 2024-03-21
11
76
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "lru_redux"
4
-
5
3
  module Liquid
6
4
  # Context keeps the variable stack and resolves variables, as well as keywords
7
5
  #
@@ -41,7 +39,6 @@ module Liquid
41
39
  @filters = []
42
40
  @global_filter = nil
43
41
  @disabled_tags = {}
44
- @expression_cache = LruRedux::ThreadSafeCache.new(1000)
45
42
 
46
43
  # Instead of constructing new StringScanner objects for each Expression parse,
47
44
  # we recycle the same one.
@@ -183,7 +180,7 @@ module Liquid
183
180
  # Example:
184
181
  # products == empty #=> products.empty?
185
182
  def [](expression)
186
- evaluate(Expression.parse(expression, @string_scanner, @expression_cache))
183
+ evaluate(Expression.parse(expression, @string_scanner))
187
184
  end
188
185
 
189
186
  def key?(key)
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "lru_redux"
4
-
5
3
  module Liquid
6
4
  class Expression
7
5
  LITERALS = {
@@ -79,10 +77,17 @@ module Liquid
79
77
  end
80
78
 
81
79
  ss.string = markup
82
- # the first byte must be a digit, a period, or a dash
80
+ # the first byte must be a digit or a dash
83
81
  byte = ss.scan_byte
84
82
 
85
- return false if byte != DASH && byte != DOT && (byte < ZERO || byte > NINE)
83
+ return false if byte != DASH && (byte < ZERO || byte > NINE)
84
+
85
+ if byte == DASH
86
+ peek_byte = ss.peek_byte
87
+
88
+ # if it starts with a dash, the next byte must be a digit
89
+ return false if peek_byte.nil? || !(peek_byte >= ZERO && peek_byte <= NINE)
90
+ end
86
91
 
87
92
  # The markup could be a float with multiple dots
88
93
  first_dot_pos = nil
data/lib/liquid/lexer.rb CHANGED
@@ -161,6 +161,12 @@ module Liquid
161
161
  end
162
162
  # rubocop:enable Metrics/BlockNesting
163
163
  output << EOS
164
+ rescue ::ArgumentError => e
165
+ if e.message == "invalid byte sequence in #{ss.string.encoding}"
166
+ raise SyntaxError, "Invalid byte sequence in #{ss.string.encoding}"
167
+ else
168
+ raise
169
+ end
164
170
  end
165
171
 
166
172
  def raise_syntax_error(start_pos, ss)
@@ -2,12 +2,14 @@
2
2
  errors:
3
3
  syntax:
4
4
  tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{tag}"
5
+ block_tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: {% %{tag} %}{% end%{tag} %}"
5
6
  assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
6
7
  capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
7
8
  case: "Syntax Error in 'case' - Valid syntax: case [condition]"
8
9
  case_invalid_when: "Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %}"
9
10
  case_invalid_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) "
10
11
  cycle: "Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]"
12
+ doc_invalid_nested: "Syntax Error in 'doc' - Nested doc tags are not allowed"
11
13
  for: "Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]"
12
14
  for_invalid_in: "For loops require an 'in' clause"
13
15
  for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset"
@@ -3,7 +3,6 @@
3
3
  require 'cgi'
4
4
  require 'base64'
5
5
  require 'bigdecimal'
6
-
7
6
  module Liquid
8
7
  module StandardFilters
9
8
  MAX_I32 = (1 << 31) - 1
@@ -64,7 +63,7 @@ module Liquid
64
63
  # @liquid_syntax string | downcase
65
64
  # @liquid_return [string]
66
65
  def downcase(input)
67
- input.to_s.downcase
66
+ Utils.to_s(input).downcase
68
67
  end
69
68
 
70
69
  # @liquid_public_docs
@@ -75,7 +74,7 @@ module Liquid
75
74
  # @liquid_syntax string | upcase
76
75
  # @liquid_return [string]
77
76
  def upcase(input)
78
- input.to_s.upcase
77
+ Utils.to_s(input).upcase
79
78
  end
80
79
 
81
80
  # @liquid_public_docs
@@ -86,7 +85,7 @@ module Liquid
86
85
  # @liquid_syntax string | capitalize
87
86
  # @liquid_return [string]
88
87
  def capitalize(input)
89
- input.to_s.capitalize
88
+ Utils.to_s(input).capitalize
90
89
  end
91
90
 
92
91
  # @liquid_public_docs
@@ -97,7 +96,7 @@ module Liquid
97
96
  # @liquid_syntax string | escape
98
97
  # @liquid_return [string]
99
98
  def escape(input)
100
- CGI.escapeHTML(input.to_s) unless input.nil?
99
+ CGI.escapeHTML(Utils.to_s(input)) unless input.nil?
101
100
  end
102
101
  alias_method :h, :escape
103
102
 
@@ -109,7 +108,7 @@ module Liquid
109
108
  # @liquid_syntax string | escape_once
110
109
  # @liquid_return [string]
111
110
  def escape_once(input)
112
- input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
111
+ Utils.to_s(input).gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
113
112
  end
114
113
 
115
114
  # @liquid_public_docs
@@ -124,7 +123,7 @@ module Liquid
124
123
  # @liquid_syntax string | url_encode
125
124
  # @liquid_return [string]
126
125
  def url_encode(input)
127
- CGI.escape(input.to_s) unless input.nil?
126
+ CGI.escape(Utils.to_s(input)) unless input.nil?
128
127
  end
129
128
 
130
129
  # @liquid_public_docs
@@ -138,7 +137,7 @@ module Liquid
138
137
  def url_decode(input)
139
138
  return if input.nil?
140
139
 
141
- result = CGI.unescape(input.to_s)
140
+ result = CGI.unescape(Utils.to_s(input))
142
141
  raise Liquid::ArgumentError, "invalid byte sequence in #{result.encoding}" unless result.valid_encoding?
143
142
 
144
143
  result
@@ -152,7 +151,7 @@ module Liquid
152
151
  # @liquid_syntax string | base64_encode
153
152
  # @liquid_return [string]
154
153
  def base64_encode(input)
155
- Base64.strict_encode64(input.to_s)
154
+ Base64.strict_encode64(Utils.to_s(input))
156
155
  end
157
156
 
158
157
  # @liquid_public_docs
@@ -163,7 +162,7 @@ module Liquid
163
162
  # @liquid_syntax string | base64_decode
164
163
  # @liquid_return [string]
165
164
  def base64_decode(input)
166
- input = input.to_s
165
+ input = Utils.to_s(input)
167
166
  StandardFilters.try_coerce_encoding(Base64.strict_decode64(input), encoding: input.encoding)
168
167
  rescue ::ArgumentError
169
168
  raise Liquid::ArgumentError, "invalid base64 provided to base64_decode"
@@ -177,7 +176,7 @@ module Liquid
177
176
  # @liquid_syntax string | base64_url_safe_encode
178
177
  # @liquid_return [string]
179
178
  def base64_url_safe_encode(input)
180
- Base64.urlsafe_encode64(input.to_s)
179
+ Base64.urlsafe_encode64(Utils.to_s(input))
181
180
  end
182
181
 
183
182
  # @liquid_public_docs
@@ -188,7 +187,7 @@ module Liquid
188
187
  # @liquid_syntax string | base64_url_safe_decode
189
188
  # @liquid_return [string]
190
189
  def base64_url_safe_decode(input)
191
- input = input.to_s
190
+ input = Utils.to_s(input)
192
191
  StandardFilters.try_coerce_encoding(Base64.urlsafe_decode64(input), encoding: input.encoding)
193
192
  rescue ::ArgumentError
194
193
  raise Liquid::ArgumentError, "invalid base64 provided to base64_url_safe_decode"
@@ -212,7 +211,7 @@ module Liquid
212
211
  if input.is_a?(Array)
213
212
  input.slice(offset, length) || []
214
213
  else
215
- input.to_s.slice(offset, length) || ''
214
+ Utils.to_s(input).slice(offset, length) || ''
216
215
  end
217
216
  rescue RangeError
218
217
  if I64_RANGE.cover?(length) && I64_RANGE.cover?(offset)
@@ -236,10 +235,10 @@ module Liquid
236
235
  # @liquid_return [string]
237
236
  def truncate(input, length = 50, truncate_string = "...")
238
237
  return if input.nil?
239
- input_str = input.to_s
238
+ input_str = Utils.to_s(input)
240
239
  length = Utils.to_integer(length)
241
240
 
242
- truncate_string_str = truncate_string.to_s
241
+ truncate_string_str = Utils.to_s(truncate_string)
243
242
 
244
243
  l = length - truncate_string_str.length
245
244
  l = 0 if l < 0
@@ -263,7 +262,7 @@ module Liquid
263
262
  # @liquid_return [string]
264
263
  def truncatewords(input, words = 15, truncate_string = "...")
265
264
  return if input.nil?
266
- input = input.to_s
265
+ input = Utils.to_s(input)
267
266
  words = Utils.to_integer(words)
268
267
  words = 1 if words <= 0
269
268
 
@@ -277,7 +276,8 @@ module Liquid
277
276
  return input if wordlist.length <= words
278
277
 
279
278
  wordlist.pop
280
- wordlist.join(" ").concat(truncate_string.to_s)
279
+ truncate_string = Utils.to_s(truncate_string)
280
+ wordlist.join(" ").concat(truncate_string)
281
281
  end
282
282
 
283
283
  # @liquid_public_docs
@@ -288,7 +288,9 @@ module Liquid
288
288
  # @liquid_syntax string | split: string
289
289
  # @liquid_return [array[string]]
290
290
  def split(input, pattern)
291
- input.to_s.split(pattern.to_s)
291
+ pattern = Utils.to_s(pattern)
292
+ input = Utils.to_s(input)
293
+ input.split(pattern)
292
294
  end
293
295
 
294
296
  # @liquid_public_docs
@@ -299,7 +301,8 @@ module Liquid
299
301
  # @liquid_syntax string | strip
300
302
  # @liquid_return [string]
301
303
  def strip(input)
302
- input.to_s.strip
304
+ input = Utils.to_s(input)
305
+ input.strip
303
306
  end
304
307
 
305
308
  # @liquid_public_docs
@@ -310,7 +313,8 @@ module Liquid
310
313
  # @liquid_syntax string | lstrip
311
314
  # @liquid_return [string]
312
315
  def lstrip(input)
313
- input.to_s.lstrip
316
+ input = Utils.to_s(input)
317
+ input.lstrip
314
318
  end
315
319
 
316
320
  # @liquid_public_docs
@@ -321,7 +325,8 @@ module Liquid
321
325
  # @liquid_syntax string | rstrip
322
326
  # @liquid_return [string]
323
327
  def rstrip(input)
324
- input.to_s.rstrip
328
+ input = Utils.to_s(input)
329
+ input.rstrip
325
330
  end
326
331
 
327
332
  # @liquid_public_docs
@@ -332,8 +337,9 @@ module Liquid
332
337
  # @liquid_syntax string | strip_html
333
338
  # @liquid_return [string]
334
339
  def strip_html(input)
340
+ input = Utils.to_s(input)
335
341
  empty = ''
336
- result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
342
+ result = input.gsub(STRIP_HTML_BLOCKS, empty)
337
343
  result.gsub!(STRIP_HTML_TAGS, empty)
338
344
  result
339
345
  end
@@ -346,7 +352,8 @@ module Liquid
346
352
  # @liquid_syntax string | strip_newlines
347
353
  # @liquid_return [string]
348
354
  def strip_newlines(input)
349
- input.to_s.gsub(/\r?\n/, '')
355
+ input = Utils.to_s(input)
356
+ input.gsub(/\r?\n/, '')
350
357
  end
351
358
 
352
359
  # @liquid_public_docs
@@ -357,6 +364,7 @@ module Liquid
357
364
  # @liquid_syntax array | join
358
365
  # @liquid_return [string]
359
366
  def join(input, glue = ' ')
367
+ glue = Utils.to_s(glue)
360
368
  InputIterator.new(input, context).join(glue)
361
369
  end
362
370
 
@@ -424,29 +432,59 @@ module Liquid
424
432
  # @liquid_syntax array | where: string, string
425
433
  # @liquid_return [array[untyped]]
426
434
  def where(input, property, target_value = nil)
427
- ary = InputIterator.new(input, context)
435
+ filter_array(input, property, target_value) { |ary, &block| ary.select(&block) }
436
+ end
428
437
 
429
- if ary.empty?
430
- []
431
- elsif target_value.nil?
432
- ary.select do |item|
433
- item[property]
434
- rescue TypeError
435
- raise_property_error(property)
436
- rescue NoMethodError
437
- return nil unless item.respond_to?(:[])
438
- raise
439
- end
440
- else
441
- ary.select do |item|
442
- item[property] == target_value
443
- rescue TypeError
444
- raise_property_error(property)
445
- rescue NoMethodError
446
- return nil unless item.respond_to?(:[])
447
- raise
448
- end
449
- end
438
+ # @liquid_public_docs
439
+ # @liquid_type filter
440
+ # @liquid_category array
441
+ # @liquid_summary
442
+ # Filters an array to exclude items with a specific property value.
443
+ # @liquid_description
444
+ # This requires you to provide both the property name and the associated value.
445
+ # @liquid_syntax array | reject: string, string
446
+ # @liquid_return [array[untyped]]
447
+ def reject(input, property, target_value = nil)
448
+ filter_array(input, property, target_value) { |ary, &block| ary.reject(&block) }
449
+ end
450
+
451
+ # @liquid_public_docs
452
+ # @liquid_type filter
453
+ # @liquid_category array
454
+ # @liquid_summary
455
+ # Tests if any item in an array has a specific property value.
456
+ # @liquid_description
457
+ # This requires you to provide both the property name and the associated value.
458
+ # @liquid_syntax array | has: string, string
459
+ # @liquid_return [boolean]
460
+ def has(input, property, target_value = nil)
461
+ filter_array(input, property, target_value, false) { |ary, &block| ary.any?(&block) }
462
+ end
463
+
464
+ # @liquid_public_docs
465
+ # @liquid_type filter
466
+ # @liquid_category array
467
+ # @liquid_summary
468
+ # Returns the first item in an array with a specific property value.
469
+ # @liquid_description
470
+ # This requires you to provide both the property name and the associated value.
471
+ # @liquid_syntax array | find: string, string
472
+ # @liquid_return [untyped]
473
+ def find(input, property, target_value = nil)
474
+ filter_array(input, property, target_value, nil) { |ary, &block| ary.find(&block) }
475
+ end
476
+
477
+ # @liquid_public_docs
478
+ # @liquid_type filter
479
+ # @liquid_category array
480
+ # @liquid_summary
481
+ # Returns the index of the first item in an array with a specific property value.
482
+ # @liquid_description
483
+ # This requires you to provide both the property name and the associated value.
484
+ # @liquid_syntax array | find_index: string, string
485
+ # @liquid_return [number]
486
+ def find_index(input, property, target_value = nil)
487
+ filter_array(input, property, target_value, nil) { |ary, &block| ary.find_index(&block) }
450
488
  end
451
489
 
452
490
  # @liquid_public_docs
@@ -543,7 +581,10 @@ module Liquid
543
581
  # @liquid_syntax string | replace: string, string
544
582
  # @liquid_return [string]
545
583
  def replace(input, string, replacement = '')
546
- input.to_s.gsub(string.to_s, replacement.to_s)
584
+ string = Utils.to_s(string)
585
+ replacement = Utils.to_s(replacement)
586
+ input = Utils.to_s(input)
587
+ input.gsub(string, replacement)
547
588
  end
548
589
 
549
590
  # @liquid_public_docs
@@ -554,7 +595,10 @@ module Liquid
554
595
  # @liquid_syntax string | replace_first: string, string
555
596
  # @liquid_return [string]
556
597
  def replace_first(input, string, replacement = '')
557
- input.to_s.sub(string.to_s, replacement.to_s)
598
+ string = Utils.to_s(string)
599
+ replacement = Utils.to_s(replacement)
600
+ input = Utils.to_s(input)
601
+ input.sub(string, replacement)
558
602
  end
559
603
 
560
604
  # @liquid_public_docs
@@ -565,9 +609,9 @@ module Liquid
565
609
  # @liquid_syntax string | replace_last: string, string
566
610
  # @liquid_return [string]
567
611
  def replace_last(input, string, replacement)
568
- input = input.to_s
569
- string = string.to_s
570
- replacement = replacement.to_s
612
+ input = Utils.to_s(input)
613
+ string = Utils.to_s(string)
614
+ replacement = Utils.to_s(replacement)
571
615
 
572
616
  start_index = input.rindex(string)
573
617
 
@@ -619,7 +663,9 @@ module Liquid
619
663
  # @liquid_syntax string | append: string
620
664
  # @liquid_return [string]
621
665
  def append(input, string)
622
- input.to_s + string.to_s
666
+ input = Utils.to_s(input)
667
+ string = Utils.to_s(string)
668
+ input + string
623
669
  end
624
670
 
625
671
  # @liquid_public_docs
@@ -648,7 +694,9 @@ module Liquid
648
694
  # @liquid_syntax string | prepend: string
649
695
  # @liquid_return [string]
650
696
  def prepend(input, string)
651
- string.to_s + input.to_s
697
+ input = Utils.to_s(input)
698
+ string = Utils.to_s(string)
699
+ string + input
652
700
  end
653
701
 
654
702
  # @liquid_public_docs
@@ -659,10 +707,20 @@ module Liquid
659
707
  # @liquid_syntax string | newline_to_br
660
708
  # @liquid_return [string]
661
709
  def newline_to_br(input)
662
- input.to_s.gsub(/\r?\n/, "<br />\n")
710
+ input = Utils.to_s(input)
711
+ input.gsub(/\r?\n/, "<br />\n")
663
712
  end
664
713
 
665
- # Reformat a date using Ruby's core Time#strftime( string ) -> string
714
+ # @liquid_public_docs
715
+ # @liquid_type filter
716
+ # @liquid_category date
717
+ # @liquid_summary
718
+ # Formats a date according to a specified format string.
719
+ # @liquid_description
720
+ # This filter formats a date using various format specifiers. If the format string is empty,
721
+ # the original input is returned. If the input cannot be converted to a date, the original input is returned.
722
+ #
723
+ # The following format specifiers can be used:
666
724
  #
667
725
  # %a - The abbreviated weekday name (``Sun'')
668
726
  # %A - The full weekday name (``Sunday'')
@@ -691,14 +749,15 @@ module Liquid
691
749
  # %Y - Year with century
692
750
  # %Z - Time zone name
693
751
  # %% - Literal ``%'' character
694
- #
695
- # See also: http://www.ruby-doc.org/core/Time.html#method-i-strftime
752
+ # @liquid_syntax date | date: string
753
+ # @liquid_return [string]
696
754
  def date(input, format)
697
- return input if format.to_s.empty?
755
+ str_format = Utils.to_s(format)
756
+ return input if str_format.empty?
698
757
 
699
758
  return input unless (date = Utils.to_date(input))
700
759
 
701
- date.strftime(format.to_s)
760
+ date.strftime(str_format)
702
761
  end
703
762
 
704
763
  # @liquid_public_docs
@@ -918,8 +977,27 @@ module Liquid
918
977
 
919
978
  attr_reader :context
920
979
 
980
+ def filter_array(input, property, target_value, default_value = [], &block)
981
+ ary = InputIterator.new(input, context)
982
+
983
+ return default_value if ary.empty?
984
+
985
+ block.call(ary) do |item|
986
+ if target_value.nil?
987
+ item[property]
988
+ else
989
+ item[property] == target_value
990
+ end
991
+ rescue TypeError
992
+ raise_property_error(property)
993
+ rescue NoMethodError
994
+ return nil unless item.respond_to?(:[])
995
+ raise
996
+ end
997
+ end
998
+
921
999
  def raise_property_error(property)
922
- raise Liquid::ArgumentError, "cannot select the property '#{property}'"
1000
+ raise Liquid::ArgumentError, "cannot select the property '#{Utils.to_s(property)}'"
923
1001
  end
924
1002
 
925
1003
  def apply_operation(input, operand, operation)
@@ -968,7 +1046,18 @@ module Liquid
968
1046
  end
969
1047
 
970
1048
  def join(glue)
971
- to_a.join(glue.to_s)
1049
+ first = true
1050
+ output = +""
1051
+ each do |item|
1052
+ if first
1053
+ first = false
1054
+ else
1055
+ output << glue
1056
+ end
1057
+
1058
+ output << Liquid::Utils.to_s(item)
1059
+ end
1060
+ output
972
1061
  end
973
1062
 
974
1063
  def concat(args)
@@ -980,7 +1069,10 @@ module Liquid
980
1069
  end
981
1070
 
982
1071
  def uniq(&block)
983
- to_a.uniq(&block)
1072
+ to_a.uniq do |item|
1073
+ item = Utils.to_liquid_value(item)
1074
+ block ? yield(item) : item
1075
+ end
984
1076
  end
985
1077
 
986
1078
  def compact
@@ -10,7 +10,7 @@ module Liquid
10
10
  # @liquid_description
11
11
  # Variables that are declared with `decrement` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
12
12
  # or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
13
- # [snippets](/themes/architecture#snippets) included in the file.
13
+ # [snippets](/themes/architecture/snippets) included in the file.
14
14
  #
15
15
  # Similarly, variables that are created with `decrement` are independent from those created with [`assign`](/docs/api/liquid/tags/assign)
16
16
  # and [`capture`](/docs/api/liquid/tags/capture). However, `decrement` and [`increment`](/docs/api/liquid/tags/increment) share
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ # @liquid_public_docs
5
+ # @liquid_type tag
6
+ # @liquid_category syntax
7
+ # @liquid_name doc
8
+ # @liquid_summary
9
+ # Documents template elements with annotations.
10
+ # @liquid_description
11
+ # The `doc` tag allows developers to include documentation within Liquid
12
+ # templates. Any content inside `doc` tags is not rendered or outputted.
13
+ # Liquid code inside will be parsed but not executed. This facilitates
14
+ # tooling support for features like code completion, linting, and inline
15
+ # documentation.
16
+ #
17
+ # For detailed documentation syntax and examples, see the
18
+ # [`LiquidDoc` reference](/docs/storefronts/themes/tools/liquid-doc).
19
+ #
20
+ # @liquid_syntax
21
+ # {% doc %}
22
+ # Renders a message.
23
+ #
24
+ # @param {string} foo - A string value.
25
+ # @param {string} [bar] - An optional string value.
26
+ #
27
+ # @example
28
+ # {% render 'message', foo: 'Hello', bar: 'World' %}
29
+ # {% enddoc %}
30
+ class Doc < Block
31
+ NO_UNEXPECTED_ARGS = /\A\s*\z/
32
+
33
+ def initialize(tag_name, markup, parse_context)
34
+ super
35
+ ensure_valid_markup(tag_name, markup, parse_context)
36
+ end
37
+
38
+ def parse(tokens)
39
+ @body = +""
40
+
41
+ while (token = tokens.shift)
42
+ tag_name = token =~ BlockBody::FullTokenPossiblyInvalid && Regexp.last_match(2)
43
+
44
+ raise_nested_doc_error if tag_name == @tag_name
45
+
46
+ if tag_name == block_delimiter
47
+ parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
48
+ @body << Regexp.last_match(1) if Regexp.last_match(1) != ""
49
+ return
50
+ end
51
+ @body << token unless token.empty?
52
+ end
53
+
54
+ raise_tag_never_closed(block_name)
55
+ end
56
+
57
+ def render_to_output_buffer(_context, output)
58
+ output
59
+ end
60
+
61
+ def blank?
62
+ @body.empty?
63
+ end
64
+
65
+ def nodelist
66
+ [@body]
67
+ end
68
+
69
+ private
70
+
71
+ def ensure_valid_markup(tag_name, markup, parse_context)
72
+ unless NO_UNEXPECTED_ARGS.match?(markup)
73
+ raise SyntaxError, parse_context.locale.t("errors.syntax.block_tag_unexpected_args", tag: tag_name)
74
+ end
75
+ end
76
+
77
+ def raise_nested_doc_error
78
+ raise SyntaxError, parse_context.locale.t("errors.syntax.doc_invalid_nested")
79
+ end
80
+ end
81
+ end
@@ -6,7 +6,7 @@ module Liquid
6
6
  # @liquid_category theme
7
7
  # @liquid_name include
8
8
  # @liquid_summary
9
- # Renders a [snippet](/themes/architecture#snippets).
9
+ # Renders a [snippet](/themes/architecture/snippets).
10
10
  # @liquid_description
11
11
  # Inside the snippet, you can access and alter variables that are [created](/docs/api/liquid/tags/variable-tags) outside of the
12
12
  # snippet.
@@ -10,7 +10,7 @@ module Liquid
10
10
  # @liquid_description
11
11
  # Variables that are declared with `increment` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
12
12
  # or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
13
- # [snippets](/themes/architecture#snippets) included in the file.
13
+ # [snippets](/themes/architecture/snippets) included in the file.
14
14
  #
15
15
  # Similarly, variables that are created with `increment` are independent from those created with [`assign`](/docs/api/liquid/tags/assign)
16
16
  # and [`capture`](/docs/api/liquid/tags/capture). However, `increment` and [`decrement`](/docs/api/liquid/tags/decrement) share
@@ -6,7 +6,7 @@ module Liquid
6
6
  # @liquid_category theme
7
7
  # @liquid_name render
8
8
  # @liquid_summary
9
- # Renders a [snippet](/themes/architecture#snippets) or [app block](/themes/architecture/sections/section-schema#render-app-blocks).
9
+ # Renders a [snippet](/themes/architecture/snippets) or [app block](/themes/architecture/sections/section-schema#render-app-blocks).
10
10
  # @liquid_description
11
11
  # Inside snippets and app blocks, you can't directly access variables that are [created](/docs/api/liquid/tags/variable-tags) outside
12
12
  # of the snippet or app block. However, you can [specify variables as parameters](/docs/api/liquid/tags/render#render-passing-variables-to-a-snippet)
data/lib/liquid/tags.rb CHANGED
@@ -19,6 +19,7 @@ require_relative "tags/comment"
19
19
  require_relative "tags/raw"
20
20
  require_relative "tags/render"
21
21
  require_relative "tags/cycle"
22
+ require_relative "tags/doc"
22
23
 
23
24
  module Liquid
24
25
  module Tags
@@ -42,6 +43,7 @@ module Liquid
42
43
  'if' => If,
43
44
  'echo' => Echo,
44
45
  'tablerow' => TableRow,
46
+ 'doc' => Doc,
45
47
  }.freeze
46
48
  end
47
49
  end
@@ -103,6 +103,12 @@ module Liquid
103
103
 
104
104
  pos = @ss.pos -= 2
105
105
  @source.byteslice(start, pos - start)
106
+ rescue ::ArgumentError => e
107
+ if e.message == "invalid byte sequence in #{@ss.string.encoding}"
108
+ raise SyntaxError, "Invalid byte sequence in #{@ss.string.encoding}"
109
+ else
110
+ raise
111
+ end
106
112
  end
107
113
 
108
114
  def next_variable_token
data/lib/liquid/utils.rb CHANGED
@@ -89,5 +89,101 @@ module Liquid
89
89
  # Otherwise return the object itself
90
90
  obj
91
91
  end
92
+
93
+ def self.to_s(obj, seen = {})
94
+ case obj
95
+ when Hash
96
+ # If the custom hash implementation overrides `#to_s`, use their
97
+ # custom implementation. Otherwise we use Liquid's default
98
+ # implementation.
99
+ if obj.class.instance_method(:to_s) == HASH_TO_S_METHOD
100
+ hash_inspect(obj, seen)
101
+ else
102
+ obj.to_s
103
+ end
104
+ when Array
105
+ array_inspect(obj, seen)
106
+ else
107
+ obj.to_s
108
+ end
109
+ end
110
+
111
+ def self.inspect(obj, seen = {})
112
+ case obj
113
+ when Hash
114
+ # If the custom hash implementation overrides `#inspect`, use their
115
+ # custom implementation. Otherwise we use Liquid's default
116
+ # implementation.
117
+ if obj.class.instance_method(:inspect) == HASH_INSPECT_METHOD
118
+ hash_inspect(obj, seen)
119
+ else
120
+ obj.inspect
121
+ end
122
+ when Array
123
+ array_inspect(obj, seen)
124
+ else
125
+ obj.inspect
126
+ end
127
+ end
128
+
129
+ def self.array_inspect(arr, seen = {})
130
+ if seen[arr.object_id]
131
+ return "[...]"
132
+ end
133
+
134
+ seen[arr.object_id] = true
135
+ str = +"["
136
+ cursor = 0
137
+ len = arr.length
138
+
139
+ while cursor < len
140
+ if cursor > 0
141
+ str << ", "
142
+ end
143
+
144
+ item_str = inspect(arr[cursor], seen)
145
+ str << item_str
146
+ cursor += 1
147
+ end
148
+
149
+ str << "]"
150
+ str
151
+ ensure
152
+ seen.delete(arr.object_id)
153
+ end
154
+
155
+ def self.hash_inspect(hash, seen = {})
156
+ if seen[hash.object_id]
157
+ return "{...}"
158
+ end
159
+ seen[hash.object_id] = true
160
+
161
+ str = +"{"
162
+ first = true
163
+ hash.each do |key, value|
164
+ if first
165
+ first = false
166
+ else
167
+ str << ", "
168
+ end
169
+
170
+ key_str = inspect(key, seen)
171
+ str << key_str
172
+ str << "=>"
173
+
174
+ value_str = inspect(value, seen)
175
+ str << value_str
176
+ end
177
+ str << "}"
178
+ str
179
+ ensure
180
+ seen.delete(hash.object_id)
181
+ end
182
+
183
+ HASH_TO_S_METHOD = Hash.instance_method(:to_s)
184
+ private_constant :HASH_TO_S_METHOD
185
+
186
+ HASH_INSPECT_METHOD = Hash.instance_method(:inspect)
187
+ private_constant :HASH_INSPECT_METHOD
92
188
  end
93
189
  end
@@ -107,8 +107,8 @@ module Liquid
107
107
  obj.each do |o|
108
108
  render_obj_to_output(o, output)
109
109
  end
110
- when
111
- output << obj.to_s
110
+ else
111
+ output << Liquid::Utils.to_s(obj)
112
112
  end
113
113
  end
114
114
 
@@ -6,7 +6,7 @@ module Liquid
6
6
 
7
7
  attr_reader :name, :lookups
8
8
 
9
- def self.parse(markup, string_scanner, cache = nil)
9
+ def self.parse(markup, string_scanner = StringScanner.new(""), cache = nil)
10
10
  new(markup, string_scanner, cache)
11
11
  end
12
12
 
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Liquid
5
- VERSION = "5.6.1"
5
+ VERSION = "5.8.7"
6
6
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: liquid
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.6.1
4
+ version: 5.8.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tobias Lütke
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-01-13 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: strscan
@@ -120,6 +120,7 @@ files:
120
120
  - lib/liquid/tags/continue.rb
121
121
  - lib/liquid/tags/cycle.rb
122
122
  - lib/liquid/tags/decrement.rb
123
+ - lib/liquid/tags/doc.rb
123
124
  - lib/liquid/tags/echo.rb
124
125
  - lib/liquid/tags/for.rb
125
126
  - lib/liquid/tags/if.rb
@@ -158,7 +159,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
158
159
  - !ruby/object:Gem::Version
159
160
  version: 1.3.7
160
161
  requirements: []
161
- rubygems_version: 3.6.2
162
+ rubygems_version: 3.6.9
162
163
  specification_version: 4
163
164
  summary: A secure, non-evaling end user template engine with aesthetic markup.
164
165
  test_files: []