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,1271 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Naming/PredicateMethod
4
+
5
+ module Luoma
6
+ # The base class for all expressions.
7
+ class Expression
8
+ attr_reader :token, :span
9
+
10
+ #: (t_token) -> void
11
+ def initialize(token)
12
+ @token = token
13
+ @span = token
14
+ end
15
+
16
+ #: (RenderContext) -> untyped
17
+ def evaluate(context)
18
+ raise "not implemented"
19
+ end
20
+
21
+ def children
22
+ raise "not implemented"
23
+ end
24
+
25
+ #: () -> Array[Name]
26
+ def scope
27
+ []
28
+ end
29
+
30
+ #: () -> String
31
+ def to_s
32
+ raise "not implemented"
33
+ end
34
+ end
35
+
36
+ class GroupExpression < Expression
37
+ #: (t_token, Expression, Array[t_path_segment]) -> void
38
+ def initialize(token, expr, segments)
39
+ super(token)
40
+ @expr = expr
41
+ @segments = segments
42
+
43
+ @span = if segments.empty?
44
+ Luoma.span(token, expr.span)
45
+ else
46
+ Luoma.span(token, segments.last.token)
47
+ end
48
+ end
49
+
50
+ #: (RenderContext) -> untyped
51
+ def evaluate(context)
52
+ obj = @expr.evaluate(context)
53
+
54
+ return obj if @segments.empty?
55
+
56
+ obj, index = context.resolve_path(obj, @segments.map { |s| s.evaluate(context) })
57
+
58
+ if obj == :nothing
59
+ context.env.undefined.new(
60
+ path(@segments[0..index] || raise),
61
+ @span,
62
+ context.template.source,
63
+ context.template.name
64
+ )
65
+ else
66
+ obj
67
+ end
68
+ end
69
+
70
+ def children
71
+ [@expr, *@segments.grep(Variable)] #: Array[_Traversable]
72
+ end
73
+
74
+ def with(token)
75
+ @span = Luoma.span(@token, token)
76
+ self
77
+ end
78
+
79
+ #: () -> String
80
+ def to_s
81
+ "(#{@expr})#{path(@segments)}"
82
+ end
83
+
84
+ private
85
+
86
+ #: (Array[t_path_segment]) -> String
87
+ def path(segments)
88
+ if segments.empty?
89
+ ""
90
+ else
91
+ segments.map { |s| s.is_a?(Name) ? ".#{s}" : "[#{s}]" }.join
92
+ end
93
+ end
94
+ end
95
+
96
+ class TernaryExpression < Expression
97
+ #: (t_token, Expression, Expression, Expression?) -> void
98
+ def initialize(token, consequence, condition, alternative)
99
+ super(token)
100
+ @consequence = consequence
101
+ @condition = condition
102
+ @alternative = alternative
103
+ @span = Luoma.span(@consequence.token, (@alternative || @condition).span)
104
+ end
105
+
106
+ #: (RenderContext) -> untyped
107
+ def evaluate(context)
108
+ if context.env.truthy?(@condition.evaluate(context), context)
109
+ @consequence.evaluate(context)
110
+ else
111
+ @alternative&.evaluate(context) || :nothing
112
+ end
113
+ end
114
+
115
+ def children
116
+ [@consequence, @condition, @alternative].compact
117
+ end
118
+
119
+ #: () -> String
120
+ def to_s
121
+ if @alternative
122
+ "#{@consequence} if #{@condition} else #{@alternative}"
123
+ else
124
+ "#{@consequence} if #{@condition}"
125
+ end
126
+ end
127
+ end
128
+
129
+ class FilteredExpression < Expression
130
+ #: (t_token, Expression, Filter) -> void
131
+ def initialize(token, left, filter)
132
+ super(token)
133
+ @left = left
134
+ @filter = filter
135
+ @span = filter.span
136
+
137
+ # Filter names are stored as instances of `Variable` so we can "call"
138
+ # namespaced lambda expressions from the `{% import %}` tag.
139
+ #
140
+ # Here we extract an identifier from that variable for use with
141
+ # environment-defined filters that are never namespaced.
142
+ @name = filter.name.root.value if filter.name.segments.empty? && filter.name.root.is_a?(Name)
143
+ end
144
+
145
+ #: (RenderContext) -> untyped
146
+ def evaluate(context)
147
+ obj = @filter.name.evaluate(context)
148
+
149
+ if obj.is_a?(ExpressionDrop)
150
+ # User-defined filter.
151
+ evaluate_lambda(obj.expr, context)
152
+ elsif @name
153
+ evaluate_filter(@name, context) # steep:ignore
154
+ elsif context.env.strict
155
+ raise FilterArgumentError.new(
156
+ "unknown filter",
157
+ @token,
158
+ context.template.source,
159
+ context.template.name
160
+ )
161
+ end
162
+ end
163
+
164
+ def children
165
+ [@left, @filter]
166
+ end
167
+
168
+ #: () -> String
169
+ def to_s
170
+ "#{@left} | #{@filter}"
171
+ end
172
+
173
+ private
174
+
175
+ #: (LambdaExpr, RenderContext) -> untyped
176
+ def evaluate_lambda(expr, context)
177
+ if context.env.strict && !@filter.kwargs.empty?
178
+ raise FilterArgumentError.new(
179
+ "unexpected keyword arguments",
180
+ @span,
181
+ context.template.source,
182
+ context.template.name
183
+ )
184
+ end
185
+
186
+ expr.call([@left.evaluate(context), *@filter.args.map { |arg| arg.evaluate(context) }])
187
+ end
188
+
189
+ #: (RenderContext) -> untyped
190
+ def evaluate_filter(name, context)
191
+ func = context.env.filters[name]
192
+
193
+ if func.nil?
194
+ if context.env.strict
195
+ raise FilterNotFoundError.new(
196
+ "unknown filter #{name.inspect}",
197
+ @filter.token,
198
+ context.template.source,
199
+ context.template.name
200
+ )
201
+ end
202
+
203
+ return :nothing
204
+ end
205
+
206
+ filter_context = FilterContext.new(@span, context)
207
+ left = @left.evaluate(context)
208
+
209
+ # This will throw a FilterArgumentError if needed when `strict` is `true`.
210
+ args, kwargs = normalize_arguments(
211
+ context,
212
+ func,
213
+ @filter.args.map { |arg| arg.evaluate(context) },
214
+ @filter.kwargs.to_h { |arg| [arg.name.value.to_sym, arg.expression.evaluate(context)] }
215
+ )
216
+
217
+ if args.empty? && kwargs.empty?
218
+ func.call(filter_context, left)
219
+ elsif kwargs.empty?
220
+ func.call(filter_context, left, *args)
221
+ else
222
+ func.call(
223
+ filter_context,
224
+ left,
225
+ *args,
226
+ **kwargs
227
+ )
228
+ end
229
+ end
230
+
231
+ def normalize_arguments(context, method, args, kwargs)
232
+ params = method.parameters
233
+
234
+ # The first two required arguments are always `context` and `left`, neither
235
+ # of which are included in `args`.
236
+ required_positional = params.count { |type, _name| type == :req } - 2
237
+ optional_positional = params.count { |type, _name| type == :opt }
238
+ has_rest = params.any? { |type, _name| type == :rest }
239
+
240
+ required_keys = params.select { |type, _name| type == :keyreq }.map(&:last) # rubocop:disable Style/HashSlice
241
+ optional_keys = params.select { |type, _name| type == :key }.map(&:last) # rubocop:disable Style/HashSlice
242
+ has_keyrest = params.any? { |type, _name| type == :keyrest }
243
+
244
+ if context.env.strict
245
+ validate_arguments(
246
+ context,
247
+ method,
248
+ args,
249
+ kwargs,
250
+ required_positional: required_positional,
251
+ optional_positional: optional_positional,
252
+ has_rest: has_rest,
253
+ required_keys: required_keys,
254
+ optional_keys: optional_keys,
255
+ has_keyrest: has_keyrest
256
+ )
257
+ end
258
+
259
+ unless has_rest
260
+ max = required_positional + optional_positional
261
+ args = args.take(max)
262
+ end
263
+
264
+ args.fill(:nothing, args.length...required_positional)
265
+
266
+ unless has_keyrest
267
+ allowed_keys = required_keys + optional_keys
268
+ kwargs.select! { |key, _value| allowed_keys.include?(key) }
269
+ end
270
+
271
+ required_keys.each do |key|
272
+ kwargs[key] = :nothing unless kwargs.key?(key)
273
+ end
274
+
275
+ [args, kwargs]
276
+ end
277
+
278
+ def validate_arguments(
279
+ context, method, args, kwargs,
280
+ required_positional:,
281
+ optional_positional:,
282
+ has_rest:,
283
+ required_keys:,
284
+ optional_keys:,
285
+ has_keyrest:
286
+ )
287
+ if args.length < required_positional
288
+ message = [
289
+ "wrong number of arguments (given #{args.length}, ",
290
+ "expected #{required_positional}) for #{@name.inspect}"
291
+ ]
292
+ raise FilterArgumentError.new(
293
+ message.join,
294
+ @span,
295
+ context.template.source,
296
+ context.template.name
297
+ )
298
+ end
299
+
300
+ unless has_rest
301
+ max_positional = required_positional + optional_positional
302
+ if args.length > max_positional
303
+ message = [
304
+ "wrong number of arguments (given #{args.length}, ",
305
+ "expected #{required_positional}..#{max_positional}) for #{@name.inspect}"
306
+ ]
307
+ raise FilterArgumentError.new(
308
+ message.join,
309
+ @span,
310
+ context.template.source,
311
+ context.template.name
312
+ )
313
+ end
314
+ end
315
+
316
+ unless has_keyrest
317
+ allowed_keys = required_keys + optional_keys
318
+ unknown_keys = kwargs.keys - allowed_keys
319
+ unless unknown_keys.empty?
320
+ message = [
321
+ "unknown keyword#{"s" if unknown_keys.length > 1}: ",
322
+ "#{unknown_keys.map(&:inspect).join(", ")} for #{@name.inspect}"
323
+ ]
324
+ raise FilterArgumentError.new(
325
+ message.join,
326
+ @span,
327
+ context.template.source,
328
+ context.template.name
329
+ )
330
+ end
331
+ end
332
+
333
+ missing_keys = required_keys.reject { |key| kwargs.key?(key) }
334
+ unless missing_keys.empty?
335
+ message = [
336
+ "missing keyword#{"s" if missing_keys.length > 1}: ",
337
+ "#{missing_keys.map(&:inspect).join(", ")} for #{@name.inspect}"
338
+ ]
339
+
340
+ raise FilterArgumentError.new(
341
+ message.join,
342
+ @span,
343
+ context.template.source,
344
+ context.template.name
345
+ )
346
+ end
347
+ end
348
+ end
349
+
350
+ class PrefixExpression < Expression
351
+ #: (t_token, Expression) -> void
352
+ def initialize(token, right)
353
+ super(token)
354
+ @right = right
355
+ @span = Luoma.span(token, right.span)
356
+ end
357
+
358
+ def children
359
+ [@right]
360
+ end
361
+ end
362
+
363
+ class NotExpression < PrefixExpression
364
+ def evaluate(context)
365
+ !context.env.truthy?(@right.evaluate(context), context)
366
+ end
367
+
368
+ #: () -> String
369
+ def to_s
370
+ "not #{@right}"
371
+ end
372
+ end
373
+
374
+ class PosExpression < PrefixExpression
375
+ def evaluate(context)
376
+ context.env.to_numeric(@right.evaluate(context), context, default: :nothing)
377
+ end
378
+
379
+ #: () -> String
380
+ def to_s
381
+ "+#{@right}"
382
+ end
383
+ end
384
+
385
+ class NegExpression < PrefixExpression
386
+ def evaluate(context)
387
+ right = context.env.to_numeric(@right.evaluate(context), context, default: :nothing)
388
+ context.env.nothing?(right) ? :nothing : -right # steep:ignore
389
+ end
390
+
391
+ #: () -> String
392
+ def to_s
393
+ "-#{@right}"
394
+ end
395
+ end
396
+
397
+ class InfixExpression < Expression
398
+ #: (t_token, Expression, Expression) -> void
399
+ def initialize(token, left, right)
400
+ super(token)
401
+ @left = left
402
+ @right = right
403
+ @span = Luoma.span(left.span, right.span)
404
+ end
405
+
406
+ def children
407
+ [@left, @right]
408
+ end
409
+ end
410
+
411
+ class CoalesceExpression < InfixExpression
412
+ #: (RenderContext) -> untyped
413
+ def evaluate(context)
414
+ left = @left.evaluate(context)
415
+ context.env.nothing?(left) ? @right.evaluate(context) : left
416
+ end
417
+
418
+ #: () -> String
419
+ def to_s
420
+ "#{@left} orElse #{@right}"
421
+ end
422
+ end
423
+
424
+ class OrExpression < InfixExpression
425
+ #: (RenderContext) -> untyped
426
+ def evaluate(context)
427
+ left = @left.evaluate(context)
428
+ context.env.truthy?(left, context) ? left : @right.evaluate(context)
429
+ end
430
+
431
+ #: () -> String
432
+ def to_s
433
+ "#{@left} or #{@right}"
434
+ end
435
+ end
436
+
437
+ class AndExpression < InfixExpression
438
+ #: (RenderContext) -> untyped
439
+ def evaluate(context)
440
+ left = @left.evaluate(context)
441
+ context.env.truthy?(left, context) ? @right.evaluate(context) : left
442
+ end
443
+
444
+ #: () -> String
445
+ def to_s
446
+ "#{@left} and #{@right}"
447
+ end
448
+ end
449
+
450
+ class EqExpression < InfixExpression
451
+ #: (RenderContext) -> untyped
452
+ def evaluate(context)
453
+ context.env.eq?(@left.evaluate(context), @right.evaluate(context), context, @span)
454
+ end
455
+
456
+ #: () -> String
457
+ def to_s
458
+ "#{@left} == #{@right}"
459
+ end
460
+ end
461
+
462
+ class NeExpression < InfixExpression
463
+ #: (RenderContext) -> untyped
464
+ def evaluate(context)
465
+ !context.env.eq?(@left.evaluate(context), @right.evaluate(context), context, @span)
466
+ end
467
+
468
+ #: () -> String
469
+ def to_s
470
+ "#{@left} != #{@right}"
471
+ end
472
+ end
473
+
474
+ class LtExpression < InfixExpression
475
+ #: (RenderContext) -> untyped
476
+ def evaluate(context)
477
+ context.env.lt?(@left.evaluate(context), @right.evaluate(context), context, @span)
478
+ end
479
+
480
+ #: () -> String
481
+ def to_s
482
+ "#{@left} < #{@right}"
483
+ end
484
+ end
485
+
486
+ class LeExpression < InfixExpression
487
+ #: (RenderContext) -> untyped
488
+ def evaluate(context)
489
+ left = @left.evaluate(context)
490
+ right = @right.evaluate(context)
491
+ context.env.lt?(left, right, context, @span) || context.env.eq?(left, right, context, @span)
492
+ end
493
+
494
+ #: () -> String
495
+ def to_s
496
+ "#{@left} <= #{@right}"
497
+ end
498
+ end
499
+
500
+ class GtExpression < InfixExpression
501
+ #: (RenderContext) -> untyped
502
+ def evaluate(context)
503
+ context.env.lt?(@right.evaluate(context), @left.evaluate(context), context, @span)
504
+ end
505
+
506
+ #: () -> String
507
+ def to_s
508
+ "#{@left} > #{@right}"
509
+ end
510
+ end
511
+
512
+ class GeExpression < InfixExpression
513
+ #: (RenderContext) -> untyped
514
+ def evaluate(context)
515
+ left = @left.evaluate(context)
516
+ right = @right.evaluate(context)
517
+ context.env.lt?(right, left, context, @span) || context.env.eq?(left, right, context, @span)
518
+ end
519
+
520
+ #: () -> String
521
+ def to_s
522
+ "#{@left} >= #{@right}"
523
+ end
524
+ end
525
+
526
+ class ContainsExpression < InfixExpression
527
+ #: (RenderContext) -> untyped
528
+ def evaluate(context)
529
+ context.env.contains?(@left.evaluate(context), @right.evaluate(context), context, @span)
530
+ end
531
+
532
+ #: () -> String
533
+ def to_s
534
+ "#{@left} contains #{@right}"
535
+ end
536
+ end
537
+
538
+ class InExpression < InfixExpression
539
+ #: (RenderContext) -> untyped
540
+ def evaluate(context)
541
+ context.env.contains?(@right.evaluate(context), @left.evaluate(context), context, @span)
542
+ end
543
+
544
+ #: () -> String
545
+ def to_s
546
+ "#{@left} in #{@right}"
547
+ end
548
+ end
549
+
550
+ class AddExpression < InfixExpression
551
+ #: (RenderContext) -> untyped
552
+ def evaluate(context)
553
+ left = context.env.to_numeric(@left.evaluate(context), context, default: :nothing)
554
+ right = context.env.to_numeric(@right.evaluate(context), context, default: :nothing)
555
+ left == :nothing || right == :nothing ? :nothing : left + right # steep:ignore
556
+ end
557
+
558
+ #: () -> String
559
+ def to_s
560
+ "#{@left} + #{@right}"
561
+ end
562
+ end
563
+
564
+ class SubExpression < InfixExpression
565
+ #: (RenderContext) -> untyped
566
+ def evaluate(context)
567
+ left = context.env.to_numeric(@left.evaluate(context), context, default: :nothing)
568
+ right = context.env.to_numeric(@right.evaluate(context), context, default: :nothing)
569
+ left == :nothing || right == :nothing ? :nothing : left - right # steep:ignore
570
+ end
571
+
572
+ #: () -> String
573
+ def to_s
574
+ "#{@left} - #{@right}"
575
+ end
576
+ end
577
+
578
+ class MulExpression < InfixExpression
579
+ #: (RenderContext) -> untyped
580
+ def evaluate(context)
581
+ left = context.env.to_numeric(@left.evaluate(context), context, default: :nothing)
582
+ right = context.env.to_numeric(@right.evaluate(context), context, default: :nothing)
583
+ left == :nothing || right == :nothing ? :nothing : left * right # steep:ignore
584
+ end
585
+
586
+ #: () -> String
587
+ def to_s
588
+ "#{@left} * #{@right}"
589
+ end
590
+ end
591
+
592
+ class DivExpression < InfixExpression
593
+ #: (RenderContext) -> untyped
594
+ def evaluate(context)
595
+ lhs = context.env.to_numeric(@left.evaluate(context), context, default: :nothing)
596
+ rhs = context.env.to_numeric(@right.evaluate(context), context, default: :nothing)
597
+ return :nothing if lhs == :nothing || rhs == :nothing || rhs.zero? # steep:ignore
598
+
599
+ result = lhs.to_d / rhs # steep:ignore
600
+ result.frac.zero? && lhs.is_a?(::Integer) && rhs.is_a?(::Integer) ? result.to_i : result
601
+ end
602
+
603
+ #: () -> String
604
+ def to_s
605
+ "#{@left} / #{@right}"
606
+ end
607
+ end
608
+
609
+ class ModExpression < InfixExpression
610
+ #: (RenderContext) -> untyped
611
+ def evaluate(context)
612
+ lhs = context.env.to_numeric(@left.evaluate(context), context, default: :nothing)
613
+ rhs = context.env.to_numeric(@right.evaluate(context), context, default: :nothing)
614
+ return :nothing if lhs == :nothing || rhs == :nothing || rhs.zero? # steep:ignore
615
+
616
+ result = lhs.to_d % rhs # steep:ignore
617
+ result.frac.zero? && lhs.is_a?(::Integer) && rhs.is_a?(::Integer) ? result.to_i : result
618
+ end
619
+
620
+ #: () -> String
621
+ def to_s
622
+ "#{@left} % #{@right}"
623
+ end
624
+ end
625
+
626
+ class Variable < Expression
627
+ attr_reader :root, :segments
628
+
629
+ #: (t_token, Name | StringLiteral | Variable, Array[t_path_segment]) -> void
630
+ def initialize(token, root, segments)
631
+ super(token)
632
+ @root = root
633
+ @segments = segments
634
+ @span = segments.empty? ? root.span : Luoma.span(root.span, segments.last.span)
635
+ end
636
+
637
+ #: (RenderContext) -> untyped
638
+ def evaluate(context)
639
+ root_segment = @root.is_a?(Variable) ? @root.evaluate(context) : @root.value # steep:ignore
640
+ root = root_segment.is_a?(String) ? context.scopes.fetch(root_segment, :nothing) : :nothing
641
+
642
+ obj, index = if @segments.empty?
643
+ [root, 0]
644
+ else
645
+ context.resolve_path(root, @segments.map { |s| s.evaluate(context) })
646
+ end
647
+
648
+ if obj == :nothing
649
+ context.env.undefined.new(
650
+ path(@segments[0..index] || raise),
651
+ @span,
652
+ context.template.source,
653
+ context.template.name
654
+ )
655
+ else
656
+ obj
657
+ end
658
+ end
659
+
660
+ def children
661
+ if @root.is_a?(Variable)
662
+ [@root, *@segments.grep(Variable)] #: Array[_Traversable]
663
+ else
664
+ @segments.filter.grep(Variable) #: Array[_Traversable]
665
+ end
666
+ end
667
+
668
+ #: () -> String
669
+ def to_s
670
+ path(@segments)
671
+ end
672
+
673
+ # Return true if this variable has a root segment matching `name` and
674
+ # no other segments.
675
+ #: (String) -> bool
676
+ def ident?(name)
677
+ @segments.empty? && @root.is_a?(Name) && @root.value == name # steep:ignore
678
+ end
679
+
680
+ private
681
+
682
+ #: (Array[t_path_segment]) -> String
683
+ def path(segments)
684
+ root = @root.is_a?(Name) ? @root.to_s : "[#{@root}]"
685
+ return root if segments.empty?
686
+
687
+ segments_ = segments.map { |s| s.is_a?(Name) || s.is_a?(Predicate) ? ".#{s}" : "[#{s}]" }.join
688
+ "#{root}#{segments_}"
689
+ end
690
+ end
691
+
692
+ class IndexSelector < Expression
693
+ attr_reader :value
694
+
695
+ #: (t_token, Integer)
696
+ def initialize(token, value)
697
+ super(token)
698
+ @value = value
699
+ end
700
+
701
+ #: (RenderContext) -> untyped
702
+ def evaluate(context)
703
+ @value
704
+ end
705
+
706
+ def children
707
+ []
708
+ end
709
+
710
+ #: () -> String
711
+ def to_s
712
+ @value.to_s
713
+ end
714
+
715
+ def with(token)
716
+ @span = Luoma.span(@token, token)
717
+ self
718
+ end
719
+ end
720
+
721
+ class Lambda < Expression
722
+ #: (t_token, Array[Name], Expression) -> void
723
+ def initialize(token, params, expr)
724
+ super(token)
725
+ @params = params
726
+ @expr = expr
727
+ @span = Luoma.span(token, expr.span)
728
+ end
729
+
730
+ #: (RenderContext) -> untyped
731
+ def evaluate(context)
732
+ ExpressionDrop.new(LambdaExpr.new(@params.map(&:value), @expr, context))
733
+ end
734
+
735
+ def children
736
+ [@expr]
737
+ end
738
+
739
+ def scope
740
+ @params
741
+ end
742
+
743
+ #: () -> String
744
+ def to_s
745
+ "(#{@params.join(", ")}) => #{@expr}"
746
+ end
747
+ end
748
+
749
+ class StringLiteral < Expression
750
+ attr_reader :segments
751
+
752
+ #: (t_token, Array[String|Expression], t_token)
753
+ def initialize(token, segments, end_token)
754
+ super(token)
755
+ @segments = segments
756
+ @span = Luoma.span(token, end_token)
757
+ end
758
+
759
+ #: (RenderContext) -> untyped
760
+ def evaluate(context)
761
+ @segments.map do |s|
762
+ s.is_a?(String) ? s : context.env.to_string(s.evaluate(context), context)
763
+ end.join
764
+ end
765
+
766
+ def children
767
+ @segments.grep_v(String) #: Array[Expression]
768
+ end
769
+
770
+ #: () -> String
771
+ def to_s
772
+ @segments.map { |s| s.is_a?(String) ? s : "${#{s}}" }.join.inspect
773
+ end
774
+
775
+ def with(token)
776
+ @span = Luoma.span(@token, token)
777
+ self
778
+ end
779
+
780
+ # Return the string, or nil if the string literal contains interpolated expressions.
781
+ #: () -> String?
782
+ def value
783
+ @segments.first if @segments.length == 1 && @segments.first.is_a?(String) #: String?
784
+ end
785
+ end
786
+
787
+ class IntegerLiteral < Expression
788
+ attr_reader :value
789
+
790
+ #: (t_token, Integer)
791
+ def initialize(token, value)
792
+ super(token)
793
+ @value = value
794
+ end
795
+
796
+ #: (RenderContext) -> untyped
797
+ def evaluate(context)
798
+ @value
799
+ end
800
+
801
+ def children
802
+ []
803
+ end
804
+
805
+ #: () -> String
806
+ def to_s
807
+ @value.to_s
808
+ end
809
+ end
810
+
811
+ class FloatLiteral < Expression
812
+ attr_reader :value
813
+
814
+ #: (t_token, Float)
815
+ def initialize(token, value)
816
+ super(token)
817
+ @value = value
818
+ end
819
+
820
+ #: (RenderContext) -> untyped
821
+ def evaluate(context)
822
+ @value
823
+ end
824
+
825
+ def children
826
+ []
827
+ end
828
+
829
+ #: () -> String
830
+ def to_s
831
+ @value.to_s
832
+ end
833
+ end
834
+
835
+ class BooleanLiteral < Expression
836
+ attr_reader :value
837
+
838
+ #: (t_token, bool)
839
+ def initialize(token, value)
840
+ super(token)
841
+ @value = value
842
+ end
843
+
844
+ #: (RenderContext) -> untyped
845
+ def evaluate(context)
846
+ @value
847
+ end
848
+
849
+ def children
850
+ []
851
+ end
852
+
853
+ #: () -> String
854
+ def to_s
855
+ @value.to_s
856
+ end
857
+ end
858
+
859
+ class NullLiteral < Expression
860
+ #: (RenderContext) -> untyped
861
+ def evaluate(context)
862
+ nil
863
+ end
864
+
865
+ def children
866
+ []
867
+ end
868
+
869
+ #: () -> String
870
+ def to_s
871
+ ""
872
+ end
873
+ end
874
+
875
+ class RangeLiteral < Expression
876
+ #: (t_token, Expression, Expression) -> void
877
+ def initialize(token, start, stop)
878
+ super(token)
879
+ @start = start
880
+ @stop = stop
881
+ @span = Luoma.span(start.token, stop.token)
882
+ end
883
+
884
+ #: (RenderContext) -> untyped
885
+ def evaluate(context)
886
+ start = context.env.to_i(@start.evaluate(context), context, default: 0)
887
+ stop = context.env.to_i(@stop.evaluate(context), context, default: 0)
888
+ RangeDrop.new(start, stop)
889
+ end
890
+
891
+ def children
892
+ [@start, @stop]
893
+ end
894
+
895
+ #: () -> String
896
+ def to_s
897
+ "(#{@start}..#{@stop})"
898
+ end
899
+ end
900
+
901
+ class ArrayLiteral < Expression
902
+ #: (t_token token, Array[Expression|Spread] items) -> void
903
+ def initialize(token, items)
904
+ super(token)
905
+ @items = items
906
+ end
907
+
908
+ #: (RenderContext) -> untyped
909
+ def evaluate(context)
910
+ result = [] #: Array[untyped]
911
+
912
+ @items.each do |item|
913
+ if item.is_a?(Spread)
914
+ result.concat(context.env.to_a(item.expr.evaluate(context), context))
915
+ else
916
+ result << item.evaluate(context)
917
+ end
918
+ end
919
+
920
+ result
921
+ end
922
+
923
+ def children
924
+ @items
925
+ end
926
+
927
+ #: () -> String
928
+ def to_s
929
+ "[#{@items.join(", ")}]"
930
+ end
931
+
932
+ def with(token)
933
+ @span = Luoma.span(@token, token)
934
+ self
935
+ end
936
+ end
937
+
938
+ class ObjectLiteral < Expression
939
+ #: (t_token token, Array[Item|Spread] items) -> void
940
+ def initialize(token, items)
941
+ super(token)
942
+ @items = items
943
+ end
944
+
945
+ #: (RenderContext) -> untyped
946
+ def evaluate(context)
947
+ result = {} #: Hash[String, untyped]
948
+
949
+ @items.each do |item|
950
+ if item.is_a?(Spread)
951
+ result.update(context.env.to_h(item.expr.evaluate(context), context))
952
+ else
953
+ result[item.key.value] = item.expr.evaluate(context)
954
+ end
955
+ end
956
+
957
+ result
958
+ end
959
+
960
+ def children
961
+ @items
962
+ end
963
+
964
+ #: () -> String
965
+ def to_s
966
+ "{#{@items.join(", ")}}"
967
+ end
968
+
969
+ def with(token)
970
+ @span = Luoma.span(@token, token)
971
+ self
972
+ end
973
+ end
974
+
975
+ class Predicate < Expression
976
+ attr_reader :value
977
+
978
+ #: (t_token, String) -> void
979
+ def initialize(token, value)
980
+ super(token)
981
+ @value = value
982
+ end
983
+
984
+ #: (RenderContext) -> untyped
985
+ def evaluate(context)
986
+ func = context.env.predicates[@value]
987
+
988
+ if func.nil?
989
+ if context.env.strict
990
+ raise PredicateNotFoundError.new(
991
+ "unknown predicate #{@value.inspect}",
992
+ @token,
993
+ context.template.source,
994
+ context.template.name
995
+ )
996
+ end
997
+
998
+ :nothing
999
+ else
1000
+ PredicateFunction.new(@value, func)
1001
+ end
1002
+ end
1003
+
1004
+ def children
1005
+ []
1006
+ end
1007
+
1008
+ #: () -> String
1009
+ def to_s
1010
+ "#{@value}?"
1011
+ end
1012
+ end
1013
+
1014
+ class Spread
1015
+ attr_reader :token, :expr
1016
+
1017
+ #: (t_token, Expression) -> void
1018
+ def initialize(token, expr)
1019
+ @token = token
1020
+ @expr = expr
1021
+ end
1022
+
1023
+ def children
1024
+ [@expr]
1025
+ end
1026
+
1027
+ #: () -> String
1028
+ def to_s
1029
+ "...#{@expr}"
1030
+ end
1031
+ end
1032
+
1033
+ class Item
1034
+ attr_reader :token, :key, :expr
1035
+
1036
+ #: (t_token, Name, Expression) -> void
1037
+ def initialize(token, key, expr)
1038
+ @token = token
1039
+ @key = key
1040
+ @expr = expr
1041
+ end
1042
+
1043
+ def children
1044
+ [@expr]
1045
+ end
1046
+
1047
+ #: () -> String
1048
+ def to_s
1049
+ "#{@key}: #{@expr}"
1050
+ end
1051
+ end
1052
+
1053
+ class Name
1054
+ attr_reader :token, :span, :value
1055
+
1056
+ #: (t_token, String) -> void
1057
+ def initialize(token, value)
1058
+ @token = token
1059
+ @value = value
1060
+ @span = token
1061
+ end
1062
+
1063
+ #: (RenderContext) -> untyped
1064
+ def evaluate(context)
1065
+ @value
1066
+ end
1067
+
1068
+ def children
1069
+ []
1070
+ end
1071
+
1072
+ #: () -> String
1073
+ def to_s
1074
+ @value
1075
+ end
1076
+ end
1077
+
1078
+ class Filter
1079
+ attr_reader :token, :span, :name, :args, :kwargs
1080
+
1081
+ #: (t_token, Variable, Array[Expression], Array[KeywordArgument])
1082
+ def initialize(token, name, args, kwargs)
1083
+ @token = token
1084
+ @name = name
1085
+ @args = args
1086
+ @kwargs = kwargs
1087
+
1088
+ @span = if args.empty? && kwargs.empty?
1089
+ token
1090
+ elsif kwargs.empty?
1091
+ Luoma.span(token, args.last.span)
1092
+ elsif args.empty?
1093
+ Luoma.span(token, kwargs.last.span)
1094
+ else
1095
+ Luoma.span(
1096
+ token,
1097
+ kwargs.last.token[1] > args.last.token[1] ? kwargs.last.span : args.last.span
1098
+ )
1099
+ end
1100
+ end
1101
+
1102
+ def children
1103
+ @kwargs.empty? ? @args : [*@args, *@kwargs.map(&:expression)]
1104
+ end
1105
+
1106
+ def to_s
1107
+ return @name.to_s if @args.empty? && @kwargs.empty?
1108
+
1109
+ args = @args.join(", ")
1110
+ args << ", " << @kwargs.join(",") unless @kwargs.empty?
1111
+ "#{@name}: #{args}"
1112
+ end
1113
+ end
1114
+
1115
+ class KeywordArgument
1116
+ attr_reader :token, :span, :name, :expression
1117
+
1118
+ #: (t_token, Name, Expression) -> void
1119
+ def initialize(token, name, expression)
1120
+ @token = token
1121
+ @name = name
1122
+ @expression = expression
1123
+ @span = Luoma.span(token, expression.span)
1124
+ end
1125
+
1126
+ def children
1127
+ [@expression]
1128
+ end
1129
+
1130
+ #: () -> String
1131
+ def to_s
1132
+ "#{@name}: #{@expression}"
1133
+ end
1134
+
1135
+ def deconstruct
1136
+ [@name.value, @expression]
1137
+ end
1138
+
1139
+ def deconstruct_keys(keys)
1140
+ { name: @name.value, expression: @expression }
1141
+ end
1142
+ end
1143
+
1144
+ class PredicateFunction
1145
+ attr_reader :name, :func
1146
+
1147
+ #: (String, ^(untyped) -> bool) -> void
1148
+ def initialize(name, func)
1149
+ @name = name
1150
+ @func = func
1151
+ end
1152
+
1153
+ #: (untyped) -> bool
1154
+ def call(context, obj)
1155
+ @func.call(context, obj)
1156
+ end
1157
+ end
1158
+
1159
+ class LambdaExpr
1160
+ attr_reader :params
1161
+
1162
+ #: (Array[String], Expression, RenderContext) -> void
1163
+ def initialize(params, expr, context)
1164
+ @params = params
1165
+ @expr = expr
1166
+ @context = context
1167
+ end
1168
+
1169
+ #: (Array[untyped]) -> untyped
1170
+ def call(args)
1171
+ # zip pads with `nil` and ignores excess args.
1172
+ @context.extends(@params.zip(args).to_h) do
1173
+ @expr.evaluate(@context)
1174
+ end
1175
+ end
1176
+
1177
+ # Call this lambda expression for each item in `enum`, passing item and
1178
+ # index as arguments if this expression accepts two arguments, otherwise
1179
+ # just item. Excess parameters are ignored.
1180
+ #
1181
+ #: (Enumerable[untyped]) -> Array[untyped]
1182
+ def broadcast_with_index(enum)
1183
+ scope = {} #: Hash[String, untyped]
1184
+ result = [] #: Array[untyped]
1185
+
1186
+ if @params.length == 1
1187
+ param = @params[0]
1188
+
1189
+ @context.extends(scope) do
1190
+ enum.each do |item|
1191
+ scope[param] = item
1192
+ result << @expr.evaluate(@context)
1193
+ end
1194
+ end
1195
+ else
1196
+ name_param = @params[0]
1197
+ index_param = @params[1]
1198
+
1199
+ @context.extends(scope) do
1200
+ enum.each_with_index do |item, i|
1201
+ scope[index_param] = i
1202
+ scope[name_param] = item
1203
+ result << @expr.evaluate(@context)
1204
+ end
1205
+ end
1206
+ end
1207
+
1208
+ result
1209
+ end
1210
+
1211
+ # Call this lambda expression, passing `value` and `index` as arguments
1212
+ # if this expression accepts two arguments, otherwise just `value`. Excess
1213
+ # parameters are ignored.
1214
+ #
1215
+ #: (untyped, Integer) -> untyped
1216
+ def call_with_index(value, index)
1217
+ scope = { @params[0] => value }
1218
+ scope[@params[1]] = index if @params.length > 1
1219
+
1220
+ @context.extends(scope) do
1221
+ @expr.evaluate(@context)
1222
+ end
1223
+ end
1224
+
1225
+ #: () -> String
1226
+ def to_s
1227
+ "(#{@params.join(", ")}) => #{@expr}"
1228
+ end
1229
+ end
1230
+
1231
+ #: (Expression) -> String
1232
+ def self.tree_view(e)
1233
+ # (prefix, connector, class_name, inspect_value)
1234
+ nodes = [] # : Array[[String, String, String, String]]
1235
+
1236
+ # @type var visit: ^(_Traversable, String, bool) -> void
1237
+ visit = lambda do |node, prefix, is_last|
1238
+ connector = if prefix.empty?
1239
+ ""
1240
+ elsif is_last
1241
+ "└── "
1242
+ else
1243
+ "├── "
1244
+ end
1245
+
1246
+ nodes << [prefix, connector, node.class.to_s, node.to_s]
1247
+ child_prefix = prefix + (is_last ? " " : "│ ")
1248
+ node.children.each_with_index do |child, i|
1249
+ last = i == node.children.length - 1
1250
+ visit.call(child, child_prefix, last)
1251
+ end
1252
+ end
1253
+
1254
+ visit.call(e, "", true)
1255
+
1256
+ widths = nodes.map { |prefix, connector, cls| (prefix + connector + cls).length }
1257
+ max_width = widths.max || 0
1258
+
1259
+ lines = [] # : Array[String]
1260
+ nodes.zip(widths).each do |node, width|
1261
+ prefix, connector, cls, val = node
1262
+ left = prefix + connector + cls
1263
+ padding = " " * (max_width - (width || raise) + 4)
1264
+ lines << (left + padding + val)
1265
+ end
1266
+
1267
+ lines.join("\n")
1268
+ end
1269
+ end
1270
+
1271
+ # rubocop:enable Naming/PredicateMethod