liquid2 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/.rubocop.yml +46 -0
- data/.ruby-version +1 -0
- data/.vscode/settings.json +32 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/LICENSE_SHOPIFY.txt +20 -0
- data/README.md +219 -0
- data/Rakefile +23 -0
- data/Steepfile +26 -0
- data/lib/liquid2/context.rb +297 -0
- data/lib/liquid2/environment.rb +287 -0
- data/lib/liquid2/errors.rb +79 -0
- data/lib/liquid2/expression.rb +20 -0
- data/lib/liquid2/expressions/arguments.rb +25 -0
- data/lib/liquid2/expressions/array.rb +20 -0
- data/lib/liquid2/expressions/blank.rb +41 -0
- data/lib/liquid2/expressions/boolean.rb +20 -0
- data/lib/liquid2/expressions/filtered.rb +136 -0
- data/lib/liquid2/expressions/identifier.rb +43 -0
- data/lib/liquid2/expressions/lambda.rb +53 -0
- data/lib/liquid2/expressions/logical.rb +71 -0
- data/lib/liquid2/expressions/loop.rb +79 -0
- data/lib/liquid2/expressions/path.rb +33 -0
- data/lib/liquid2/expressions/range.rb +28 -0
- data/lib/liquid2/expressions/relational.rb +119 -0
- data/lib/liquid2/expressions/template_string.rb +20 -0
- data/lib/liquid2/filter.rb +95 -0
- data/lib/liquid2/filters/array.rb +202 -0
- data/lib/liquid2/filters/date.rb +20 -0
- data/lib/liquid2/filters/default.rb +16 -0
- data/lib/liquid2/filters/json.rb +15 -0
- data/lib/liquid2/filters/math.rb +87 -0
- data/lib/liquid2/filters/size.rb +11 -0
- data/lib/liquid2/filters/slice.rb +17 -0
- data/lib/liquid2/filters/sort.rb +96 -0
- data/lib/liquid2/filters/string.rb +204 -0
- data/lib/liquid2/loader.rb +59 -0
- data/lib/liquid2/loaders/file_system_loader.rb +76 -0
- data/lib/liquid2/loaders/mixins.rb +52 -0
- data/lib/liquid2/node.rb +113 -0
- data/lib/liquid2/nodes/comment.rb +18 -0
- data/lib/liquid2/nodes/output.rb +24 -0
- data/lib/liquid2/nodes/tags/assign.rb +35 -0
- data/lib/liquid2/nodes/tags/block_comment.rb +26 -0
- data/lib/liquid2/nodes/tags/capture.rb +40 -0
- data/lib/liquid2/nodes/tags/case.rb +111 -0
- data/lib/liquid2/nodes/tags/cycle.rb +63 -0
- data/lib/liquid2/nodes/tags/decrement.rb +29 -0
- data/lib/liquid2/nodes/tags/doc.rb +24 -0
- data/lib/liquid2/nodes/tags/echo.rb +31 -0
- data/lib/liquid2/nodes/tags/extends.rb +3 -0
- data/lib/liquid2/nodes/tags/for.rb +155 -0
- data/lib/liquid2/nodes/tags/if.rb +84 -0
- data/lib/liquid2/nodes/tags/include.rb +123 -0
- data/lib/liquid2/nodes/tags/increment.rb +29 -0
- data/lib/liquid2/nodes/tags/inline_comment.rb +28 -0
- data/lib/liquid2/nodes/tags/liquid.rb +29 -0
- data/lib/liquid2/nodes/tags/macro.rb +3 -0
- data/lib/liquid2/nodes/tags/raw.rb +30 -0
- data/lib/liquid2/nodes/tags/render.rb +137 -0
- data/lib/liquid2/nodes/tags/tablerow.rb +143 -0
- data/lib/liquid2/nodes/tags/translate.rb +3 -0
- data/lib/liquid2/nodes/tags/unless.rb +23 -0
- data/lib/liquid2/nodes/tags/with.rb +3 -0
- data/lib/liquid2/parser.rb +917 -0
- data/lib/liquid2/scanner.rb +595 -0
- data/lib/liquid2/static_analysis.rb +301 -0
- data/lib/liquid2/tag.rb +22 -0
- data/lib/liquid2/template.rb +182 -0
- data/lib/liquid2/undefined.rb +131 -0
- data/lib/liquid2/utils/cache.rb +80 -0
- data/lib/liquid2/utils/chain_hash.rb +40 -0
- data/lib/liquid2/utils/unescape.rb +119 -0
- data/lib/liquid2/version.rb +5 -0
- data/lib/liquid2.rb +90 -0
- data/performance/benchmark.rb +73 -0
- data/performance/memory_profile.rb +62 -0
- data/performance/profile.rb +71 -0
- data/sig/liquid2.rbs +2348 -0
- data.tar.gz.sig +0 -0
- metadata +164 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bigdecimal"
|
4
|
+
require "time"
|
5
|
+
|
6
|
+
module Liquid2
|
7
|
+
# Liquid filters and helper methods.
|
8
|
+
module Filters
|
9
|
+
# Cast _obj_ to an enumerable for use in a Liquid filter.
|
10
|
+
# @param obj [Object]
|
11
|
+
# @return [Enumerable]
|
12
|
+
def self.to_enumerable(obj)
|
13
|
+
case obj
|
14
|
+
when Array
|
15
|
+
obj.flatten
|
16
|
+
when Hash, String
|
17
|
+
[obj]
|
18
|
+
# when String
|
19
|
+
# obj.each_char
|
20
|
+
when Enumerable
|
21
|
+
obj
|
22
|
+
else
|
23
|
+
obj.respond_to?(:each) ? obj.each : [obj]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Cast _obj_ to a number.
|
28
|
+
def self.to_number(obj, default: 0)
|
29
|
+
case obj
|
30
|
+
when String
|
31
|
+
# Cast to float before integer as `to_f` will parse exponents, `to_i` will not.
|
32
|
+
# Use `Float(obj)` instead of `obj.to_f` because `to_f` ignores trailing non-digit chars.
|
33
|
+
obj.match?(/\A-?\d+(?:[eE]\+?\d+)?\Z/) ? obj.to_f.to_i : Float(obj)
|
34
|
+
when Float, Integer, BigDecimal, Numeric
|
35
|
+
# Numeric is the base class for heap allocated numbers.
|
36
|
+
obj
|
37
|
+
else
|
38
|
+
default
|
39
|
+
end
|
40
|
+
rescue ArgumentError
|
41
|
+
default
|
42
|
+
end
|
43
|
+
|
44
|
+
# Case _obj_ to an Integer.
|
45
|
+
def self.to_integer(obj)
|
46
|
+
obj.is_a?(Integer) ? obj : Integer(obj)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Cast _obj_ to a number, favouring BigDecimal over Float.
|
50
|
+
def self.to_decimal(obj, default: 0)
|
51
|
+
case obj
|
52
|
+
when String
|
53
|
+
obj.match?(/\A-?\d+(?:[eE]\+?\d+)?\Z/) ? obj.to_f.to_i : BigDecimal(obj)
|
54
|
+
when Float
|
55
|
+
BigDecimal(obj.to_s)
|
56
|
+
when Integer, BigDecimal, Numeric
|
57
|
+
obj
|
58
|
+
else
|
59
|
+
default
|
60
|
+
end
|
61
|
+
rescue ArgumentError
|
62
|
+
default
|
63
|
+
end
|
64
|
+
|
65
|
+
# Cast _obj_ to a date and time. Return `nil` if casting fails.
|
66
|
+
#
|
67
|
+
# TODO: This was copied from Shopify/liquid. Include their license and copyright.
|
68
|
+
def self.to_date(obj)
|
69
|
+
return obj if obj.respond_to?(:strftime)
|
70
|
+
|
71
|
+
if obj.is_a?(String)
|
72
|
+
return nil if obj.empty?
|
73
|
+
|
74
|
+
obj = obj.downcase
|
75
|
+
end
|
76
|
+
|
77
|
+
case obj
|
78
|
+
when "now", "today"
|
79
|
+
Time.now
|
80
|
+
when /\A\d+\z/, Integer
|
81
|
+
Time.at(obj.to_i)
|
82
|
+
when String
|
83
|
+
Time.parse(obj)
|
84
|
+
end
|
85
|
+
rescue ::ArgumentError
|
86
|
+
nil
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.fetch(obj, key, default = nil)
|
90
|
+
obj[key]
|
91
|
+
rescue ArgumentError, TypeError, NoMethodError
|
92
|
+
default
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Liquid2
|
4
|
+
# Liquid filters and helper methods.
|
5
|
+
module Filters
|
6
|
+
# Return the concatenation of items in _left_ separated by _sep_.
|
7
|
+
# Coerce items in _left_ to strings if they aren't strings already.
|
8
|
+
def self.join(left, sep = " ")
|
9
|
+
sep = Liquid2.to_s(sep)
|
10
|
+
to_enumerable(left).map { |item| Liquid2.to_s(item) }.join(Liquid2.to_s(sep))
|
11
|
+
end
|
12
|
+
|
13
|
+
# Return a copy of _left_ with nil items removed.
|
14
|
+
# Coerce _left_ to an array-like object if it is not one already.
|
15
|
+
#
|
16
|
+
# If _key_ is given, assume items in _left_ are hash-like and remove items from _left_
|
17
|
+
# where `item.fetch(key, nil)` is nil.
|
18
|
+
#
|
19
|
+
# If key is not `:undefined`, coerce it to a string before calling `fetch` on items in
|
20
|
+
# _left_.
|
21
|
+
def self.compact(left, key = :undefined, context:)
|
22
|
+
left = Liquid2::Filters.to_enumerable(left)
|
23
|
+
|
24
|
+
case key
|
25
|
+
when Liquid2::Lambda
|
26
|
+
key.map(context, left).zip(left).reject do |r, _i|
|
27
|
+
r.nil? || r.is_a?(Liquid2::Undefined)
|
28
|
+
end.map(&:last)
|
29
|
+
when :undefined
|
30
|
+
left.compact
|
31
|
+
else
|
32
|
+
# TODO: stringify key?
|
33
|
+
left.reject do |item|
|
34
|
+
item.respond_to?(:fetch) ? item.fetch(key, nil).nil? : true
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Return _left_ concatenated with _right_, or nil if _right_ is not an array.
|
40
|
+
# Coerce _left_ to an array if it isn't an array already.
|
41
|
+
def self.concat(left, right)
|
42
|
+
unless right.respond_to?(:to_ary)
|
43
|
+
raise Liquid2::LiquidArgumentError.new("expected an array", nil)
|
44
|
+
end
|
45
|
+
|
46
|
+
Filters.to_enumerable(left).to_a.concat(right)
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.find(left, key, value = nil, context:)
|
50
|
+
left = Liquid2::Filters.to_enumerable(left)
|
51
|
+
|
52
|
+
if key.is_a?(Liquid2::Lambda)
|
53
|
+
key.map(context, left).zip(left).reject do |r, i|
|
54
|
+
return i unless r.is_a?(Liquid2::Undefined) || !Liquid2.truthy?(context, r)
|
55
|
+
end
|
56
|
+
elsif !value.nil? && !Liquid2.undefined?(value)
|
57
|
+
left.each do |item|
|
58
|
+
return item if fetch(item, key) == value
|
59
|
+
end
|
60
|
+
else
|
61
|
+
left.each do |item|
|
62
|
+
return item if fetch(item, key)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.find_index(left, key, value = nil, context:)
|
70
|
+
left = Liquid2::Filters.to_enumerable(left)
|
71
|
+
|
72
|
+
if key.is_a?(Liquid2::Lambda)
|
73
|
+
key.map(context, left).reject.with_index do |r, index|
|
74
|
+
return index unless r.is_a?(Liquid2::Undefined) || !Liquid2.truthy?(context, r)
|
75
|
+
end
|
76
|
+
elsif !value.nil? && !Liquid2.undefined?(value)
|
77
|
+
left.each_with_index do |item, index|
|
78
|
+
return index if fetch(item, key) == value
|
79
|
+
end
|
80
|
+
else
|
81
|
+
left.each_with_index do |item, index|
|
82
|
+
return index if fetch(item, key)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
nil
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.has(left, key, value = nil, context:)
|
90
|
+
left = Liquid2::Filters.to_enumerable(left)
|
91
|
+
|
92
|
+
if key.is_a?(Liquid2::Lambda)
|
93
|
+
key.map(context, left).reject do |r|
|
94
|
+
return true unless r.is_a?(Liquid2::Undefined) || !Liquid2.truthy?(context, r)
|
95
|
+
end
|
96
|
+
elsif !value.nil? && !Liquid2.undefined?(value)
|
97
|
+
left.each do |item|
|
98
|
+
return true if fetch(item, key) == value
|
99
|
+
end
|
100
|
+
else
|
101
|
+
left.each do |item|
|
102
|
+
return true if fetch(item, key)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
false
|
107
|
+
end
|
108
|
+
|
109
|
+
# Return the first item in _left_, or `nil` if _left_ does not have a first item.
|
110
|
+
def self.first(left)
|
111
|
+
case left
|
112
|
+
when String
|
113
|
+
left[0]
|
114
|
+
else
|
115
|
+
left.first if left.respond_to?(:first)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Return the last item in _left_, or `nil` if _left_ does not have a last item.
|
120
|
+
def self.last(left)
|
121
|
+
case left
|
122
|
+
when String
|
123
|
+
left[-1]
|
124
|
+
else
|
125
|
+
left.last if left.respond_to?(:last)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def self.map(left, key, context:)
|
130
|
+
left = Liquid2::Filters.to_enumerable(left)
|
131
|
+
|
132
|
+
if key.is_a?(Liquid2::Lambda)
|
133
|
+
key.map(context, left).map do |item|
|
134
|
+
item.is_a?(Liquid2::Undefined) ? nil : item
|
135
|
+
end
|
136
|
+
else
|
137
|
+
key = Liquid2.to_s(key)
|
138
|
+
left.map { |item| item[key] }
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Return _left_ with all items in reverse order.
|
143
|
+
# Coerce _left_ to an array if it isn't an array already.
|
144
|
+
def self.reverse(left)
|
145
|
+
to_enumerable(left).to_a.reverse
|
146
|
+
end
|
147
|
+
|
148
|
+
def self.reject(left, key, value = nil, context:)
|
149
|
+
left = Liquid2::Filters.to_enumerable(left)
|
150
|
+
|
151
|
+
if key.is_a?(Liquid2::Lambda)
|
152
|
+
key.map(context, left).zip(left).reject do |r, _item|
|
153
|
+
r.is_a?(Liquid2::Undefined) || Liquid2.truthy?(context, r)
|
154
|
+
end.map(&:last)
|
155
|
+
elsif !value.nil? && !Liquid2.undefined?(value)
|
156
|
+
key = Liquid2.to_s(key)
|
157
|
+
left.reject do |item|
|
158
|
+
fetch(item, key) == value
|
159
|
+
end
|
160
|
+
else
|
161
|
+
key = Liquid2.to_s(key)
|
162
|
+
left.reject do |item|
|
163
|
+
Liquid2.truthy?(context, fetch(item, key))
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def self.where(left, key, value = nil, context:)
|
169
|
+
left = Liquid2::Filters.to_enumerable(left)
|
170
|
+
|
171
|
+
if key.is_a?(Liquid2::Lambda)
|
172
|
+
key.map(context, left).zip(left).filter do |r, _item|
|
173
|
+
r.is_a?(Liquid2::Undefined) || Liquid2.truthy?(context, r)
|
174
|
+
end.map(&:last)
|
175
|
+
elsif !value.nil? && !Liquid2.undefined?(value)
|
176
|
+
key = Liquid2.to_s(key)
|
177
|
+
left.filter do |item|
|
178
|
+
fetch(item, key) == value
|
179
|
+
end
|
180
|
+
else
|
181
|
+
key = Liquid2.to_s(key)
|
182
|
+
left.filter do |item|
|
183
|
+
Liquid2.truthy?(context, fetch(item, key))
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Deduplicate items in _left_.
|
189
|
+
# Coerce _left_ to an array if it isn't an array already.
|
190
|
+
def self.uniq(left, key = nil, context:)
|
191
|
+
left = Liquid2::Filters.to_enumerable(left)
|
192
|
+
|
193
|
+
if key.nil?
|
194
|
+
left.to_a.uniq
|
195
|
+
elsif key.is_a?(Liquid2::Lambda)
|
196
|
+
key.map(context, left).zip(left).uniq { |r, _item| r }.map(&:last)
|
197
|
+
else
|
198
|
+
left.to_a.uniq { |item| fetch(item, key) }
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Liquid2
|
4
|
+
# Liquid filters and helper methods.
|
5
|
+
module Filters
|
6
|
+
# Format date and time object _left_ with _format_.
|
7
|
+
# Coerce _left_ to a `Time` if it is not a time-like object already.
|
8
|
+
# Coerce _format_ to a string if it is not a string already.
|
9
|
+
def self.date(left, format)
|
10
|
+
format = Liquid2.to_s(format)
|
11
|
+
return left if format.empty?
|
12
|
+
|
13
|
+
if (date = Filters.to_date(left))
|
14
|
+
date.strftime(format)
|
15
|
+
else
|
16
|
+
left
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Liquid2
|
4
|
+
# Liquid filters and helper methods.
|
5
|
+
module Filters
|
6
|
+
# Return _left_, or _default_ if _obj_ is `nil`, `false` or empty.
|
7
|
+
# If _allow_false_ is `true`, _left_ is returned if _left_ is `false`.
|
8
|
+
def self.default(left, default = "", context:, allow_false: false)
|
9
|
+
return default if left.respond_to?(:force_default) && left.force_default
|
10
|
+
|
11
|
+
obj = left.respond_to?(:to_liquid) ? left.to_liquid(context) : left
|
12
|
+
falsey = allow_false ? left.nil? : !obj
|
13
|
+
falsey || (left.respond_to?(:empty?) && left.empty?) ? default : left
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Liquid2
|
4
|
+
# Liquid filters and helper methods.
|
5
|
+
module Filters
|
6
|
+
# Return _left_ serialized in JSON format.
|
7
|
+
def self.json(left, pretty: false)
|
8
|
+
if pretty
|
9
|
+
JSON.pretty_generate(left)
|
10
|
+
else
|
11
|
+
JSON.generate(left)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Liquid2
|
4
|
+
# Liquid filters and helper methods.
|
5
|
+
module Filters
|
6
|
+
# Return the absolute value of _left_.
|
7
|
+
def self.abs(left)
|
8
|
+
to_number(left).abs
|
9
|
+
end
|
10
|
+
|
11
|
+
# Return the maximum of _left_ and _right_.
|
12
|
+
def self.at_least(left, right)
|
13
|
+
[to_number(left), to_number(right)].max
|
14
|
+
end
|
15
|
+
|
16
|
+
# Return the minimum of _left_ and _right_.
|
17
|
+
def self.at_most(left, right)
|
18
|
+
[to_number(left), to_number(right)].min
|
19
|
+
end
|
20
|
+
|
21
|
+
# Return _left_ rounded up to the next whole number.
|
22
|
+
def self.ceil(left)
|
23
|
+
to_number(left).ceil
|
24
|
+
end
|
25
|
+
|
26
|
+
# Return the result of dividing _left_ by _right_.
|
27
|
+
# If both _left_ and _right_ are integers, integer division is performed.
|
28
|
+
def self.divided_by(left, right)
|
29
|
+
to_decimal(left) / to_decimal(right) # steep:ignore
|
30
|
+
rescue ZeroDivisionError => e
|
31
|
+
raise LiquidTypeError.new(e.message, nil)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Return the result of multiplying _left_ by _right_.
|
35
|
+
def self.times(left, right)
|
36
|
+
to_decimal(left) * to_decimal(right) # steep:ignore
|
37
|
+
end
|
38
|
+
|
39
|
+
# Return _left_ rounded down to the next whole number.
|
40
|
+
def self.floor(left)
|
41
|
+
to_number(left).floor
|
42
|
+
end
|
43
|
+
|
44
|
+
# Return _right_ subtracted from _left_.
|
45
|
+
def self.minus(left, right)
|
46
|
+
to_decimal(left) - to_decimal(right) # steep:ignore
|
47
|
+
end
|
48
|
+
|
49
|
+
# Return the remainder of dividing _left_ by _right_.
|
50
|
+
def self.modulo(left, right)
|
51
|
+
to_decimal(left) % to_decimal(right) # steep:ignore
|
52
|
+
rescue ZeroDivisionError => e
|
53
|
+
raise LiquidTypeError.new(e.message, nil)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Return _right_ added to _left_.
|
57
|
+
def self.plus(left, right)
|
58
|
+
to_decimal(left) + to_decimal(right) # steep:ignore
|
59
|
+
end
|
60
|
+
|
61
|
+
# Return _left_ rounded to _ndigits_ decimal digits.
|
62
|
+
def self.round(left, ndigits = 0)
|
63
|
+
left = to_decimal(left)
|
64
|
+
return left.round if ndigits == 0 # rubocop:disable Style/NumericPredicate
|
65
|
+
|
66
|
+
left.round(to_decimal(ndigits)) # steep:ignore
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.sum(left, key = nil, context:)
|
70
|
+
left = Liquid2::Filters.to_enumerable(left)
|
71
|
+
|
72
|
+
case key
|
73
|
+
when Liquid2::Lambda
|
74
|
+
items = key.map(context, left).reject do |item|
|
75
|
+
Liquid2.undefined?(item)
|
76
|
+
end
|
77
|
+
|
78
|
+
items.sum { |item| Liquid2::Filters.to_decimal(item) }
|
79
|
+
when nil, Liquid2::Undefined
|
80
|
+
left.sum { |item| Liquid2::Filters.to_decimal(item) } # steep:ignore
|
81
|
+
else
|
82
|
+
k = Liquid2.to_s(key)
|
83
|
+
left.sum { |item| Liquid2::Filters.to_decimal(fetch(item, k)) } # steep:ignore
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Liquid2
|
4
|
+
# Liquid filters and helper methods.
|
5
|
+
module Filters
|
6
|
+
# Return the subsequence of _left_ starting at _start_ up to _length_.
|
7
|
+
def self.slice(left, start, length = 1)
|
8
|
+
length = 1 if Liquid2.undefined?(length)
|
9
|
+
case left
|
10
|
+
when Array
|
11
|
+
left.slice(to_integer(start), to_integer(length)) || []
|
12
|
+
else
|
13
|
+
Liquid2.to_s(left).slice(to_integer(start), to_integer(length)) || ""
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Liquid2
|
4
|
+
# Liquid filters and helper methods.
|
5
|
+
module Filters
|
6
|
+
def self.sort(left, key = nil, context:)
|
7
|
+
left = Liquid2::Filters.to_enumerable(left)
|
8
|
+
|
9
|
+
case key
|
10
|
+
when Liquid2::Lambda
|
11
|
+
key.map(context, left).zip(left).sort do |a, b|
|
12
|
+
nil_safe_compare(a.first, b.first)
|
13
|
+
end.map(&:last)
|
14
|
+
when nil, Liquid2::Undefined
|
15
|
+
left.sort { |a, b| nil_safe_compare(a, b) }
|
16
|
+
else
|
17
|
+
key = Liquid2.to_s(key)
|
18
|
+
left.sort { |a, b| nil_safe_compare(fetch(a, key), fetch(b, key)) }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.sort_natural(left, key = nil, context:)
|
23
|
+
left = Liquid2::Filters.to_enumerable(left)
|
24
|
+
|
25
|
+
case key
|
26
|
+
when Liquid2::Lambda
|
27
|
+
key.map(context, left).zip(left).sort do |a, b|
|
28
|
+
nil_safe_casecmp(a.first, b.first)
|
29
|
+
end.map(&:last)
|
30
|
+
when nil, Liquid2::Undefined
|
31
|
+
left.sort { |a, b| nil_safe_casecmp(a, b) }
|
32
|
+
else
|
33
|
+
key = Liquid2.to_s(key)
|
34
|
+
left.sort { |a, b| nil_safe_casecmp(fetch(a, key), fetch(b, key)) }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.sort_numeric(left, key = nil, context:)
|
39
|
+
left = Liquid2::Filters.to_enumerable(left)
|
40
|
+
|
41
|
+
case key
|
42
|
+
when Liquid2::Lambda
|
43
|
+
key.map(context, left).zip(left).sort do |a, b|
|
44
|
+
numeric_compare(a.first, b.first)
|
45
|
+
end.map(&:last)
|
46
|
+
when nil, Liquid2::Undefined
|
47
|
+
left.sort { |a, b| numeric_compare(a, b) }
|
48
|
+
else
|
49
|
+
key = Liquid2.to_s(key)
|
50
|
+
left.sort { |a, b| numeric_compare(fetch(a, key), fetch(b, key)) }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.nil_safe_compare(left, right)
|
55
|
+
result = left <=> right
|
56
|
+
|
57
|
+
if result
|
58
|
+
result
|
59
|
+
elsif left.nil?
|
60
|
+
1
|
61
|
+
elsif right.nil?
|
62
|
+
-1
|
63
|
+
else
|
64
|
+
raise Liquid2::LiquidArgumentError.new("can't sort incomparable type", nil)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.nil_safe_casecmp(left, right)
|
69
|
+
if !left.nil? && !right.nil?
|
70
|
+
left.to_s.casecmp(right.to_s)
|
71
|
+
elsif left.nil? && right.nil?
|
72
|
+
0
|
73
|
+
else
|
74
|
+
left.nil? ? 1 : -1
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.numeric_compare(left, right)
|
79
|
+
# @type var res: untyped
|
80
|
+
res = ints(left) <=> ints(right)
|
81
|
+
res || -1
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.ints(obj)
|
85
|
+
case obj
|
86
|
+
when Integer, Float, BigDecimal
|
87
|
+
[obj]
|
88
|
+
else
|
89
|
+
numeric = obj.to_s.scan(/-?\d+/)
|
90
|
+
return [Float::INFINITY] if numeric.empty?
|
91
|
+
|
92
|
+
numeric.map(&:to_i)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|