Module: Liquid2::Filters

Defined in:
lib/liquid2/filter.rb,
lib/liquid2/filters/date.rb,
lib/liquid2/filters/json.rb,
lib/liquid2/filters/math.rb,
lib/liquid2/filters/size.rb,
lib/liquid2/filters/sort.rb,
lib/liquid2/filters/array.rb,
lib/liquid2/filters/slice.rb,
lib/liquid2/filters/string.rb,
lib/liquid2/filters/default.rb

Overview

Liquid filters and helper methods.

Constant Summary collapse

INFINITY_ARRAY =

: [Float]

[Float::INFINITY].freeze
RE_HTML_BLOCKS =
Regexp.union(
  %r{<script.*?</script>}m,
  /<!--.*?-->/m,
  %r{<style.*?</style>}m
)
RE_HTML_TAGS =
/<.*?>/m

Class Method Summary collapse

Class Method Details

.abs(left) ⇒ Object

Return the absolute value of left.



7
8
9
# File 'lib/liquid2/filters/math.rb', line 7

def self.abs(left)
  to_number(left).abs
end

.append(left, right) ⇒ Object

Return left concatenated with right. Coerce left and right to strings if they aren't strings already.



11
12
13
# File 'lib/liquid2/filters/string.rb', line 11

def self.append(left, right)
  Liquid2.to_s(left) + Liquid2.to_s(right)
end

.at_least(left, right) ⇒ Object

Return the maximum of left and right.



12
13
14
# File 'lib/liquid2/filters/math.rb', line 12

def self.at_least(left, right)
  [to_number(left), to_number(right)].max
end

.at_most(left, right) ⇒ Object

Return the minimum of left and right.



17
18
19
# File 'lib/liquid2/filters/math.rb', line 17

def self.at_most(left, right)
  [to_number(left), to_number(right)].min
end

.base64_decode(left) ⇒ Object



190
191
192
193
# File 'lib/liquid2/filters/string.rb', line 190

def self.base64_decode(left)
  decoded = Base64.strict_decode64(Liquid2.to_s(left)).force_encoding(Encoding::UTF_8)
  decoded if decoded.valid_encoding?
end

.base64_encode(left) ⇒ Object



186
187
188
# File 'lib/liquid2/filters/string.rb', line 186

def self.base64_encode(left)
  Base64.strict_encode64(Liquid2.to_s(left)).force_encoding(Encoding::UTF_8)
end

.base64_url_safe_decode(left) ⇒ Object



199
200
201
202
# File 'lib/liquid2/filters/string.rb', line 199

def self.base64_url_safe_decode(left)
  decoded = Base64.urlsafe_decode64(Liquid2.to_s(left)).force_encoding(Encoding::UTF_8)
  decoded if decoded.valid_encoding?
end

.base64_url_safe_encode(left) ⇒ Object



195
196
197
# File 'lib/liquid2/filters/string.rb', line 195

def self.base64_url_safe_encode(left)
  Base64.urlsafe_encode64(Liquid2.to_s(left)).force_encoding(Encoding::UTF_8)
end

.better_slice(left, start_ = :undefined, stop_ = :undefined, step_ = :undefined, start: :undefined, stop: :undefined, step: :undefined) ⇒ Object



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
49
50
51
52
53
54
55
# File 'lib/liquid2/filters/slice.rb', line 17

def self.better_slice(
  left,
  start_ = :undefined, stop_ = :undefined, step_ = :undefined,
  start: :undefined, stop: :undefined, step: :undefined
)
  # Give priority to keyword arguments, default to nil if neither are given.
  start = start_ == :undefined ? nil : start_ if start == :undefined
  stop = stop_ == :undefined ? nil : stop_ if stop == :undefined
  step = step_ == :undefined ? nil : step_ if step == :undefined

  step = to_integer(step || 1)
  length = left.length
  return [] if length.zero? || step.zero?

  start = to_integer(start) unless start.nil?
  stop = to_integer(stop) unless stop.nil?

  normalized_start = if start.nil?
                       step.negative? ? length - 1 : 0
                     elsif start&.negative?
                       [length + start, 0].max
                     else
                       [start, length - 1].min
                     end

  normalized_stop = if stop.nil?
                      step.negative? ? -1 : length
                    elsif stop&.negative?
                      [length + stop, -1].max
                    else
                      [stop, length].min
                    end

  # This does not work with Ruby 3.1
  # left[(normalized_start...normalized_stop).step(step)]
  #
  # But this does.
  (normalized_start...normalized_stop).step(step).map { |i| left[i] }
end

.capitalize(left) ⇒ Object

Return left with the first character in uppercase and the rest lowercase. Coerce left to a string if it is not one already.



17
18
19
# File 'lib/liquid2/filters/string.rb', line 17

def self.capitalize(left)
  Liquid2.to_s(left).capitalize
end

.ceil(left) ⇒ Object

Return left rounded up to the next whole number.



22
23
24
# File 'lib/liquid2/filters/math.rb', line 22

def self.ceil(left)
  to_number(left).ceil
end

.compact(left, key = :undefined, context:) ⇒ Object

Return a copy of left with nil items removed. Coerce left to an array-like object if it is not one already.

If key is given, assume items in left are hash-like and remove items from left where item.fetch(key, nil) is nil.

If key is not :undefined, coerce it to a string before calling fetch on items in left.



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/liquid2/filters/array.rb', line 21

def self.compact(left, key = :undefined, context:)
  left = Liquid2::Filters.to_enumerable(left)

  case key
  when Liquid2::Lambda
    key.map(context, left).zip(left).reject do |r, _i|
      r.nil? || r.is_a?(Liquid2::Undefined)
    end.map(&:last)
  when :undefined
    left.compact
  else
    left.reject do |item|
      item.respond_to?(:fetch) ? item.fetch(key, nil).nil? : true
    end
  end
end

.concat(left, right) ⇒ Object

Return left concatenated with right, or nil if right is not an array. Coerce left to an array if it isn't an array already.



40
41
42
43
44
45
46
# File 'lib/liquid2/filters/array.rb', line 40

def self.concat(left, right)
  unless right.respond_to?(:to_ary)
    raise Liquid2::LiquidArgumentError.new("expected an array", nil)
  end

  Filters.to_enumerable(left).to_a.concat(right)
end

.date(left, format) ⇒ Object

Format date and time object left with format. Coerce left to a Time if it is not a time-like object already. Coerce format to a string if it is not a string already.



9
10
11
12
13
14
15
16
17
18
# File 'lib/liquid2/filters/date.rb', line 9

def self.date(left, format)
  format = Liquid2.to_s(format)
  return left if format.empty?

  if (date = Filters.to_date(left))
    date.strftime(format)
  else
    left
  end
end

.default(left, default = "", context:, allow_false: false) ⇒ Object

Return left, or default if obj is nil, false or empty. If allow_false is true, left is returned if left is false.



8
9
10
11
12
13
14
# File 'lib/liquid2/filters/default.rb', line 8

def self.default(left, default = "", context:, allow_false: false)
  return default if left.respond_to?(:force_default) && left.force_default

  obj = left.respond_to?(:to_liquid) ? left.to_liquid(context) : left
  falsey = allow_false ? left.nil? : !obj
  falsey || (left.respond_to?(:empty?) && left.empty?) ? default : left
end

.divided_by(left, right) ⇒ Object

Return the result of dividing left by right. If both left and right are integers, integer division is performed.



28
29
30
31
32
# File 'lib/liquid2/filters/math.rb', line 28

def self.divided_by(left, right)
  to_decimal(left) / to_decimal(right) # steep:ignore
rescue ZeroDivisionError => e
  raise LiquidTypeError.new(e.message, nil)
end

.downcase(left) ⇒ Object

Return left with all characters converted to lowercase. Coerce left to a string if it is not one already.



23
24
25
# File 'lib/liquid2/filters/string.rb', line 23

def self.downcase(left)
  Liquid2.to_s(left).downcase
end

.escape(left) ⇒ Object

Return left with special HTML characters replaced with their HTML-safe escape sequences. Coerce left to a string if it is not one already.



35
36
37
# File 'lib/liquid2/filters/string.rb', line 35

def self.escape(left)
  CGI.escape_html(Liquid2.to_s(left)) unless left.nil?
end

.escape_once(left) ⇒ Object

Return left with special HTML characters replaced with their HTML-safe escape sequences. Coerce left to a string if it is not one already.

It is safe to use escape_once on string values that already contain HTML-escape sequences.



43
44
45
# File 'lib/liquid2/filters/string.rb', line 43

def self.escape_once(left)
  CGI.escape_html(CGI.unescape_html(Liquid2.to_s(left)))
end

.fetch(obj, key, default = nil) ⇒ Object



86
87
88
89
90
91
92
# File 'lib/liquid2/filter.rb', line 86

def self.fetch(obj, key, default = nil)
  # TODO: benchmark without rescue
  # TODO: try defensive respond_to?
  obj[key]
rescue ArgumentError, TypeError, NoMethodError
  default
end

.find(left, key, value = nil, context:) ⇒ Object



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/liquid2/filters/array.rb', line 48

def self.find(left, key, value = nil, context:)
  left = Liquid2::Filters.to_enumerable(left)

  if key.is_a?(Liquid2::Lambda)
    key.map(context, left).zip(left).reject do |r, i|
      return i unless r.is_a?(Liquid2::Undefined) || !Liquid2.truthy?(context, r)
    end
  elsif !value.nil? && !Liquid2.undefined?(value)
    left.each do |item|
      return item if fetch(item, key) == value
    end
  else
    left.each do |item|
      return item if fetch(item, key)
    end
  end

  nil
end

.find_index(left, key, value = nil, context:) ⇒ Object



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/liquid2/filters/array.rb', line 68

def self.find_index(left, key, value = nil, context:)
  left = Liquid2::Filters.to_enumerable(left)

  if key.is_a?(Liquid2::Lambda)
    key.map(context, left).reject.with_index do |r, index|
      return index unless r.is_a?(Liquid2::Undefined) || !Liquid2.truthy?(context, r)
    end
  elsif !value.nil? && !Liquid2.undefined?(value)
    left.each_with_index do |item, index|
      return index if fetch(item, key) == value
    end
  else
    left.each_with_index do |item, index|
      return index if fetch(item, key)
    end
  end

  nil
end

.first(left) ⇒ Object

Return the first item in left, or nil if left does not have a first item.



109
110
111
112
113
114
115
116
# File 'lib/liquid2/filters/array.rb', line 109

def self.first(left)
  case left
  when String
    left[0]
  else
    left.first if left.respond_to?(:first)
  end
end

.floor(left) ⇒ Object

Return left rounded down to the next whole number.



40
41
42
# File 'lib/liquid2/filters/math.rb', line 40

def self.floor(left)
  to_number(left).floor
end

.has(left, key, value = nil, context:) ⇒ Object



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/liquid2/filters/array.rb', line 88

def self.has(left, key, value = nil, context:)
  left = Liquid2::Filters.to_enumerable(left)

  if key.is_a?(Liquid2::Lambda)
    key.map(context, left).reject do |r|
      return true unless r.is_a?(Liquid2::Undefined) || !Liquid2.truthy?(context, r)
    end
  elsif !value.nil? && !Liquid2.undefined?(value)
    left.each do |item|
      return true if fetch(item, key) == value
    end
  else
    left.each do |item|
      return true if fetch(item, key)
    end
  end

  false
end

.ints(obj) ⇒ Object



86
87
88
89
90
91
92
93
94
95
# File 'lib/liquid2/filters/sort.rb', line 86

def self.ints(obj)
  if obj.is_a?(Integer) || obj.is_a?(Float) || obj.is_a?(BigDecimal)
    [obj]
  else
    numeric = obj.to_s.scan(/(?<=\.)0+|-?\d+/)
    return INFINITY_ARRAY if numeric.empty?

    numeric.map(&:to_i)
  end
end

.join(left, sep = " ") ⇒ Object

Return the concatenation of items in left separated by sep. Coerce items in left to strings if they aren't strings already.



8
9
10
11
# File 'lib/liquid2/filters/array.rb', line 8

def self.join(left, sep = " ")
  sep = Liquid2.to_s(sep)
  to_enumerable(left).map { |item| Liquid2.to_s(item) }.join(Liquid2.to_s(sep))
end

.json(left, pretty: false) ⇒ Object

Return left serialized in JSON format.



7
8
9
10
11
12
13
# File 'lib/liquid2/filters/json.rb', line 7

def self.json(left, pretty: false)
  if pretty
    JSON.pretty_generate(left)
  else
    JSON.generate(left)
  end
end

.last(left) ⇒ Object

Return the last item in left, or nil if left does not have a last item.



119
120
121
122
123
124
125
126
# File 'lib/liquid2/filters/array.rb', line 119

def self.last(left)
  case left
  when String
    left[-1]
  else
    left.last if left.respond_to?(:last)
  end
end

.lstrip(left) ⇒ Object

Return left with leading whitespace removed. Coerce left to a string if it is not one already.



49
50
51
# File 'lib/liquid2/filters/string.rb', line 49

def self.lstrip(left)
  Liquid2.to_s(left).lstrip
end

.map(left, key, context:) ⇒ Object



128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/liquid2/filters/array.rb', line 128

def self.map(left, key, context:)
  left = Liquid2::Filters.to_enumerable(left)

  if key.is_a?(Liquid2::Lambda)
    key.map(context, left).map do |item|
      item.is_a?(Liquid2::Undefined) ? nil : item
    end
  else
    key = Liquid2.to_s(key)
    left.map { |item| item[key] }
  end
end

.minus(left, right) ⇒ Object

Return right subtracted from left.



45
46
47
# File 'lib/liquid2/filters/math.rb', line 45

def self.minus(left, right)
  to_decimal(left) - to_decimal(right) # steep:ignore
end

.modulo(left, right) ⇒ Object

Return the remainder of dividing left by right.



50
51
52
53
54
# File 'lib/liquid2/filters/math.rb', line 50

def self.modulo(left, right)
  to_decimal(left) % to_decimal(right) # steep:ignore
rescue ZeroDivisionError => e
  raise LiquidTypeError.new(e.message, nil)
end

.newline_to_br(left) ⇒ Object

Return left with LF or CRLF replaced with <br />\n.



66
67
68
# File 'lib/liquid2/filters/string.rb', line 66

def self.newline_to_br(left)
  Liquid2.to_s(left).gsub(/\r?\n/, "<br />\n")
end

.nil_safe_casecmp(left, right) ⇒ Object



70
71
72
73
74
75
76
77
78
# File 'lib/liquid2/filters/sort.rb', line 70

def self.nil_safe_casecmp(left, right)
  if !left.nil? && !right.nil?
    left.to_s.casecmp(right.to_s)
  elsif left.nil? && right.nil?
    0
  else
    left.nil? ? 1 : -1
  end
end

.nil_safe_compare(left, right) ⇒ Object



56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/liquid2/filters/sort.rb', line 56

def self.nil_safe_compare(left, right)
  result = left <=> right

  if result
    result
  elsif left.nil?
    1
  elsif right.nil?
    -1
  else
    raise Liquid2::LiquidArgumentError.new("can't sort incomparable type", nil)
  end
end

.numeric_compare(left, right) ⇒ Object



80
81
82
83
84
# File 'lib/liquid2/filters/sort.rb', line 80

def self.numeric_compare(left, right)
  # @type var res: untyped
  res = ints(left) <=> ints(right)
  res || -1
end

.plus(left, right) ⇒ Object

Return right added to left.



57
58
59
# File 'lib/liquid2/filters/math.rb', line 57

def self.plus(left, right)
  to_decimal(left) + to_decimal(right) # steep:ignore
end

.prepend(left, right) ⇒ Object

Return right concatenated with left. Coerce left and right to strings if they aren't strings already.



72
73
74
# File 'lib/liquid2/filters/string.rb', line 72

def self.prepend(left, right)
  Liquid2.to_s(right) + Liquid2.to_s(left)
end

.reject(left, key, value = nil, context:) ⇒ Object



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/liquid2/filters/array.rb', line 147

def self.reject(left, key, value = nil, context:)
  left = Liquid2::Filters.to_enumerable(left)

  if key.is_a?(Liquid2::Lambda)
    key.map(context, left).zip(left).reject do |r, _item|
      r.is_a?(Liquid2::Undefined) || Liquid2.truthy?(context, r)
    end.map(&:last)
  elsif !value.nil? && !Liquid2.undefined?(value)
    key = Liquid2.to_s(key)
    left.reject do |item|
      fetch(item, key) == value
    end
  else
    key = Liquid2.to_s(key)
    left.reject do |item|
      Liquid2.truthy?(context, fetch(item, key))
    end
  end
end

.remove(left, pattern) ⇒ Object

Return left with all occurrences of pattern removed. All arguments are coerced to strings if they aren't strings already.



101
102
103
# File 'lib/liquid2/filters/string.rb', line 101

def self.remove(left, pattern)
  Liquid2.to_s(left).gsub(Liquid2.to_s(pattern), Liquid2.to_s(""))
end

.remove_first(left, pattern) ⇒ Object

Return left with the first occurrence of pattern removed. All arguments are coerced to strings if they aren't strings already.



107
108
109
# File 'lib/liquid2/filters/string.rb', line 107

def self.remove_first(left, pattern)
  Liquid2.to_s(left).sub(Liquid2.to_s(pattern), Liquid2.to_s(""))
end

.remove_last(left, pattern) ⇒ Object

Return left with the last occurrence of pattern removed. All arguments are coerced to strings if they aren't strings already.



113
114
115
116
117
118
119
120
# File 'lib/liquid2/filters/string.rb', line 113

def self.remove_last(left, pattern)
  return left if Liquid2.undefined?(pattern)

  head, match, tail = Liquid2.to_s(left).rpartition(Liquid2.to_s(pattern))
  return left if match.empty?

  head + tail
end

.replace(left, pattern, replacement = "") ⇒ Object

Return left with all occurrences of pattern replaced with replacement. All arguments are coerced to strings if they aren't strings already.



78
79
80
# File 'lib/liquid2/filters/string.rb', line 78

def self.replace(left, pattern, replacement = "")
  Liquid2.to_s(left).gsub(Liquid2.to_s(pattern), Liquid2.to_s(replacement))
end

.replace_first(left, pattern, replacement = "") ⇒ Object

Return left with the first occurrence of pattern replaced with replacement. All arguments are coerced to strings if they aren't strings already.



84
85
86
# File 'lib/liquid2/filters/string.rb', line 84

def self.replace_first(left, pattern, replacement = "")
  Liquid2.to_s(left).sub(Liquid2.to_s(pattern), Liquid2.to_s(replacement))
end

.replace_last(left, pattern, replacement) ⇒ Object

Return left with the last occurrence of pattern replaced with replacement. All arguments are coerced to strings if they aren't strings already.



90
91
92
93
94
95
96
97
# File 'lib/liquid2/filters/string.rb', line 90

def self.replace_last(left, pattern, replacement)
  return left + replacement if Liquid2.undefined?(pattern)

  head, match, tail = Liquid2.to_s(left).rpartition(Liquid2.to_s(pattern))
  return left if match.empty?

  head + Liquid2.to_s(replacement) + tail
end

.reverse(left) ⇒ Object

Return left with all items in reverse order. Coerce left to an array if it isn't an array already.



143
144
145
# File 'lib/liquid2/filters/array.rb', line 143

def self.reverse(left)
  to_enumerable(left).to_a.reverse
end

.round(left, ndigits = 0) ⇒ Object

Return left rounded to ndigits decimal digits.



62
63
64
65
66
67
# File 'lib/liquid2/filters/math.rb', line 62

def self.round(left, ndigits = 0)
  left = to_decimal(left)
  return left.round if ndigits == 0 # rubocop:disable Style/NumericPredicate

  left.round(to_decimal(ndigits)) # steep:ignore
end

.rstrip(left) ⇒ Object

Return left with trailing whitespace removed. Coerce left to a string if it is not one already.



55
56
57
# File 'lib/liquid2/filters/string.rb', line 55

def self.rstrip(left)
  Liquid2.to_s(left).rstrip
end

.size(left) ⇒ Object

Return the size of left, or zero if left has no size.



7
8
9
# File 'lib/liquid2/filters/size.rb', line 7

def self.size(left)
  left.respond_to?(:size) ? left.size : 0
end

.slice(left, start, length = 1) ⇒ Object

Return the subsequence of left starting at start up to length.



7
8
9
10
11
12
13
14
15
# File 'lib/liquid2/filters/slice.rb', line 7

def self.slice(left, start, length = 1)
  length = 1 if Liquid2.undefined?(length)
  case left
  when Array
    left.slice(to_integer(start), to_integer(length)) || []
  else
    Liquid2.to_s(left).slice(to_integer(start), to_integer(length)) || ""
  end
end

.sort(left, key = nil, context:) ⇒ Object



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# File 'lib/liquid2/filters/sort.rb', line 8

def self.sort(left, key = nil, context:)
  left = Liquid2::Filters.to_enumerable(left)

  case key
  when Liquid2::Lambda
    key.map(context, left).zip(left).sort do |a, b|
      nil_safe_compare(a.first, b.first)
    end.map(&:last)
  when nil, Liquid2::Undefined
    left.sort { |a, b| nil_safe_compare(a, b) }
  else
    key = Liquid2.to_s(key)
    left.sort { |a, b| nil_safe_compare(fetch(a, key), fetch(b, key)) }
  end
end

.sort_natural(left, key = nil, context:) ⇒ Object



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/liquid2/filters/sort.rb', line 24

def self.sort_natural(left, key = nil, context:)
  left = Liquid2::Filters.to_enumerable(left)

  case key
  when Liquid2::Lambda
    key.map(context, left).zip(left).sort do |a, b|
      nil_safe_casecmp(a.first, b.first)
    end.map(&:last)
  when nil, Liquid2::Undefined
    left.sort { |a, b| nil_safe_casecmp(a, b) }
  else
    key = Liquid2.to_s(key)
    left.sort { |a, b| nil_safe_casecmp(fetch(a, key), fetch(b, key)) }
  end
end

.sort_numeric(left, key = nil, context:) ⇒ Object



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/liquid2/filters/sort.rb', line 40

def self.sort_numeric(left, key = nil, context:)
  left = Liquid2::Filters.to_enumerable(left)

  case key
  when Liquid2::Lambda
    key.map(context, left).zip(left).sort do |a, b|
      numeric_compare(a.first, b.first)
    end.map(&:last)
  when nil, Liquid2::Undefined
    left.sort { |a, b| numeric_compare(a, b) }
  else
    key = Liquid2.to_s(key)
    left.sort { |a, b| numeric_compare(fetch(a, key), fetch(b, key)) }
  end
end

.split(left, pattern) ⇒ Object

Split left on every occurrence of pattern.



123
124
125
# File 'lib/liquid2/filters/string.rb', line 123

def self.split(left, pattern)
  Liquid2.to_s(left).split(Liquid2.to_s(pattern))
end

.strip(left) ⇒ Object

Return left with leading and trailing whitespace removed. Coerce left to a string if it is not one already.



61
62
63
# File 'lib/liquid2/filters/string.rb', line 61

def self.strip(left)
  Liquid2.to_s(left).strip
end

.strip_html(left) ⇒ Object

Return left with HTML tags removed.



136
137
138
# File 'lib/liquid2/filters/string.rb', line 136

def self.strip_html(left)
  Liquid2.to_s(left).gsub(RE_HTML_BLOCKS, "").gsub(RE_HTML_TAGS, "")
end

.strip_newlines(left) ⇒ Object

Return left with CR and LF removed.



141
142
143
# File 'lib/liquid2/filters/string.rb', line 141

def self.strip_newlines(left)
  Liquid2.to_s(left).gsub(/\r?\n/, "")
end

.sum(left, key = nil, context:) ⇒ Object



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/liquid2/filters/math.rb', line 69

def self.sum(left, key = nil, context:)
  left = Liquid2::Filters.to_enumerable(left)

  case key
  when Liquid2::Lambda
    items = key.map(context, left).reject do |item|
      Liquid2.undefined?(item)
    end

    items.sum { |item| Liquid2::Filters.to_decimal(item) }
  when nil, Liquid2::Undefined
    left.sum { |item| Liquid2::Filters.to_decimal(item) } # steep:ignore
  else
    k = Liquid2.to_s(key)
    left.sum { |item| Liquid2::Filters.to_decimal(fetch(item, k)) } # steep:ignore
  end
end

.times(left, right) ⇒ Object

Return the result of multiplying left by right.



35
36
37
# File 'lib/liquid2/filters/math.rb', line 35

def self.times(left, right)
  to_decimal(left) * to_decimal(right) # steep:ignore
end

.to_date(obj) ⇒ Object

Cast obj to a date and time. Return nil if casting fails. NOTE: This was copied from Shopify/liquid.



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/liquid2/filter.rb', line 65

def self.to_date(obj)
  return obj if obj.respond_to?(:strftime)

  if obj.is_a?(String)
    return nil if obj.empty?

    obj = obj.downcase
  end

  case obj
  when "now", "today"
    Time.now
  when /\A\d+\z/, Integer
    Time.at(obj.to_i)
  when String
    Time.parse(obj)
  end
rescue ::ArgumentError
  nil
end

.to_decimal(obj, default: 0) ⇒ Object

Cast obj to a number, favouring BigDecimal over Float.



48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/liquid2/filter.rb', line 48

def self.to_decimal(obj, default: 0)
  case obj
  when String
    obj.match?(/\A-?\d+(?:[eE]\+?\d+)?\Z/) ? obj.to_f.to_i : BigDecimal(obj)
  when Float
    BigDecimal(obj.to_s)
  when Integer, BigDecimal, Numeric
    obj
  else
    default
  end
rescue ArgumentError
  default
end

.to_enumerable(obj) ⇒ Enumerable

Cast obj to an enumerable for use in a Liquid filter.

Parameters:

Returns:



12
13
14
15
16
17
18
19
20
21
22
23
# File 'lib/liquid2/filter.rb', line 12

def self.to_enumerable(obj)
  case obj
  when Array
    obj.flatten
  when Hash, String
    [obj]
  when Enumerable
    obj
  else
    obj.respond_to?(:each) ? obj.each : [obj]
  end
end

.to_integer(obj) ⇒ Object

Case obj to an Integer.



43
44
45
# File 'lib/liquid2/filter.rb', line 43

def self.to_integer(obj)
  obj.is_a?(Integer) ? obj : Integer(obj)
end

.to_number(obj, default: 0) ⇒ Object

Cast obj to a number.



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/liquid2/filter.rb', line 26

def self.to_number(obj, default: 0)
  case obj
  when String
    # Cast to float before integer as `to_f` will parse exponents, `to_i` will not.
    # Use `Float(obj)` instead of `obj.to_f` because `to_f` ignores trailing non-digit chars.
    obj.match?(/\A-?\d+(?:[eE]\+?\d+)?\Z/) ? obj.to_f.to_i : Float(obj)
  when Float, Integer, BigDecimal, Numeric
    # Numeric is the base class for heap allocated numbers.
    obj
  else
    default
  end
rescue ArgumentError
  default
end

.truncate(left, max_length = 50, ellipsis = "...") ⇒ Object



145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/liquid2/filters/string.rb', line 145

def self.truncate(left, max_length = 50, ellipsis = "...")
  return if left.nil? || Liquid2.undefined?(left)

  left = Liquid2.to_s(left)
  max_length = to_integer(max_length)
  return left if left.length <= max_length

  ellipsis = Liquid2.to_s(ellipsis)
  return ellipsis[0, max_length] if ellipsis.length >= max_length

  "#{left[0...max_length - ellipsis.length]}#{ellipsis}"
end

.truncatewords(left, max_words = 15, ellipsis = "...") ⇒ Object



158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/liquid2/filters/string.rb', line 158

def self.truncatewords(left, max_words = 15, ellipsis = "...")
  return if left.nil? || Liquid2.undefined?(left)

  left = Liquid2.to_s(left)
  max_words = to_integer(max_words).clamp(1, 10_000)
  words = left.split(" ", max_words + 1)
  return left if words.length <= max_words

  ellipsis = Liquid2.to_s(ellipsis)
  words.pop
  "#{words.join(" ")}#{ellipsis}"
end

.uniq(left, key = nil, context:) ⇒ Object

Deduplicate items in left. Coerce left to an array if it isn't an array already.



189
190
191
192
193
194
195
196
197
198
199
# File 'lib/liquid2/filters/array.rb', line 189

def self.uniq(left, key = nil, context:)
  left = Liquid2::Filters.to_enumerable(left)

  if key.nil?
    left.to_a.uniq
  elsif key.is_a?(Liquid2::Lambda)
    key.map(context, left).zip(left).uniq { |r, _item| r }.map(&:last)
  else
    left.to_a.uniq { |item| fetch(item, key) }
  end
end

.upcase(left) ⇒ Object

Return left with all characters converted to uppercase. Coerce left to a string if it is not one already.



29
30
31
# File 'lib/liquid2/filters/string.rb', line 29

def self.upcase(left)
  Liquid2.to_s(left).upcase
end

.url_decode(left) ⇒ Object



175
176
177
178
179
180
181
182
183
184
# File 'lib/liquid2/filters/string.rb', line 175

def self.url_decode(left)
  return if left.nil? || Liquid2.undefined?(left)

  decoded = CGI.unescape(Liquid2.to_s(left))
  unless decoded.valid_encoding?
    raise Liquid2::LiquidArgumentError.new("invalid byte sequence", nil)
  end

  decoded
end

.url_encode(left) ⇒ Object



171
172
173
# File 'lib/liquid2/filters/string.rb', line 171

def self.url_encode(left)
  CGI.escape(Liquid2.to_s(left)) unless left.nil? || Liquid2.undefined?(left)
end

.where(left, key, value = nil, context:) ⇒ Object



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/liquid2/filters/array.rb', line 167

def self.where(left, key, value = nil, context:)
  left = Liquid2::Filters.to_enumerable(left)

  if key.is_a?(Liquid2::Lambda)
    key.map(context, left).zip(left).filter do |r, _item|
      r.is_a?(Liquid2::Undefined) || Liquid2.truthy?(context, r)
    end.map(&:last)
  elsif !value.nil? && !Liquid2.undefined?(value)
    key = Liquid2.to_s(key)
    left.filter do |item|
      fetch(item, key) == value
    end
  else
    key = Liquid2.to_s(key)
    left.filter do |item|
      Liquid2.truthy?(context, fetch(item, key))
    end
  end
end