Module: Luoma::Filters

Defined in:
lib/luoma/filters/date.rb,
lib/luoma/filters/json.rb,
lib/luoma/filters/math.rb,
lib/luoma/filters/size.rb,
lib/luoma/filters/sort.rb,
lib/luoma/filters/array.rb,
lib/luoma/filters/slice.rb,
lib/luoma/filters/string.rb,
lib/luoma/filters/default.rb,
sig/luoma/filters/date.rbs,
sig/luoma/filters/json.rbs,
sig/luoma/filters/math.rbs,
sig/luoma/filters/size.rbs,
sig/luoma/filters/sort.rbs,
sig/luoma/filters/array.rbs,
sig/luoma/filters/slice.rbs,
sig/luoma/filters/string.rbs,
sig/luoma/filters/default.rbs

Constant Summary collapse

INFINITY_ARRAY =

: [Float]

Returns:

  • (::Array[untyped])
[Float::INFINITY].freeze
RE_HTML_BLOCKS =

Returns:

  • (::Regexp)
Regexp.union(
  %r{<script.*?</script>}m,
  /<!--.*?-->/m,
  %r{<style.*?</style>}m
)
RE_HTML_TAGS =

Returns:

  • (::Regexp)
/<.*?>/m

Class Method Summary collapse

Class Method Details

.abs(context, left) ⇒ Object

Return the absolute value of left.

Parameters:

Returns:

  • (Object)


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

def self.abs(context, left)
  left_ = context.to_numeric(left)
  context.nothing?(left_) ? left_ : left_.abs
end

.all(context, left, key = :nothing, value = :nothing) ⇒ Object

Return true if all items in left are truthy.

Parameters:

  • context (FilterContext)
  • left (Object)
  • key (Object) (defaults to: :nothing)
  • value (Object) (defaults to: :nothing)

Returns:

  • (Object)


41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/luoma/filters/array.rb', line 41

def self.all(context, left, key = :nothing, value = :nothing)
  left = context.to_enumerable(left)

  case key
  when :nothing
    left.all? { |i| context.truthy?(i) }
  when ExpressionDrop
    key.expr.broadcast_with_index(left).all? { |i| context.truthy?(i) }
  else
    key = context.to_string(key)
    if value == :nothing
      left.map { |i| context.fetch(i, key) }.all? { |j| context.truthy?(j) }
    else
      left.map { |i| context.fetch(i, key) }.all? { |j| context.eq?(j, value) }
    end
  end
end

.any(context, left, key = :nothing, value = :nothing) ⇒ Object

Return true if at least one item in left is truthy.

Parameters:

  • context (FilterContext)
  • left (Object)
  • key (Object) (defaults to: :nothing)
  • value (Object) (defaults to: :nothing)

Returns:

  • (Object)


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

def self.any(context, left, key = :nothing, value = :nothing)
  left = context.to_enumerable(left)

  case key
  when :nothing
    left.any? { |i| context.truthy?(i) }
  when ExpressionDrop
    key.expr.broadcast_with_index(left).any? { |i| context.truthy?(i) }
  else
    key = context.to_string(key)
    if value == :nothing
      left.map { |i| context.fetch(i, key) }.any? { |j| context.truthy?(j) }
    else
      left.map { |i| context.fetch(i, key) }.any? { |j| context.eq?(j, value) }
    end
  end
end

.append(context, left, right) ⇒ Object

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

Parameters:

Returns:

  • (Object)


9
10
11
# File 'lib/luoma/filters/string.rb', line 9

def self.append(context, left, right)
  context.to_string(left) + context.to_string(right)
end

.at_least(context, left, right) ⇒ Object

Return the maximum of left and right.

Parameters:

Returns:

  • (Object)


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

def self.at_least(context, left, right)
  [context.to_numeric(left, default: nil), context.to_numeric(right, default: nil)].compact.max || :nothing
end

.at_most(context, left, right) ⇒ Object

Return the minimum of left and right.

Parameters:

Returns:

  • (Object)


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

def self.at_most(context, left, right)
  [context.to_numeric(left, default: nil), context.to_numeric(right, default: nil)].compact.min || :nothing
end

.base64_decodeObject

Parameters:

Returns:

  • (Object)


95
# File 'sig/luoma/filters/string.rbs', line 95

def self.base64_decode: (FilterContext context, untyped left) -> untyped

.base64_encodeObject

Parameters:

Returns:

  • (Object)


93
# File 'sig/luoma/filters/string.rbs', line 93

def self.base64_encode: (FilterContext context, untyped left) -> untyped

.base64_url_safe_decodeObject

Parameters:

Returns:

  • (Object)


99
# File 'sig/luoma/filters/string.rbs', line 99

def self.base64_url_safe_decode: (FilterContext context, untyped left) -> untyped

.base64_url_safe_encodeObject

Parameters:

Returns:

  • (Object)


97
# File 'sig/luoma/filters/string.rbs', line 97

def self.base64_url_safe_encode: (FilterContext context, untyped left) -> untyped

.capitalize(context, 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.

Parameters:

Returns:

  • (Object)


15
16
17
# File 'lib/luoma/filters/string.rb', line 15

def self.capitalize(context, left)
  context.to_string(left).capitalize
end

.ceil(context, left) ⇒ Object

Return left rounded up to the next whole number.

Parameters:

Returns:

  • (Object)


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

def self.ceil(context, left)
  left_ = context.to_numeric(left)
  context.nothing?(left_) ? left_ : left_.ceil
end

.compact(context, left, key = :nothing) ⇒ 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 :nothing, coerce it to a string before calling fetch on items in left.

Parameters:

  • context (FilterContext)
  • left (Object)
  • key (Object) (defaults to: :nothing)

Returns:

  • (Object)


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

def self.compact(context, left, key = :nothing)
  left = context.to_enumerable(left)

  case key
  when :nothing
    left.reject { |value| value.nil? || context.nothing?(value) }
  when ExpressionDrop
    left.each_with_index.reject do |item, index|
      value = key.expr.call_with_index(item, index)
      value.nil? || context.nothing?(value)
    end.map(&:first)
  else
    left.reject do |item|
      value = context.fetch(item, key)
      value.nil? || context.nothing?(value)
    end
  end
end

.concat(context, 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.

Parameters:

Returns:

  • (Object)


88
89
90
# File 'lib/luoma/filters/array.rb', line 88

def self.concat(context, left, right)
  context.to_a(left).concat(context.to_a(right))
end

.date(context, 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.

Parameters:

Returns:

  • (Object)


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

def self.date(context, left, format)
  format = context.to_string(format)
  return left if format.empty?

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

.default(context, left, default = "", 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.

Parameters:

  • context (FilterContext)
  • left (Object)
  • default (::String) (defaults to: "")
  • allow_false: (Boolean) (defaults to: false)

Returns:

  • (Object)


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

def self.default(context, left, default = "", allow_false: false)
  return default if left.is_a?(FalsyStrictUndefinedDrop)

  left_ = left.is_a?(Drop) ? left.to_primitive(:boolean, context.render_context) : left
  return left_ if allow_false && left_ == false

  !context.truthy?(left_) || Luoma::Predicates.empty?(context.render_context, left_) ? default : left_
end

.divided_by(context, left, right) ⇒ Object

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

Parameters:

Returns:

  • (Object)


29
30
31
32
33
34
35
36
# File 'lib/luoma/filters/math.rb', line 29

def self.divided_by(context, left, right)
  lhs = context.to_numeric(left, default: :nothing)
  rhs = context.to_numeric(right, default: :nothing)
  return :nothing if context.nothing?(lhs) || context.nothing?(rhs) || rhs.zero? # steep:ignore

  result = lhs.to_d / rhs # steep:ignore
  result.frac.zero? && lhs.is_a?(::Integer) && rhs.is_a?(::Integer) ? result.to_i : result
end

.downcase(context, left) ⇒ Object

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

Parameters:

Returns:

  • (Object)


21
22
23
# File 'lib/luoma/filters/string.rb', line 21

def self.downcase(context, left)
  context.to_string(left).downcase
end

.escape(context, left) ⇒ untyped?

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

Parameters:

Returns:

  • (untyped, nil)


33
34
35
# File 'lib/luoma/filters/string.rb', line 33

def self.escape(context, left)
  CGI.escape_html(context.to_string(left)) unless left.nil?
end

.escape_once(context, 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.

Parameters:

Returns:

  • (Object)


41
42
43
# File 'lib/luoma/filters/string.rb', line 41

def self.escape_once(context, left)
  CGI.escape_html(CGI.unescape_html(context.to_string(left)))
end

.find(context, left, key, value = :nothing) ⇒ untyped?

Return the first item in left where key is equal to value, or nil if no such item exists.

If value is not given, return the first item where key is truthy.

If key is a Lambda expression, evaluate the expression for each item in left and return the first item where the expression results in a truthy value, ignoring value.

Parameters:

  • context (FilterContext)
  • left (Object)
  • key (Object)
  • value (Object, nil) (defaults to: :nothing)

Returns:

  • (untyped, nil)


100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/luoma/filters/array.rb', line 100

def self.find(context, left, key, value = :nothing)
  left = context.to_enumerable(left)

  if key.is_a?(ExpressionDrop)
    key.expr.broadcast_with_index(left).zip(left) do |r, i|
      return i if context.truthy?(r)
    end
  elsif value == :nothing
    key_ = context.to_string(key)
    left.each do |item|
      return item if context.truthy?(context.fetch(item, key_)) || context.eq?(item, key)
    end
  else
    key = context.to_string(key)
    left.each do |item|
      return item if context.eq?(context.fetch(item, key), value) || context.eq?(item, value)
    end
  end

  nil
end

.find_index(context, left, key, value = :nothing) ⇒ untyped?

Return the index of the first item in left where key is equal to value, or nil if no such item exists.

If value is not given, return the index of the first item where key is truthy.

If key is a Lambda expression, evaluate the expression for each item in left and return the index of the first item where the expression results in a truthy value, ignoring value.

Parameters:

  • context (FilterContext)
  • left (Object)
  • key (Object)
  • value (Object, nil) (defaults to: :nothing)

Returns:

  • (untyped, nil)


131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/luoma/filters/array.rb', line 131

def self.find_index(context, left, key, value = :nothing)
  left = context.to_enumerable(left)

  if key.is_a?(ExpressionDrop)
    key.expr.broadcast_with_index(left).each_with_index do |item, index|
      return index if context.truthy?(item)
    end
  elsif value == :nothing
    key_ = context.to_string(key)
    left.each_with_index do |item, index|
      return index if context.truthy?(context.fetch(item, key_)) || context.eq?(item, key)
    end
  else
    key = context.to_string(key)
    left.each_with_index do |item, index|
      return index if context.eq?(context.fetch(item, key), value) || context.eq?(item, value)
    end
  end

  nil
end

.first(context, left) ⇒ Object

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

Parameters:

Returns:

  • (Object)


157
158
159
160
161
162
163
164
165
166
167
# File 'lib/luoma/filters/array.rb', line 157

def self.first(context, left)
  case left
  when String
    left.empty? ? :nothing : left[0]
  when Hash, Array
    left.empty? ? :nothing : left.first
  else
    left_ = context.to_a(left)
    left_.empty? ? :nothing : left_.first
  end
end

.flat_map(context, left, key) ⇒ Object

Parameters:

Returns:

  • (Object)


196
197
198
199
200
201
202
203
204
205
# File 'lib/luoma/filters/array.rb', line 196

def self.flat_map(context, left, key)
  left = context.to_enumerable(left)

  if key.is_a?(ExpressionDrop)
    key.expr.broadcast_with_index(left).flatten!(1)
  else
    key = context.to_string(key)
    left.flat_map { |item| context.fetch(item, key) }
  end
end

.flatten(context, left, depth = nil) ⇒ Object

If left is enumerable, return left flattened to depth. Otherwise return left.

Parameters:

  • context (FilterContext)
  • left (Object)
  • depth (Object, nil) (defaults to: nil)

Returns:

  • (Object)


13
14
15
16
17
18
19
# File 'lib/luoma/filters/array.rb', line 13

def self.flatten(context, left, depth = nil)
  left = context.to_enumerable(left)

  context.to_a(left).flatten(
    depth.nil? ? depth : context.to_numeric(depth, default: 1).to_i # steep:ignore
  )
end

.floor(context, left) ⇒ Object

Return left rounded down to the next whole number.

Parameters:

Returns:

  • (Object)


46
47
48
49
# File 'lib/luoma/filters/math.rb', line 46

def self.floor(context, left)
  left_ = context.to_numeric(left)
  context.nothing?(left_) ? left_ : left_.floor
end

.hastrue, false

Parameters:

  • context (FilterContext)
  • left (Object)
  • key (Object)
  • value (Object, nil)

Returns:

  • (true, false)


35
# File 'sig/luoma/filters/array.rbs', line 35

def self.has: (FilterContext context, untyped left, untyped key, ?untyped? value) -> (true | false)

.ints(obj, context) ⇒ ::Array[untyped], untyped

Parameters:

Returns:

  • (::Array[untyped], untyped)


102
103
104
105
106
107
108
109
110
111
# File 'lib/luoma/filters/sort.rb', line 102

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

    numeric.map(&:to_i) # steep:ignore
  end
end

.join(context, 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.

Parameters:

  • context (FilterContext)
  • left (Object)
  • sep (::String) (defaults to: " ")

Returns:

  • (Object)


7
8
9
# File 'lib/luoma/filters/array.rb', line 7

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

.json(context, left, pretty: false) ⇒ Object

Return left serialized in JSON format.

Parameters:

  • context (FilterContext)
  • left (Object)
  • pretty: (Boolean) (defaults to: false)

Returns:

  • (Object)


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

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

.last(context, left) ⇒ Object

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

Parameters:

Returns:

  • (Object)


173
174
175
176
177
178
179
180
181
182
183
# File 'lib/luoma/filters/array.rb', line 173

def self.last(context, left)
  case left
  when String
    left.empty? ? :nothing : left[-1]
  when Array
    left.empty? ? :nothing : left.last
  else
    left_ = context.to_a(left)
    left_.empty? ? :nothing : left_.last
  end
end

.lstrip(context, left) ⇒ Object

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

Parameters:

Returns:

  • (Object)


47
48
49
# File 'lib/luoma/filters/string.rb', line 47

def self.lstrip(context, left)
  context.to_string(left).lstrip
end

.map(context, left, key) ⇒ Object

Parameters:

Returns:

  • (Object)


185
186
187
188
189
190
191
192
193
194
# File 'lib/luoma/filters/array.rb', line 185

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

  if key.is_a?(ExpressionDrop)
    key.expr.broadcast_with_index(left)
  else
    key = context.to_string(key)
    left.map { |item| context.fetch(item, key) }
  end
end

.max(context, left, key = :nothing) ⇒ Object

Return the maximum numeric value found in left. Coerce left to an array if it's not an array already.

Parameters:

  • context (FilterContext)
  • left (Object)
  • key (Object) (defaults to: :nothing)

Returns:

  • (Object)


341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/luoma/filters/array.rb', line 341

def self.max(context, left, key = :nothing)
  left = context.to_enumerable(left)

  # @type var max_: ^(Enumerable[untyped]) ?{ (Integer, untyped) -> untyped } -> untyped
  max_ = lambda do |enum, &key|
    best_item = nil # untyped
    best_value = nil #: Numeric?

    enum.each_with_index do |item, index|
      value = context.to_numeric(key ? key.call(item, index) : item, default: nil)
      next if value.nil?

      if best_value.nil? || context.lt?(best_value, value)
        best_item = item
        best_value = value
      end
    end

    best_item
  end

  if key == :nothing
    max_.call(left)
  elsif key.is_a?(ExpressionDrop)
    max_.call(left) { |item, index| key.expr.call_with_index(item, index) }
  else
    key = context.to_string(key)
    max_.call(left) { |item, _index| context.fetch(item, key, default: nil) }
  end
end

.min(context, left, key = :nothing) ⇒ Object

Return the minimum numeric value found in left. Coerce left to an array if it's not an array already.

Parameters:

  • context (FilterContext)
  • left (Object)
  • key (Object) (defaults to: :nothing)

Returns:

  • (Object)


308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/luoma/filters/array.rb', line 308

def self.min(context, left, key = :nothing)
  left = context.to_enumerable(left)

  # @type var min_: ^(Enumerable[untyped]) ?{ (untyped, Integer) -> untyped } -> untyped
  min_ = lambda do |enum, &key|
    best_item = nil #:untyped
    best_value = nil #: Numeric?

    enum.each_with_index do |item, index|
      value = context.to_numeric(key ? key.call(item, index) : item, default: nil)
      next if value.nil?

      if best_value.nil? || context.lt?(value, best_value)
        best_item = item
        best_value = value
      end
    end

    best_item
  end

  if key == :nothing
    min_.call(left)
  elsif key.is_a?(ExpressionDrop)
    min_.call(left) { |item, index| key.expr.call_with_index(item, index) }
  else
    key = context.to_string(key)
    min_.call(left) { |item, _index| context.fetch(item, key, default: nil) }
  end
end

.minus(context, left, right) ⇒ Object

Return right subtracted from left.

Parameters:

Returns:

  • (Object)


52
53
54
55
56
# File 'lib/luoma/filters/math.rb', line 52

def self.minus(context, left, right)
  lhs = context.to_numeric(left, default: :nothing)
  rhs = context.to_numeric(right, default: :nothing)
  context.nothing?(lhs) || context.nothing?(rhs) ? :nothing : lhs - rhs # steep:ignore
end

.modulo(context, left, right) ⇒ Object

Return the remainder of dividing left by right.

Parameters:

Returns:

  • (Object)


59
60
61
62
63
64
65
66
# File 'lib/luoma/filters/math.rb', line 59

def self.modulo(context, left, right)
  lhs = context.to_numeric(left, default: :nothing)
  rhs = context.to_numeric(right, default: :nothing)
  return :nothing if context.nothing?(lhs) || context.nothing?(rhs) || rhs.zero? # steep:ignore

  result = lhs.to_d % rhs # steep:ignore
  result.frac.zero? && lhs.is_a?(::Integer) && rhs.is_a?(::Integer) ? result.to_i : result
end

.newline_to_br(context, left) ⇒ Object

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

Parameters:

Returns:

  • (Object)


64
65
66
# File 'lib/luoma/filters/string.rb', line 64

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

.nil_safe_casecmp(left, right) ⇒ untyped, ...

Parameters:

  • left (Object)
  • right (Object)

Returns:

  • (untyped, 0, 1, -1)


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

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

.numeric_compare(left, right, context) ⇒ Object

Parameters:

Returns:

  • (Object)


97
98
99
100
# File 'lib/luoma/filters/sort.rb', line 97

def self.numeric_compare(left, right, context)
  res = ints(left, context) <=> ints(right, context)
  res || -1
end

.plus(context, left, right) ⇒ Object

Return right added to left.

Parameters:

Returns:

  • (Object)


69
70
71
72
73
# File 'lib/luoma/filters/math.rb', line 69

def self.plus(context, left, right)
  lhs = context.to_numeric(left, default: :nothing)
  rhs = context.to_numeric(right, default: :nothing)
  context.nothing?(lhs) || context.nothing?(rhs) ? :nothing : lhs + rhs # steep:ignore
end

.prepend_(context, left, right) ⇒ Object

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

Parameters:

Returns:

  • (Object)


70
71
72
# File 'lib/luoma/filters/string.rb', line 70

def self.prepend_(context, left, right)
  context.to_string(right) + context.to_string(left)
end

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

Parameters:

  • context (FilterContext)
  • left (Object)
  • key (Object)
  • value (Object, nil) (defaults to: :nothing)

Returns:

  • (Object)


213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/luoma/filters/array.rb', line 213

def self.reject(context, left, key, value = :nothing)
  left = context.to_enumerable(left)

  if key.is_a?(ExpressionDrop)
    left.each_with_index.reject do |item, index|
      context.truthy?(key.expr.call_with_index(item, index))
    end.map(&:first)
  elsif value == :nothing
    key_ = context.to_string(key)
    left.reject do |item|
      context.truthy?(context.fetch(item, key_)) || context.eq?(item, key)
    end
  else
    key = context.to_string(key)
    left.reject do |item|
      context.eq?(context.fetch(item, key), value) || context.eq?(item, value)
    end
  end
end

.remove(context, left, pattern) ⇒ Object

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

Parameters:

Returns:

  • (Object)


99
100
101
# File 'lib/luoma/filters/string.rb', line 99

def self.remove(context, left, pattern)
  context.to_string(left).gsub(context.to_string(pattern), context.to_string(""))
end

.remove_first(context, left, pattern) ⇒ Object

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

Parameters:

Returns:

  • (Object)


105
106
107
# File 'lib/luoma/filters/string.rb', line 105

def self.remove_first(context, left, pattern)
  context.to_string(left).sub(context.to_string(pattern), context.to_string(""))
end

.remove_last(context, left, pattern) ⇒ Object

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

Parameters:

Returns:

  • (Object)


111
112
113
114
115
116
117
118
# File 'lib/luoma/filters/string.rb', line 111

def self.remove_last(context, left, pattern)
  return left if context.nothing?(pattern)

  head, match, tail = context.to_string(left).rpartition(context.to_string(pattern))
  return left if match.empty?

  head + tail
end

.replace(context, 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.

Parameters:

  • context (FilterContext)
  • left (Object)
  • pattern (Object)
  • replacement (::String) (defaults to: "")

Returns:

  • (Object)


76
77
78
# File 'lib/luoma/filters/string.rb', line 76

def self.replace(context, left, pattern, replacement = "")
  context.to_string(left).gsub(context.to_string(pattern), context.to_string(replacement))
end

.replace_first(context, 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.

Parameters:

  • context (FilterContext)
  • left (Object)
  • pattern (Object)
  • replacement (::String) (defaults to: "")

Returns:

  • (Object)


82
83
84
# File 'lib/luoma/filters/string.rb', line 82

def self.replace_first(context, left, pattern, replacement = "")
  context.to_string(left).sub(context.to_string(pattern), context.to_string(replacement))
end

.replace_last(context, 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.

Parameters:

  • context (FilterContext)
  • left (Object)
  • pattern (Object)
  • replacement (Object)

Returns:

  • (Object)


88
89
90
91
92
93
94
95
# File 'lib/luoma/filters/string.rb', line 88

def self.replace_last(context, left, pattern, replacement)
  return left + replacement if context.nothing?(pattern)

  head, match, tail = context.to_string(left).rpartition(context.to_string(pattern))
  return left if match.empty?

  head + context.to_string(replacement) + tail
end

.reverse(context, left) ⇒ Object

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

Parameters:

Returns:

  • (Object)


209
210
211
# File 'lib/luoma/filters/array.rb', line 209

def self.reverse(context, left)
  context.to_a(left).reverse
end

.round(context, left, ndigits = 0) ⇒ Object

Return left rounded to ndigits decimal digits.

Parameters:

  • context (FilterContext)
  • left (Object)
  • ndigits (::Integer) (defaults to: 0)

Returns:

  • (Object)


76
77
78
79
80
81
82
# File 'lib/luoma/filters/math.rb', line 76

def self.round(context, left, ndigits = 0)
  left_ = context.to_numeric(left)
  return left_ if context.nothing?(left_)
  return left_.round if ndigits == 0 # rubocop:disable Style/NumericPredicate

  left_.round(context.to_i(ndigits, default: 0))
end

.rstrip(context, left) ⇒ Object

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

Parameters:

Returns:

  • (Object)


53
54
55
# File 'lib/luoma/filters/string.rb', line 53

def self.rstrip(context, left)
  context.to_string(left).rstrip
end

.size(context, left) ⇒ Integer

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

Parameters:

Returns:

  • (Integer)


6
7
8
9
10
11
# File 'lib/luoma/filters/size.rb', line 6

def self.size(context, left)
  return left.length(context.render_context) if left.is_a?(Drop)
  return left.size if left.respond_to?(:size)

  0
end

.slice(context, left, start_ = :nothing, stop_ = :nothing, step_ = :nothing, start: :nothing, stop: :nothing, step: :nothing) ⇒ Object



5
6
7
8
9
10
11
12
13
14
15
16
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
# File 'lib/luoma/filters/slice.rb', line 5

def self.slice(
  context,
  left,
  start_ = :nothing, stop_ = :nothing, step_ = :nothing,
  start: :nothing, stop: :nothing, step: :nothing
)
  left = context.to_a(left) unless left.is_a?(String) || left.is_a?(Drop)
  length = left.is_a?(Drop) ? left.length(context.render_context) : left.length

  # Give priority to keyword arguments, default to nil if neither are given.
  start = start_ == :nothing ? nil : start_ if start == :nothing
  stop = stop_ == :nothing ? nil : stop_ if stop == :nothing
  step = step_ == :nothing ? nil : step_ if step == :nothing
  step = context.to_i(step || 1, default: 0)

  return [] if length.zero? || step.zero?

  start = context.to_i(start, default: 0) unless start.nil?
  stop = context.to_i(stop, default: 0) 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

  if left.is_a?(Drop)
    left.slice(normalized_start, normalized_stop, step)
  else
    # This does not work for strings.
    # left[(normalized_start...normalized_stop).step(step)]
    #
    # But this does.
    (normalized_start...normalized_stop).step(step).map { |i| left[i] }
  end
end

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

Return a sorted array of items from left. If left contains incomparable items, return left as an array with its original ordering.

Coerce left to an array if it's not one already.

Parameters:

  • context (FilterContext)
  • left (Object)
  • key (Object, nil) (defaults to: nil)

Returns:

  • (Object)


12
13
14
15
16
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/luoma/filters/sort.rb', line 12

def self.sort(context, left, key = nil)
  left = context.to_a(left)

  # @type var sort_: ^(Array[untyped]) ?{ (untyped, Integer) -> untyped } -> Array[untyped]
  sort_ = lambda do |array, &key|
    decorated = array.each_with_index.map do |item, index|
      [item, key ? key.call(item, index) : item]
    end

    begin
      decorated.sort! do |(_, a), (_, b)|
        cmp = context.cmp(a, b)

        case cmp
        when nil
          if a.nil? || context.nothing?(a)
            1
          elsif b.nil? || context.nothing?(b)
            -1
          else
            raise IncomparableValuesError
          end
        else
          cmp
        end
      end
    rescue IncomparableValuesError
      raise LuomaError.new("Cannot sort incomparable values") if context.render_context.env.strict

      return array # rubocop:disable Lint/NoReturnInBeginEndBlocks
    end

    decorated.map!(&:first)
  end

  if key.nil? || context.nothing?(key)
    sort_.call(left)
  elsif key.is_a?(ExpressionDrop)
    sort_.call(left) { |item, index| key.expr.call_with_index(item, index) }
  else
    key = context.to_string(key)
    sort_.call(left) { |item, _index| context.fetch(item, key) }
  end
end

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

Parameters:

  • context (FilterContext)
  • left (Object)
  • key (Object, nil) (defaults to: :nothing)

Returns:

  • (Object)


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

def self.sort_natural(context, left, key = :nothing)
  left = context.to_enumerable(left)

  if key.nil? || context.nothing?(key)
    left.sort { |a, b| nil_safe_casecmp(a, b) }
  elsif key.is_a?(ExpressionDrop)
    key.expr.broadcast_with_index(left).zip(left).sort do |a, b|
      nil_safe_casecmp(a.first, b.first)
    end.map(&:last)
  else
    key = context.to_string(key)
    left.sort { |a, b| nil_safe_casecmp(context.fetch(a, key), context.fetch(b, key)) }
  end
end

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

Parameters:

  • context (FilterContext)
  • left (Object)
  • key (Object, nil) (defaults to: :nothing)

Returns:

  • (Object)


72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/luoma/filters/sort.rb', line 72

def self.sort_numeric(context, left, key = :nothing)
  left = context.to_enumerable(left)

  if key.nil? || context.nothing?(key)
    left.sort { |a, b| numeric_compare(a, b, context) }
  elsif key.is_a?(ExpressionDrop)
    key.expr.broadcast_with_index(left).zip(left).sort do |a, b|
      numeric_compare(a.first, b.first, context)
    end.map(&:last)
  else
    key = context.to_string(key)
    left.sort { |a, b| numeric_compare(context.fetch(a, key), context.fetch(b, key), context) }
  end
end

.split(context, left, pattern) ⇒ Object

Split left on every occurrence of pattern.

Parameters:

Returns:

  • (Object)


121
122
123
# File 'lib/luoma/filters/string.rb', line 121

def self.split(context, left, pattern)
  context.to_string(left).split(context.to_string(pattern))
end

.squish(context, left) ⇒ Object

Parameters:

Returns:

  • (Object)


182
183
184
# File 'lib/luoma/filters/string.rb', line 182

def self.squish(context, left)
  context.to_string(left).strip.gsub(/\s+/, " ") unless left.nil?
end

.strip(context, left) ⇒ Object

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

Parameters:

Returns:

  • (Object)


59
60
61
# File 'lib/luoma/filters/string.rb', line 59

def self.strip(context, left)
  context.to_string(left).strip
end

.strip_html(context, left) ⇒ Object

Return left with HTML tags removed.

Parameters:

Returns:

  • (Object)


134
135
136
# File 'lib/luoma/filters/string.rb', line 134

def self.strip_html(context, left)
  context.to_string(left).gsub(RE_HTML_BLOCKS, "").gsub(RE_HTML_TAGS, "")
end

.strip_newlines(context, left) ⇒ Object

Return left with CR and LF removed.

Parameters:

Returns:

  • (Object)


139
140
141
# File 'lib/luoma/filters/string.rb', line 139

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

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

Return the sum of all numeric values in the input array.

Parameters:

  • context (FilterContext)
  • left (Object)
  • key (Object, nil) (defaults to: :nothing)

Returns:

  • (Object)


287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/luoma/filters/array.rb', line 287

def self.sum(context, left, key = :nothing)
  left = context.to_enumerable(left)

  if key == :nothing
    left.sum { |v| context.to_numeric(v, default: 0) }
  elsif key.is_a?(ExpressionDrop)
    key.expr.broadcast_with_index(left).sum { |v| context.to_numeric(v, default: 0) }
  else
    key = context.to_string(key)
    left.sum { |v| context.to_numeric(context.fetch(v, key, default: 0)) }
  end
end

.take(context, left, count) ⇒ Array[untyped]

Return at most count items from _left as a new array. Coerce left to an array if it's not an array already.

Parameters:

Returns:

  • (Array[untyped])


375
376
377
378
379
# File 'lib/luoma/filters/array.rb', line 375

def self.take(context, left, count)
  count_ = context.to_i(count, default: 0)
  count_ = 0 if count_.negative?
  context.to_enumerable(left).take(count_)
end

.times(context, left, right) ⇒ Object

Return the result of multiplying left by right.

Parameters:

Returns:

  • (Object)


39
40
41
42
43
# File 'lib/luoma/filters/math.rb', line 39

def self.times(context, left, right)
  lhs = context.to_numeric(left, default: :nothing)
  rhs = context.to_numeric(right, default: :nothing)
  context.nothing?(lhs) || context.nothing?(rhs) ? :nothing : lhs * rhs # steep:ignore
end

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

Parameters:

  • context (FilterContext)
  • left (Object)
  • max_length (::Integer) (defaults to: 50)
  • ellipsis (::String) (defaults to: "...")

Returns:

  • (Object)


143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/luoma/filters/string.rb', line 143

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

  left = context.to_string(left)
  max_length = context.to_i(max_length)
  return left if left.length <= max_length

  ellipsis = context.to_string(ellipsis)
  return ellipsis[0, max_length] if ellipsis.length >= max_length

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

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

Parameters:

  • context (FilterContext)
  • left (Object)
  • max_words (::Integer) (defaults to: 15)
  • ellipsis (::String) (defaults to: "...")

Returns:

  • (Object)


156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/luoma/filters/string.rb', line 156

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

  left = context.to_string(left)
  max_words = context.to_i(max_words).clamp(1, 10_000)
  words = left.split(" ", max_words + 1)
  return left if words.length <= max_words

  ellipsis = context.to_string(ellipsis)
  words.pop
  "#{words.join(" ")}#{ellipsis}"
end

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

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

Parameters:

  • context (FilterContext)
  • left (Object)
  • key (Object, nil) (defaults to: :nothing)

Returns:

  • (Object)


255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/luoma/filters/array.rb', line 255

def self.uniq(context, left, key = :nothing)
  left = context.to_a(left)

  # Like Array#uniq but using our own comparator, context#eq?.
  # @type var uniq_: ^(Array[untyped]) ?{ (untyped, Integer) -> untyped } -> Array[untyped]
  uniq_ = lambda do |array, &key|
    decorated = [] #: Array[untyped]
    result = [] #: Array[untyped]

    array.each_with_index do |item, index|
      value = key ? key.call(item, index) : item

      unless decorated.any? { |existing| context.eq?(existing, value) }
        decorated << value
        result << item
      end
    end

    result
  end

  if key == :nothing
    uniq_.call(left)
  elsif key.is_a?(ExpressionDrop)
    uniq_.call(left) { |item, index| key.expr.call_with_index(item, index) }
  else
    key = context.to_string(key)
    uniq_.call(left) { |item, _index| context.fetch(item, key) }
  end
end

.upcase(context, left) ⇒ Object

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

Parameters:

Returns:

  • (Object)


27
28
29
# File 'lib/luoma/filters/string.rb', line 27

def self.upcase(context, left)
  context.to_string(left).upcase
end

.url_decode(context, left) ⇒ Object

Parameters:

Returns:

  • (Object)


173
174
175
176
177
178
179
180
# File 'lib/luoma/filters/string.rb', line 173

def self.url_decode(context, left)
  return if left.nil? || context.nothing?(left)

  decoded = CGI.unescape(context.to_string(left))
  raise context.argument_error("invalid byte sequence") unless decoded.valid_encoding?

  decoded
end

.url_encode(context, left) ⇒ Object

Parameters:

Returns:

  • (Object)


169
170
171
# File 'lib/luoma/filters/string.rb', line 169

def self.url_encode(context, left)
  CGI.escape(context.to_string(left)) unless left.nil? || context.nothing?(left)
end

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

Parameters:

  • context (FilterContext)
  • left (Object)
  • key (Object)
  • value (Object, nil) (defaults to: :nothing)

Returns:

  • (Object)


233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/luoma/filters/array.rb', line 233

def self.where(context, left, key, value = :nothing)
  left = context.to_enumerable(left)

  if key.is_a?(ExpressionDrop)
    left.each_with_index.filter do |item, index|
      context.truthy?(key.expr.call_with_index(item, index))
    end.map(&:first)
  elsif value == :nothing
    key_ = context.to_string(key)
    left.filter do |item|
      context.truthy?(context.fetch(item, key_)) || context.eq?(item, key)
    end
  else
    key = context.to_string(key)
    left.filter do |item|
      context.eq?(context.fetch(item, key), value) || context.eq?(item, value)
    end
  end
end

.zip(context, left, right) ⇒ Object

Combine left with right, producing an array or pairs. Coerce left and right to arrays if they aren't arrays already.

Parameters:

Returns:

  • (Object)


302
303
304
# File 'lib/luoma/filters/array.rb', line 302

def self.zip(context, left, right)
  context.to_a(left).zip(context.to_a(right))
end