luoma 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 +2 -0
- data/.rdoc_options +16 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +68 -0
- data/Rakefile +69 -0
- data/Steepfile +41 -0
- data/certs/jgrp.pem +27 -0
- data/docs/configuration.md +180 -0
- data/docs/custom_filters.md +3 -0
- data/docs/custom_tags.md +3 -0
- data/docs/expressions.md +3 -0
- data/docs/extension_types.md +41 -0
- data/docs/filter_reference.md +1702 -0
- data/docs/index.md +73 -0
- data/docs/luoma_for_template_authors.md +3 -0
- data/docs/markdown.md +111 -0
- data/docs/predicate_reference.md +3 -0
- data/docs/static_analysis.md +3 -0
- data/docs/tag_reference.md +352 -0
- data/docs/template_loaders.md +177 -0
- data/docs/undefined_variables.md +3 -0
- data/docs-requirements.txt +11 -0
- data/docs_/cycle.md +17 -0
- data/docs_/font_rendering_example.md +157 -0
- data/docs_/header_block_example.md +51 -0
- data/docs_/increment_and_decrement.md +49 -0
- data/docs_/logo_style_example.md +40 -0
- data/docs_/migration.md +11 -0
- data/docs_/notes.md +19 -0
- data/lib/luoma/cache.rb +80 -0
- data/lib/luoma/chain_hash.rb +54 -0
- data/lib/luoma/context.rb +227 -0
- data/lib/luoma/drop.rb +84 -0
- data/lib/luoma/drops/block.rb +33 -0
- data/lib/luoma/drops/expression.rb +29 -0
- data/lib/luoma/drops/html_safe.rb +36 -0
- data/lib/luoma/drops/range.rb +69 -0
- data/lib/luoma/drops/undefined.rb +119 -0
- data/lib/luoma/environment.rb +479 -0
- data/lib/luoma/errors.rb +79 -0
- data/lib/luoma/escape.rb +35 -0
- data/lib/luoma/expression.rb +1271 -0
- data/lib/luoma/filter.rb +97 -0
- data/lib/luoma/filters/array.rb +372 -0
- data/lib/luoma/filters/date.rb +19 -0
- data/lib/luoma/filters/default.rb +16 -0
- data/lib/luoma/filters/json.rb +14 -0
- data/lib/luoma/filters/math.rb +84 -0
- data/lib/luoma/filters/size.rb +13 -0
- data/lib/luoma/filters/slice.rb +52 -0
- data/lib/luoma/filters/sort.rb +113 -0
- data/lib/luoma/filters/string.rb +186 -0
- data/lib/luoma/fnv.rb +15 -0
- data/lib/luoma/lexer.rb +106 -0
- data/lib/luoma/lexer_legacy.rb +396 -0
- data/lib/luoma/lexer_unified.rb +279 -0
- data/lib/luoma/loader.rb +50 -0
- data/lib/luoma/loaders/choice_loader.rb +20 -0
- data/lib/luoma/loaders/file_system_loader.rb +75 -0
- data/lib/luoma/loaders/mixins.rb +52 -0
- data/lib/luoma/markup.rb +109 -0
- data/lib/luoma/parser.rb +252 -0
- data/lib/luoma/parser_unified.rb +979 -0
- data/lib/luoma/predicates/blank.rb +19 -0
- data/lib/luoma/predicates/defined.rb +10 -0
- data/lib/luoma/predicates/empty.rb +15 -0
- data/lib/luoma/predicates/type.rb +35 -0
- data/lib/luoma/static_analysis.rb +427 -0
- data/lib/luoma/tags/assign.rb +63 -0
- data/lib/luoma/tags/break.rb +23 -0
- data/lib/luoma/tags/capture.rb +43 -0
- data/lib/luoma/tags/case.rb +137 -0
- data/lib/luoma/tags/comment.rb +17 -0
- data/lib/luoma/tags/continue.rb +23 -0
- data/lib/luoma/tags/define.rb +42 -0
- data/lib/luoma/tags/else.rb +30 -0
- data/lib/luoma/tags/for.rb +108 -0
- data/lib/luoma/tags/if.rb +109 -0
- data/lib/luoma/tags/import.rb +91 -0
- data/lib/luoma/tags/include.rb +77 -0
- data/lib/luoma/tags/output.rb +20 -0
- data/lib/luoma/tags/raw.rb +26 -0
- data/lib/luoma/tags/render.rb +92 -0
- data/lib/luoma/tags/with.rb +55 -0
- data/lib/luoma/template.rb +178 -0
- data/lib/luoma/token.rb +84 -0
- data/lib/luoma/version.rb +5 -0
- data/lib/luoma.rb +75 -0
- data/sig/luoma/cache.rbs +44 -0
- data/sig/luoma/chain_hash.rbs +26 -0
- data/sig/luoma/context.rbs +115 -0
- data/sig/luoma/drop.rbs +44 -0
- data/sig/luoma/drops/block.rbs +10 -0
- data/sig/luoma/drops/expression.rbs +10 -0
- data/sig/luoma/drops/html_safe.rbs +14 -0
- data/sig/luoma/drops/range.rbs +15 -0
- data/sig/luoma/drops/undefined.rbs +27 -0
- data/sig/luoma/environment.rbs +212 -0
- data/sig/luoma/errors.rbs +67 -0
- data/sig/luoma/escape.rbs +15 -0
- data/sig/luoma/expression.rbs +514 -0
- data/sig/luoma/filter.rbs +66 -0
- data/sig/luoma/filters/array.rbs +74 -0
- data/sig/luoma/filters/date.rbs +9 -0
- data/sig/luoma/filters/default.rbs +8 -0
- data/sig/luoma/filters/json.rbs +7 -0
- data/sig/luoma/filters/math.rbs +37 -0
- data/sig/luoma/filters/size.rbs +6 -0
- data/sig/luoma/filters/slice.rbs +10 -0
- data/sig/luoma/filters/sort.rbs +17 -0
- data/sig/luoma/filters/string.rbs +103 -0
- data/sig/luoma/fnv.rbs +3 -0
- data/sig/luoma/lexer.rbs +42 -0
- data/sig/luoma/lexer_legacy.rbs +81 -0
- data/sig/luoma/lexer_unified.rbs +49 -0
- data/sig/luoma/loader.rbs +40 -0
- data/sig/luoma/loaders/choice_loader.rbs +9 -0
- data/sig/luoma/loaders/file_system_loader.rbs +19 -0
- data/sig/luoma/loaders/mixins.rbs +16 -0
- data/sig/luoma/markup.rbs +68 -0
- data/sig/luoma/parser.rbs +105 -0
- data/sig/luoma/parser_unified.rbs +150 -0
- data/sig/luoma/predicates/blank.rbs +6 -0
- data/sig/luoma/predicates/defined.rbs +6 -0
- data/sig/luoma/predicates/empty.rbs +6 -0
- data/sig/luoma/predicates/type.rbs +21 -0
- data/sig/luoma/static_analysis.rbs +141 -0
- data/sig/luoma/tags/assign.rbs +12 -0
- data/sig/luoma/tags/break.rbs +11 -0
- data/sig/luoma/tags/capture.rbs +14 -0
- data/sig/luoma/tags/case.rbs +36 -0
- data/sig/luoma/tags/comment.rbs +11 -0
- data/sig/luoma/tags/continue.rbs +11 -0
- data/sig/luoma/tags/define.rbs +16 -0
- data/sig/luoma/tags/else.rbs +17 -0
- data/sig/luoma/tags/for.rbs +26 -0
- data/sig/luoma/tags/if.rbs +45 -0
- data/sig/luoma/tags/import.rbs +13 -0
- data/sig/luoma/tags/include.rbs +13 -0
- data/sig/luoma/tags/output.rbs +13 -0
- data/sig/luoma/tags/raw.rbs +11 -0
- data/sig/luoma/tags/render.rbs +13 -0
- data/sig/luoma/tags/with.rbs +15 -0
- data/sig/luoma/template.rbs +117 -0
- data/sig/luoma/token.rbs +81 -0
- data/sig/luoma.rbs +11 -0
- data/zensical.toml +363 -0
- data.tar.gz.sig +0 -0
- metadata +233 -0
- metadata.gz.sig +0 -0
data/lib/luoma/filter.rb
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Luoma
|
|
4
|
+
class FilterContext
|
|
5
|
+
attr_reader :token, :render_context, :env
|
|
6
|
+
|
|
7
|
+
#: (t_token, RenderContext)
|
|
8
|
+
def initialize(token, render_context)
|
|
9
|
+
@token = token
|
|
10
|
+
@render_context = render_context
|
|
11
|
+
@env = render_context.env
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# (String) -> TemplateTypeError
|
|
15
|
+
def type_error(message)
|
|
16
|
+
TemplateTypeError.new(
|
|
17
|
+
message, @token,
|
|
18
|
+
@render_context.template.source,
|
|
19
|
+
@render_context.template.name
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# (String) -> FilterArgumentError
|
|
24
|
+
def argument_error(message)
|
|
25
|
+
FilterArgumentError.new(
|
|
26
|
+
message, @token,
|
|
27
|
+
@render_context.template.source,
|
|
28
|
+
@render_context.template.name
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
#: (untyped) -> Array[untyped]
|
|
33
|
+
def to_a(obj)
|
|
34
|
+
@env.to_a(obj, @render_context)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
#: (untyped) -> Hash[untyped, untyped]
|
|
38
|
+
def to_h(obj)
|
|
39
|
+
@env.to_h(obj, @render_context)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
#: (untyped) -> Integer
|
|
43
|
+
def to_i(obj, default: :nothing)
|
|
44
|
+
@env.to_i(obj, @render_context, default: default)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
#: (untyped) -> String
|
|
48
|
+
def to_string(obj)
|
|
49
|
+
@env.to_string(obj, @render_context)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
#: [X] (untyped, ?default: X) -> (Numeric | X)
|
|
53
|
+
def to_numeric(obj, default: :nothing)
|
|
54
|
+
@env.to_numeric(obj, @render_context, default: default)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
#: (untyped) -> Enumerable[untyped]
|
|
58
|
+
def to_enumerable(obj)
|
|
59
|
+
@env.to_enumerable(obj, @render_context)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
#: (untyped) -> untyped
|
|
63
|
+
def to_date(obj)
|
|
64
|
+
@env.to_date(obj, @render_context)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
#: (untyped) -> bool
|
|
68
|
+
def truthy?(obj)
|
|
69
|
+
@env.truthy?(obj, @render_context)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
#: (untyped, untyped) -> bool
|
|
73
|
+
def eq?(obj, other)
|
|
74
|
+
@env.eq?(obj, other, @render_context, @token)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
#: (untyped, untyped) -> bool
|
|
78
|
+
def lt?(obj, other)
|
|
79
|
+
@env.lt?(obj, other, @render_context, @token)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
#: (untyped, untyped) -> (-1 | 1 | 0 | nil)
|
|
83
|
+
def cmp(left, right)
|
|
84
|
+
@env.cmp(left, right, @render_context, @token)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
#: (untyped) -> bool
|
|
88
|
+
def nothing?(obj)
|
|
89
|
+
@env.nothing?(obj)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
#: (untyped, untyped, ?default: untyped?) -> untyped
|
|
93
|
+
def fetch(obj, key, default: :nothing)
|
|
94
|
+
obj.respond_to?(:[]) && obj.respond_to?(:key?) && key.is_a?(String) && obj.key?(key) ? obj[key] : default
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Luoma
|
|
4
|
+
module Filters
|
|
5
|
+
# Return the concatenation of items in _left_ separated by _sep_.
|
|
6
|
+
# Coerce items in _left_ to strings if they aren't strings already.
|
|
7
|
+
def self.join(context, left, sep = " ")
|
|
8
|
+
context.to_enumerable(left).map { |item| context.to_string(item) }.join(context.to_string(sep))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# If _left_ is enumerable, return _left_ flattened to _depth_. Otherwise
|
|
12
|
+
# return _left_.
|
|
13
|
+
def self.flatten(context, left, depth = nil)
|
|
14
|
+
left = context.to_enumerable(left)
|
|
15
|
+
|
|
16
|
+
context.to_a(left).flatten(
|
|
17
|
+
depth.nil? ? depth : context.to_numeric(depth, default: 1).to_i # steep:ignore
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Return true if at least one item in _left_ is truthy.
|
|
22
|
+
def self.any(context, left, key = :nothing, value = :nothing)
|
|
23
|
+
left = context.to_enumerable(left)
|
|
24
|
+
|
|
25
|
+
case key
|
|
26
|
+
when :nothing
|
|
27
|
+
left.any? { |i| context.truthy?(i) }
|
|
28
|
+
when ExpressionDrop
|
|
29
|
+
key.expr.broadcast_with_index(left).any? { |i| context.truthy?(i) }
|
|
30
|
+
else
|
|
31
|
+
key = context.to_string(key)
|
|
32
|
+
if value == :nothing
|
|
33
|
+
left.map { |i| context.fetch(i, key) }.any? { |j| context.truthy?(j) }
|
|
34
|
+
else
|
|
35
|
+
left.map { |i| context.fetch(i, key) }.any? { |j| context.eq?(j, value) }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Return true if all items in _left_ are truthy.
|
|
41
|
+
def self.all(context, left, key = :nothing, value = :nothing)
|
|
42
|
+
left = context.to_enumerable(left)
|
|
43
|
+
|
|
44
|
+
case key
|
|
45
|
+
when :nothing
|
|
46
|
+
left.all? { |i| context.truthy?(i) }
|
|
47
|
+
when ExpressionDrop
|
|
48
|
+
key.expr.broadcast_with_index(left).all? { |i| context.truthy?(i) }
|
|
49
|
+
else
|
|
50
|
+
key = context.to_string(key)
|
|
51
|
+
if value == :nothing
|
|
52
|
+
left.map { |i| context.fetch(i, key) }.all? { |j| context.truthy?(j) }
|
|
53
|
+
else
|
|
54
|
+
left.map { |i| context.fetch(i, key) }.all? { |j| context.eq?(j, value) }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Return a copy of _left_ with nil items removed.
|
|
60
|
+
# Coerce _left_ to an array-like object if it is not one already.
|
|
61
|
+
#
|
|
62
|
+
# If _key_ is given, assume items in _left_ are hash-like and remove items from _left_
|
|
63
|
+
# where `item.fetch(key, nil)` is nil.
|
|
64
|
+
#
|
|
65
|
+
# If key is not `:nothing`, coerce it to a string before calling `fetch` on items in
|
|
66
|
+
# _left_.
|
|
67
|
+
def self.compact(context, left, key = :nothing)
|
|
68
|
+
left = context.to_enumerable(left)
|
|
69
|
+
|
|
70
|
+
case key
|
|
71
|
+
when :nothing
|
|
72
|
+
left.reject { |value| value.nil? || context.nothing?(value) }
|
|
73
|
+
when ExpressionDrop
|
|
74
|
+
left.each_with_index.reject do |item, index|
|
|
75
|
+
value = key.expr.call_with_index(item, index)
|
|
76
|
+
value.nil? || context.nothing?(value)
|
|
77
|
+
end.map(&:first)
|
|
78
|
+
else
|
|
79
|
+
left.reject do |item|
|
|
80
|
+
value = context.fetch(item, key)
|
|
81
|
+
value.nil? || context.nothing?(value)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Return _left_ concatenated with _right_.
|
|
87
|
+
# Coerce _left_ and _right_ to an array if they aren't arrays already.
|
|
88
|
+
def self.concat(context, left, right)
|
|
89
|
+
context.to_a(left).concat(context.to_a(right))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Return the first item in _left_ where _key_ is equal to _value_, or nil
|
|
93
|
+
# if no such item exists.
|
|
94
|
+
#
|
|
95
|
+
# If _value_ is not given, return the first item where _key_ is truthy.
|
|
96
|
+
#
|
|
97
|
+
# If _key_ is a Lambda expression, evaluate the expression for each item in
|
|
98
|
+
# _left_ and return the first item where the expression results in a truthy
|
|
99
|
+
# value, ignoring _value_.
|
|
100
|
+
def self.find(context, left, key, value = :nothing)
|
|
101
|
+
left = context.to_enumerable(left)
|
|
102
|
+
|
|
103
|
+
if key.is_a?(ExpressionDrop)
|
|
104
|
+
key.expr.broadcast_with_index(left).zip(left) do |r, i|
|
|
105
|
+
return i if context.truthy?(r)
|
|
106
|
+
end
|
|
107
|
+
elsif value == :nothing
|
|
108
|
+
key_ = context.to_string(key)
|
|
109
|
+
left.each do |item|
|
|
110
|
+
return item if context.truthy?(context.fetch(item, key_)) || context.eq?(item, key)
|
|
111
|
+
end
|
|
112
|
+
else
|
|
113
|
+
key = context.to_string(key)
|
|
114
|
+
left.each do |item|
|
|
115
|
+
return item if context.eq?(context.fetch(item, key), value) || context.eq?(item, value)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Return the index of the first item in _left_ where _key_ is equal to
|
|
123
|
+
# _value_, or nil if no such item exists.
|
|
124
|
+
#
|
|
125
|
+
# If _value_ is not given, return the index of the first item where _key_
|
|
126
|
+
# is truthy.
|
|
127
|
+
#
|
|
128
|
+
# If _key_ is a Lambda expression, evaluate the expression for each item in
|
|
129
|
+
# _left_ and return the index of the first item where the expression
|
|
130
|
+
# results in a truthy value, ignoring _value_.
|
|
131
|
+
def self.find_index(context, left, key, value = :nothing)
|
|
132
|
+
left = context.to_enumerable(left)
|
|
133
|
+
|
|
134
|
+
if key.is_a?(ExpressionDrop)
|
|
135
|
+
key.expr.broadcast_with_index(left).each_with_index do |item, index|
|
|
136
|
+
return index if context.truthy?(item)
|
|
137
|
+
end
|
|
138
|
+
elsif value == :nothing
|
|
139
|
+
key_ = context.to_string(key)
|
|
140
|
+
left.each_with_index do |item, index|
|
|
141
|
+
return index if context.truthy?(context.fetch(item, key_)) || context.eq?(item, key)
|
|
142
|
+
end
|
|
143
|
+
else
|
|
144
|
+
key = context.to_string(key)
|
|
145
|
+
left.each_with_index do |item, index|
|
|
146
|
+
return index if context.eq?(context.fetch(item, key), value) || context.eq?(item, value)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
nil
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Return the first item in _left_, or `:nothing` if _left_ does not have a
|
|
154
|
+
# first item.
|
|
155
|
+
#
|
|
156
|
+
# Coerce _left_ to an array if it is not a string, hash or array.
|
|
157
|
+
def self.first(context, left)
|
|
158
|
+
case left
|
|
159
|
+
when String
|
|
160
|
+
left.empty? ? :nothing : left[0]
|
|
161
|
+
when Hash, Array
|
|
162
|
+
left.empty? ? :nothing : left.first
|
|
163
|
+
else
|
|
164
|
+
left_ = context.to_a(left)
|
|
165
|
+
left_.empty? ? :nothing : left_.first
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Return the last item in _left_, or `:nothing` if _left_ does not have a
|
|
170
|
+
# last item.
|
|
171
|
+
#
|
|
172
|
+
# Coerce _left_ to an array if it is not a string or array.
|
|
173
|
+
def self.last(context, left)
|
|
174
|
+
case left
|
|
175
|
+
when String
|
|
176
|
+
left.empty? ? :nothing : left[-1]
|
|
177
|
+
when Array
|
|
178
|
+
left.empty? ? :nothing : left.last
|
|
179
|
+
else
|
|
180
|
+
left_ = context.to_a(left)
|
|
181
|
+
left_.empty? ? :nothing : left_.last
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def self.map(context, left, key)
|
|
186
|
+
left = context.to_enumerable(left)
|
|
187
|
+
|
|
188
|
+
if key.is_a?(ExpressionDrop)
|
|
189
|
+
key.expr.broadcast_with_index(left)
|
|
190
|
+
else
|
|
191
|
+
key = context.to_string(key)
|
|
192
|
+
left.map { |item| context.fetch(item, key) }
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def self.flat_map(context, left, key)
|
|
197
|
+
left = context.to_enumerable(left)
|
|
198
|
+
|
|
199
|
+
if key.is_a?(ExpressionDrop)
|
|
200
|
+
key.expr.broadcast_with_index(left).flatten!(1)
|
|
201
|
+
else
|
|
202
|
+
key = context.to_string(key)
|
|
203
|
+
left.flat_map { |item| context.fetch(item, key) }
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Return _left_ with all items in reverse order.
|
|
208
|
+
# Coerce _left_ to an array if it isn't an array already.
|
|
209
|
+
def self.reverse(context, left)
|
|
210
|
+
context.to_a(left).reverse
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def self.reject(context, left, key, value = :nothing)
|
|
214
|
+
left = context.to_enumerable(left)
|
|
215
|
+
|
|
216
|
+
if key.is_a?(ExpressionDrop)
|
|
217
|
+
left.each_with_index.reject do |item, index|
|
|
218
|
+
context.truthy?(key.expr.call_with_index(item, index))
|
|
219
|
+
end.map(&:first)
|
|
220
|
+
elsif value == :nothing
|
|
221
|
+
key_ = context.to_string(key)
|
|
222
|
+
left.reject do |item|
|
|
223
|
+
context.truthy?(context.fetch(item, key_)) || context.eq?(item, key)
|
|
224
|
+
end
|
|
225
|
+
else
|
|
226
|
+
key = context.to_string(key)
|
|
227
|
+
left.reject do |item|
|
|
228
|
+
context.eq?(context.fetch(item, key), value) || context.eq?(item, value)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def self.where(context, left, key, value = :nothing)
|
|
234
|
+
left = context.to_enumerable(left)
|
|
235
|
+
|
|
236
|
+
if key.is_a?(ExpressionDrop)
|
|
237
|
+
left.each_with_index.filter do |item, index|
|
|
238
|
+
context.truthy?(key.expr.call_with_index(item, index))
|
|
239
|
+
end.map(&:first)
|
|
240
|
+
elsif value == :nothing
|
|
241
|
+
key_ = context.to_string(key)
|
|
242
|
+
left.filter do |item|
|
|
243
|
+
context.truthy?(context.fetch(item, key_)) || context.eq?(item, key)
|
|
244
|
+
end
|
|
245
|
+
else
|
|
246
|
+
key = context.to_string(key)
|
|
247
|
+
left.filter do |item|
|
|
248
|
+
context.eq?(context.fetch(item, key), value) || context.eq?(item, value)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Deduplicate items in _left_.
|
|
254
|
+
# Coerce _left_ to an array if it isn't an array already.
|
|
255
|
+
def self.uniq(context, left, key = :nothing)
|
|
256
|
+
left = context.to_a(left)
|
|
257
|
+
|
|
258
|
+
# Like Array#uniq but using our own comparator, context#eq?.
|
|
259
|
+
# @type var uniq_: ^(Array[untyped]) ?{ (untyped, Integer) -> untyped } -> Array[untyped]
|
|
260
|
+
uniq_ = lambda do |array, &key|
|
|
261
|
+
decorated = [] #: Array[untyped]
|
|
262
|
+
result = [] #: Array[untyped]
|
|
263
|
+
|
|
264
|
+
array.each_with_index do |item, index|
|
|
265
|
+
value = key ? key.call(item, index) : item
|
|
266
|
+
|
|
267
|
+
unless decorated.any? { |existing| context.eq?(existing, value) }
|
|
268
|
+
decorated << value
|
|
269
|
+
result << item
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
result
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
if key == :nothing
|
|
277
|
+
uniq_.call(left)
|
|
278
|
+
elsif key.is_a?(ExpressionDrop)
|
|
279
|
+
uniq_.call(left) { |item, index| key.expr.call_with_index(item, index) }
|
|
280
|
+
else
|
|
281
|
+
key = context.to_string(key)
|
|
282
|
+
uniq_.call(left) { |item, _index| context.fetch(item, key) }
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Return the sum of all numeric values in the input array.
|
|
287
|
+
def self.sum(context, left, key = :nothing)
|
|
288
|
+
left = context.to_enumerable(left)
|
|
289
|
+
|
|
290
|
+
if key == :nothing
|
|
291
|
+
left.sum { |v| context.to_numeric(v, default: 0) }
|
|
292
|
+
elsif key.is_a?(ExpressionDrop)
|
|
293
|
+
key.expr.broadcast_with_index(left).sum { |v| context.to_numeric(v, default: 0) }
|
|
294
|
+
else
|
|
295
|
+
key = context.to_string(key)
|
|
296
|
+
left.sum { |v| context.to_numeric(context.fetch(v, key, default: 0)) }
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Combine _left_ with _right_, producing an array or pairs.
|
|
301
|
+
# Coerce _left_ and _right_ to arrays if they aren't arrays already.
|
|
302
|
+
def self.zip(context, left, right)
|
|
303
|
+
context.to_a(left).zip(context.to_a(right))
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Return the minimum numeric value found in _left_.
|
|
307
|
+
# Coerce _left_ to an array if it's not an array already.
|
|
308
|
+
def self.min(context, left, key = :nothing)
|
|
309
|
+
left = context.to_enumerable(left)
|
|
310
|
+
|
|
311
|
+
# @type var min_: ^(Enumerable[untyped]) ?{ (untyped, Integer) -> untyped } -> untyped
|
|
312
|
+
min_ = lambda do |enum, &key|
|
|
313
|
+
best_item = nil #:untyped
|
|
314
|
+
best_value = nil #: Numeric?
|
|
315
|
+
|
|
316
|
+
enum.each_with_index do |item, index|
|
|
317
|
+
value = context.to_numeric(key ? key.call(item, index) : item, default: nil)
|
|
318
|
+
next if value.nil?
|
|
319
|
+
|
|
320
|
+
if best_value.nil? || context.lt?(value, best_value)
|
|
321
|
+
best_item = item
|
|
322
|
+
best_value = value
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
best_item
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
if key == :nothing
|
|
330
|
+
min_.call(left)
|
|
331
|
+
elsif key.is_a?(ExpressionDrop)
|
|
332
|
+
min_.call(left) { |item, index| key.expr.call_with_index(item, index) }
|
|
333
|
+
else
|
|
334
|
+
key = context.to_string(key)
|
|
335
|
+
min_.call(left) { |item, _index| context.fetch(item, key, default: nil) }
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Return the maximum numeric value found in _left_.
|
|
340
|
+
# Coerce _left_ to an array if it's not an array already.
|
|
341
|
+
def self.max(context, left, key = :nothing)
|
|
342
|
+
left = context.to_enumerable(left)
|
|
343
|
+
|
|
344
|
+
# @type var max_: ^(Enumerable[untyped]) ?{ (Integer, untyped) -> untyped } -> untyped
|
|
345
|
+
max_ = lambda do |enum, &key|
|
|
346
|
+
best_item = nil # untyped
|
|
347
|
+
best_value = nil #: Numeric?
|
|
348
|
+
|
|
349
|
+
enum.each_with_index do |item, index|
|
|
350
|
+
value = context.to_numeric(key ? key.call(item, index) : item, default: nil)
|
|
351
|
+
next if value.nil?
|
|
352
|
+
|
|
353
|
+
if best_value.nil? || context.lt?(best_value, value)
|
|
354
|
+
best_item = item
|
|
355
|
+
best_value = value
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
best_item
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
if key == :nothing
|
|
363
|
+
max_.call(left)
|
|
364
|
+
elsif key.is_a?(ExpressionDrop)
|
|
365
|
+
max_.call(left) { |item, index| key.expr.call_with_index(item, index) }
|
|
366
|
+
else
|
|
367
|
+
key = context.to_string(key)
|
|
368
|
+
max_.call(left) { |item, _index| context.fetch(item, key, default: nil) }
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Luoma
|
|
4
|
+
module Filters
|
|
5
|
+
# Format date and time object _left_ with _format_.
|
|
6
|
+
# Coerce _left_ to a `Time` if it is not a time-like object already.
|
|
7
|
+
# Coerce _format_ to a string if it is not a string already.
|
|
8
|
+
def self.date(context, left, format)
|
|
9
|
+
format = context.to_string(format)
|
|
10
|
+
return left if format.empty?
|
|
11
|
+
|
|
12
|
+
if (date = context.to_date(left))
|
|
13
|
+
date.strftime(format)
|
|
14
|
+
else
|
|
15
|
+
left
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Luoma
|
|
4
|
+
module Filters
|
|
5
|
+
# Return _left_, or _default_ if _obj_ is `nil`, `false` or empty.
|
|
6
|
+
# If _allow_false_ is `true`, _left_ is returned if _left_ is `false`.
|
|
7
|
+
def self.default(context, left, default = "", allow_false: false)
|
|
8
|
+
return default if left.is_a?(FalsyStrictUndefinedDrop)
|
|
9
|
+
|
|
10
|
+
left_ = left.is_a?(Drop) ? left.to_primitive(:boolean, context.render_context) : left
|
|
11
|
+
return left_ if allow_false && left_ == false
|
|
12
|
+
|
|
13
|
+
!context.truthy?(left_) || Luoma::Predicates.empty?(context.render_context, left_) ? default : left_
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Luoma
|
|
4
|
+
module Filters
|
|
5
|
+
# Return _left_ serialized in JSON format.
|
|
6
|
+
def self.json(context, left, pretty: false)
|
|
7
|
+
if pretty
|
|
8
|
+
JSON.pretty_generate(left)
|
|
9
|
+
else
|
|
10
|
+
JSON.generate(left)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Luoma
|
|
4
|
+
module Filters
|
|
5
|
+
# Return the absolute value of `left`.
|
|
6
|
+
def self.abs(context, left)
|
|
7
|
+
left_ = context.to_numeric(left)
|
|
8
|
+
context.nothing?(left_) ? left_ : left_.abs
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Return the maximum of `left` and `right`.
|
|
12
|
+
def self.at_least(context, left, right)
|
|
13
|
+
[context.to_numeric(left, default: nil), context.to_numeric(right, default: nil)].compact.max || :nothing
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Return the minimum of `left` and `right`.
|
|
17
|
+
def self.at_most(context, left, right)
|
|
18
|
+
[context.to_numeric(left, default: nil), context.to_numeric(right, default: nil)].compact.min || :nothing
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Return `left` rounded up to the next whole number.
|
|
22
|
+
def self.ceil(context, left)
|
|
23
|
+
left_ = context.to_numeric(left)
|
|
24
|
+
context.nothing?(left_) ? left_ : left_.ceil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Return the result of dividing `left` by `right`.
|
|
28
|
+
# If both `left` and `right` are integers, integer division is performed.
|
|
29
|
+
def self.divided_by(context, left, right)
|
|
30
|
+
lhs = context.to_numeric(left, default: :nothing)
|
|
31
|
+
rhs = context.to_numeric(right, default: :nothing)
|
|
32
|
+
return :nothing if context.nothing?(lhs) || context.nothing?(rhs) || rhs.zero? # steep:ignore
|
|
33
|
+
|
|
34
|
+
result = lhs.to_d / rhs # steep:ignore
|
|
35
|
+
result.frac.zero? && lhs.is_a?(::Integer) && rhs.is_a?(::Integer) ? result.to_i : result
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Return the result of multiplying `left` by `right`.
|
|
39
|
+
def self.times(context, left, right)
|
|
40
|
+
lhs = context.to_numeric(left, default: :nothing)
|
|
41
|
+
rhs = context.to_numeric(right, default: :nothing)
|
|
42
|
+
context.nothing?(lhs) || context.nothing?(rhs) ? :nothing : lhs * rhs # steep:ignore
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Return `left` rounded down to the next whole number.
|
|
46
|
+
def self.floor(context, left)
|
|
47
|
+
left_ = context.to_numeric(left)
|
|
48
|
+
context.nothing?(left_) ? left_ : left_.floor
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Return `right` subtracted from `left`.
|
|
52
|
+
def self.minus(context, left, right)
|
|
53
|
+
lhs = context.to_numeric(left, default: :nothing)
|
|
54
|
+
rhs = context.to_numeric(right, default: :nothing)
|
|
55
|
+
context.nothing?(lhs) || context.nothing?(rhs) ? :nothing : lhs - rhs # steep:ignore
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Return the remainder of dividing `left` by `right`.
|
|
59
|
+
def self.modulo(context, left, right)
|
|
60
|
+
lhs = context.to_numeric(left, default: :nothing)
|
|
61
|
+
rhs = context.to_numeric(right, default: :nothing)
|
|
62
|
+
return :nothing if context.nothing?(lhs) || context.nothing?(rhs) || rhs.zero? # steep:ignore
|
|
63
|
+
|
|
64
|
+
result = lhs.to_d % rhs # steep:ignore
|
|
65
|
+
result.frac.zero? && lhs.is_a?(::Integer) && rhs.is_a?(::Integer) ? result.to_i : result
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Return `right` added to `left`.
|
|
69
|
+
def self.plus(context, left, right)
|
|
70
|
+
lhs = context.to_numeric(left, default: :nothing)
|
|
71
|
+
rhs = context.to_numeric(right, default: :nothing)
|
|
72
|
+
context.nothing?(lhs) || context.nothing?(rhs) ? :nothing : lhs + rhs # steep:ignore
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Return `left` rounded to _ndigits_ decimal digits.
|
|
76
|
+
def self.round(context, left, ndigits = 0)
|
|
77
|
+
left_ = context.to_numeric(left)
|
|
78
|
+
return left_ if context.nothing?(left_)
|
|
79
|
+
return left_.round if ndigits == 0 # rubocop:disable Style/NumericPredicate
|
|
80
|
+
|
|
81
|
+
left_.round(context.to_i(ndigits, default: 0))
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Luoma
|
|
4
|
+
module Filters
|
|
5
|
+
# Return the size of _left_, or zero if _left_ has no size.
|
|
6
|
+
def self.size(context, left)
|
|
7
|
+
return left.length(context.render_context) if left.is_a?(Drop)
|
|
8
|
+
return left.size if left.respond_to?(:size)
|
|
9
|
+
|
|
10
|
+
0
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Luoma
|
|
4
|
+
module Filters
|
|
5
|
+
def self.slice(
|
|
6
|
+
context,
|
|
7
|
+
left,
|
|
8
|
+
start_ = :nothing, stop_ = :nothing, step_ = :nothing,
|
|
9
|
+
start: :nothing, stop: :nothing, step: :nothing
|
|
10
|
+
)
|
|
11
|
+
left = context.to_a(left) unless left.is_a?(String) || left.is_a?(Drop)
|
|
12
|
+
length = left.is_a?(Drop) ? left.length(context.render_context) : left.length
|
|
13
|
+
|
|
14
|
+
# Give priority to keyword arguments, default to nil if neither are given.
|
|
15
|
+
start = start_ == :nothing ? nil : start_ if start == :nothing
|
|
16
|
+
stop = stop_ == :nothing ? nil : stop_ if stop == :nothing
|
|
17
|
+
step = step_ == :nothing ? nil : step_ if step == :nothing
|
|
18
|
+
step = context.to_i(step || 1, default: 0)
|
|
19
|
+
|
|
20
|
+
return [] if length.zero? || step.zero?
|
|
21
|
+
|
|
22
|
+
start = context.to_i(start, default: 0) unless start.nil?
|
|
23
|
+
stop = context.to_i(stop, default: 0) unless stop.nil?
|
|
24
|
+
|
|
25
|
+
normalized_start = if start.nil?
|
|
26
|
+
step.negative? ? length - 1 : 0
|
|
27
|
+
elsif start&.negative?
|
|
28
|
+
[length + start, 0].max
|
|
29
|
+
else
|
|
30
|
+
[start, length - 1].min
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
normalized_stop = if stop.nil?
|
|
34
|
+
step.negative? ? -1 : length
|
|
35
|
+
elsif stop&.negative?
|
|
36
|
+
[length + stop, -1].max
|
|
37
|
+
else
|
|
38
|
+
[stop, length].min
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if left.is_a?(Drop)
|
|
42
|
+
left.slice(normalized_start, normalized_stop, step)
|
|
43
|
+
else
|
|
44
|
+
# This does not work for strings.
|
|
45
|
+
# left[(normalized_start...normalized_stop).step(step)]
|
|
46
|
+
#
|
|
47
|
+
# But this does.
|
|
48
|
+
(normalized_start...normalized_stop).step(step).map { |i| left[i] }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|