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.
Files changed (152) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +2 -0
  3. data/.rdoc_options +16 -0
  4. data/CHANGELOG.md +5 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +68 -0
  7. data/Rakefile +69 -0
  8. data/Steepfile +41 -0
  9. data/certs/jgrp.pem +27 -0
  10. data/docs/configuration.md +180 -0
  11. data/docs/custom_filters.md +3 -0
  12. data/docs/custom_tags.md +3 -0
  13. data/docs/expressions.md +3 -0
  14. data/docs/extension_types.md +41 -0
  15. data/docs/filter_reference.md +1702 -0
  16. data/docs/index.md +73 -0
  17. data/docs/luoma_for_template_authors.md +3 -0
  18. data/docs/markdown.md +111 -0
  19. data/docs/predicate_reference.md +3 -0
  20. data/docs/static_analysis.md +3 -0
  21. data/docs/tag_reference.md +352 -0
  22. data/docs/template_loaders.md +177 -0
  23. data/docs/undefined_variables.md +3 -0
  24. data/docs-requirements.txt +11 -0
  25. data/docs_/cycle.md +17 -0
  26. data/docs_/font_rendering_example.md +157 -0
  27. data/docs_/header_block_example.md +51 -0
  28. data/docs_/increment_and_decrement.md +49 -0
  29. data/docs_/logo_style_example.md +40 -0
  30. data/docs_/migration.md +11 -0
  31. data/docs_/notes.md +19 -0
  32. data/lib/luoma/cache.rb +80 -0
  33. data/lib/luoma/chain_hash.rb +54 -0
  34. data/lib/luoma/context.rb +227 -0
  35. data/lib/luoma/drop.rb +84 -0
  36. data/lib/luoma/drops/block.rb +33 -0
  37. data/lib/luoma/drops/expression.rb +29 -0
  38. data/lib/luoma/drops/html_safe.rb +36 -0
  39. data/lib/luoma/drops/range.rb +69 -0
  40. data/lib/luoma/drops/undefined.rb +119 -0
  41. data/lib/luoma/environment.rb +479 -0
  42. data/lib/luoma/errors.rb +79 -0
  43. data/lib/luoma/escape.rb +35 -0
  44. data/lib/luoma/expression.rb +1271 -0
  45. data/lib/luoma/filter.rb +97 -0
  46. data/lib/luoma/filters/array.rb +372 -0
  47. data/lib/luoma/filters/date.rb +19 -0
  48. data/lib/luoma/filters/default.rb +16 -0
  49. data/lib/luoma/filters/json.rb +14 -0
  50. data/lib/luoma/filters/math.rb +84 -0
  51. data/lib/luoma/filters/size.rb +13 -0
  52. data/lib/luoma/filters/slice.rb +52 -0
  53. data/lib/luoma/filters/sort.rb +113 -0
  54. data/lib/luoma/filters/string.rb +186 -0
  55. data/lib/luoma/fnv.rb +15 -0
  56. data/lib/luoma/lexer.rb +106 -0
  57. data/lib/luoma/lexer_legacy.rb +396 -0
  58. data/lib/luoma/lexer_unified.rb +279 -0
  59. data/lib/luoma/loader.rb +50 -0
  60. data/lib/luoma/loaders/choice_loader.rb +20 -0
  61. data/lib/luoma/loaders/file_system_loader.rb +75 -0
  62. data/lib/luoma/loaders/mixins.rb +52 -0
  63. data/lib/luoma/markup.rb +109 -0
  64. data/lib/luoma/parser.rb +252 -0
  65. data/lib/luoma/parser_unified.rb +979 -0
  66. data/lib/luoma/predicates/blank.rb +19 -0
  67. data/lib/luoma/predicates/defined.rb +10 -0
  68. data/lib/luoma/predicates/empty.rb +15 -0
  69. data/lib/luoma/predicates/type.rb +35 -0
  70. data/lib/luoma/static_analysis.rb +427 -0
  71. data/lib/luoma/tags/assign.rb +63 -0
  72. data/lib/luoma/tags/break.rb +23 -0
  73. data/lib/luoma/tags/capture.rb +43 -0
  74. data/lib/luoma/tags/case.rb +137 -0
  75. data/lib/luoma/tags/comment.rb +17 -0
  76. data/lib/luoma/tags/continue.rb +23 -0
  77. data/lib/luoma/tags/define.rb +42 -0
  78. data/lib/luoma/tags/else.rb +30 -0
  79. data/lib/luoma/tags/for.rb +108 -0
  80. data/lib/luoma/tags/if.rb +109 -0
  81. data/lib/luoma/tags/import.rb +91 -0
  82. data/lib/luoma/tags/include.rb +77 -0
  83. data/lib/luoma/tags/output.rb +20 -0
  84. data/lib/luoma/tags/raw.rb +26 -0
  85. data/lib/luoma/tags/render.rb +92 -0
  86. data/lib/luoma/tags/with.rb +55 -0
  87. data/lib/luoma/template.rb +178 -0
  88. data/lib/luoma/token.rb +84 -0
  89. data/lib/luoma/version.rb +5 -0
  90. data/lib/luoma.rb +75 -0
  91. data/sig/luoma/cache.rbs +44 -0
  92. data/sig/luoma/chain_hash.rbs +26 -0
  93. data/sig/luoma/context.rbs +115 -0
  94. data/sig/luoma/drop.rbs +44 -0
  95. data/sig/luoma/drops/block.rbs +10 -0
  96. data/sig/luoma/drops/expression.rbs +10 -0
  97. data/sig/luoma/drops/html_safe.rbs +14 -0
  98. data/sig/luoma/drops/range.rbs +15 -0
  99. data/sig/luoma/drops/undefined.rbs +27 -0
  100. data/sig/luoma/environment.rbs +212 -0
  101. data/sig/luoma/errors.rbs +67 -0
  102. data/sig/luoma/escape.rbs +15 -0
  103. data/sig/luoma/expression.rbs +514 -0
  104. data/sig/luoma/filter.rbs +66 -0
  105. data/sig/luoma/filters/array.rbs +74 -0
  106. data/sig/luoma/filters/date.rbs +9 -0
  107. data/sig/luoma/filters/default.rbs +8 -0
  108. data/sig/luoma/filters/json.rbs +7 -0
  109. data/sig/luoma/filters/math.rbs +37 -0
  110. data/sig/luoma/filters/size.rbs +6 -0
  111. data/sig/luoma/filters/slice.rbs +10 -0
  112. data/sig/luoma/filters/sort.rbs +17 -0
  113. data/sig/luoma/filters/string.rbs +103 -0
  114. data/sig/luoma/fnv.rbs +3 -0
  115. data/sig/luoma/lexer.rbs +42 -0
  116. data/sig/luoma/lexer_legacy.rbs +81 -0
  117. data/sig/luoma/lexer_unified.rbs +49 -0
  118. data/sig/luoma/loader.rbs +40 -0
  119. data/sig/luoma/loaders/choice_loader.rbs +9 -0
  120. data/sig/luoma/loaders/file_system_loader.rbs +19 -0
  121. data/sig/luoma/loaders/mixins.rbs +16 -0
  122. data/sig/luoma/markup.rbs +68 -0
  123. data/sig/luoma/parser.rbs +105 -0
  124. data/sig/luoma/parser_unified.rbs +150 -0
  125. data/sig/luoma/predicates/blank.rbs +6 -0
  126. data/sig/luoma/predicates/defined.rbs +6 -0
  127. data/sig/luoma/predicates/empty.rbs +6 -0
  128. data/sig/luoma/predicates/type.rbs +21 -0
  129. data/sig/luoma/static_analysis.rbs +141 -0
  130. data/sig/luoma/tags/assign.rbs +12 -0
  131. data/sig/luoma/tags/break.rbs +11 -0
  132. data/sig/luoma/tags/capture.rbs +14 -0
  133. data/sig/luoma/tags/case.rbs +36 -0
  134. data/sig/luoma/tags/comment.rbs +11 -0
  135. data/sig/luoma/tags/continue.rbs +11 -0
  136. data/sig/luoma/tags/define.rbs +16 -0
  137. data/sig/luoma/tags/else.rbs +17 -0
  138. data/sig/luoma/tags/for.rbs +26 -0
  139. data/sig/luoma/tags/if.rbs +45 -0
  140. data/sig/luoma/tags/import.rbs +13 -0
  141. data/sig/luoma/tags/include.rbs +13 -0
  142. data/sig/luoma/tags/output.rbs +13 -0
  143. data/sig/luoma/tags/raw.rbs +11 -0
  144. data/sig/luoma/tags/render.rbs +13 -0
  145. data/sig/luoma/tags/with.rbs +15 -0
  146. data/sig/luoma/template.rbs +117 -0
  147. data/sig/luoma/token.rbs +81 -0
  148. data/sig/luoma.rbs +11 -0
  149. data/zensical.toml +363 -0
  150. data.tar.gz.sig +0 -0
  151. metadata +233 -0
  152. metadata.gz.sig +0 -0
@@ -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