Class: Luoma::FilteredExpression

Inherits:
Expression show all
Defined in:
lib/luoma/expression.rb,
sig/luoma/expression.rbs

Instance Attribute Summary

Attributes inherited from Expression

#span, #token

Instance Method Summary collapse

Methods inherited from Expression

#scope

Constructor Details

#initialize(token, left, filter) ⇒ FilteredExpression

Returns a new instance of FilteredExpression.

Signature:

  • (t_token, Expression, Filter) -> void

Parameters:



131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/luoma/expression.rb', line 131

def initialize(token, left, filter)
  super(token)
  @left = left
  @filter = filter
  @span = filter.span

  # Filter names are stored as instances of `Variable` so we can "call"
  # namespaced lambda expressions from the `{% import %}` tag.
  #
  # Here we extract an identifier from that variable for use with
  # environment-defined filters that are never namespaced.
  @name = filter.name.root.value if filter.name.segments.empty? && filter.name.root.is_a?(Name)
end

Instance Method Details

#childrenObject



164
165
166
# File 'lib/luoma/expression.rb', line 164

def children
  [@left, @filter]
end

#evaluate(context) ⇒ Object

Signature:

  • (RenderContext) -> untyped



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/luoma/expression.rb', line 146

def evaluate(context)
  obj = @filter.name.evaluate(context)

  if obj.is_a?(ExpressionDrop)
    # User-defined filter.
    evaluate_lambda(obj.expr, context)
  elsif @name
    evaluate_filter(@name, context) # steep:ignore
  elsif context.env.strict
    raise FilterArgumentError.new(
      "unknown filter",
      @token,
      context.template.source,
      context.template.name
    )
  end
end

#evaluate_filter(name, context) ⇒ Object

Signature:

  • (RenderContext) -> untyped

Parameters:

Returns:

  • (Object)


190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/luoma/expression.rb', line 190

def evaluate_filter(name, context)
  func = context.env.filters[name]

  if func.nil?
    if context.env.strict
      raise FilterNotFoundError.new(
        "unknown filter #{name.inspect}",
        @filter.token,
        context.template.source,
        context.template.name
      )
    end

    return :nothing
  end

  filter_context = FilterContext.new(@span, context)
  left = @left.evaluate(context)

  # This will throw a FilterArgumentError if needed when `strict` is `true`.
  args, kwargs = normalize_arguments(
    context,
    func,
    @filter.args.map { |arg| arg.evaluate(context) },
    @filter.kwargs.to_h { |arg| [arg.name.value.to_sym, arg.expression.evaluate(context)] }
  )

  if args.empty? && kwargs.empty?
    func.call(filter_context, left)
  elsif kwargs.empty?
    func.call(filter_context, left, *args)
  else
    func.call(
      filter_context,
      left,
      *args,
      **kwargs
    )
  end
end

#evaluate_lambda(expr, context) ⇒ Object

Signature:

  • (LambdaExpr, RenderContext) -> untyped

Parameters:

Returns:

  • (Object)


176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/luoma/expression.rb', line 176

def evaluate_lambda(expr, context)
  if context.env.strict && !@filter.kwargs.empty?
    raise FilterArgumentError.new(
      "unexpected keyword arguments",
      @span,
      context.template.source,
      context.template.name
    )
  end

  expr.call([@left.evaluate(context), *@filter.args.map { |arg| arg.evaluate(context) }])
end

#normalize_arguments(context, method, args, kwargs) ⇒ [Array[untyped], Hash[Symbol, untyped]]

Parameters:

  • context (RenderContext)
  • method (::Method)
  • args (Array[untyped])
  • kwargs (Hash[Symbol, untyped])

Returns:

  • ([Array[untyped], Hash[Symbol, untyped]])


231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/luoma/expression.rb', line 231

def normalize_arguments(context, method, args, kwargs)
  params = method.parameters

  # The first two required arguments are always `context` and `left`, neither
  # of which are included in `args`.
  required_positional = params.count { |type, _name| type == :req } - 2
  optional_positional = params.count { |type, _name| type == :opt }
  has_rest = params.any? { |type, _name| type == :rest }

  required_keys = params.select { |type, _name| type == :keyreq }.map(&:last) # rubocop:disable Style/HashSlice
  optional_keys = params.select { |type, _name| type == :key }.map(&:last) # rubocop:disable Style/HashSlice
  has_keyrest = params.any? { |type, _name| type == :keyrest }

  if context.env.strict
    validate_arguments(
      context,
      method,
      args,
      kwargs,
      required_positional: required_positional,
      optional_positional: optional_positional,
      has_rest: has_rest,
      required_keys: required_keys,
      optional_keys: optional_keys,
      has_keyrest: has_keyrest
    )
  end

  unless has_rest
    max = required_positional + optional_positional
    args = args.take(max)
  end

  args.fill(:nothing, args.length...required_positional)

  unless has_keyrest
    allowed_keys = required_keys + optional_keys
    kwargs.select! { |key, _value| allowed_keys.include?(key) }
  end

  required_keys.each do |key|
    kwargs[key] = :nothing unless kwargs.key?(key)
  end

  [args, kwargs]
end

#to_sObject

Signature:

  • () -> String



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

def to_s
  "#{@left} | #{@filter}"
end

#validate_arguments(context, method, args, kwargs, required_positional:, optional_positional:, has_rest:, required_keys:, optional_keys:, has_keyrest:) ⇒ Object



278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
# File 'lib/luoma/expression.rb', line 278

def validate_arguments(
  context, method, args, kwargs,
  required_positional:,
  optional_positional:,
  has_rest:,
  required_keys:,
  optional_keys:,
  has_keyrest:
)
  if args.length < required_positional
    message = [
      "wrong number of arguments (given #{args.length}, ",
      "expected #{required_positional}) for #{@name.inspect}"
    ]
    raise FilterArgumentError.new(
      message.join,
      @span,
      context.template.source,
      context.template.name
    )
  end

  unless has_rest
    max_positional = required_positional + optional_positional
    if args.length > max_positional
      message = [
        "wrong number of arguments (given #{args.length}, ",
        "expected #{required_positional}..#{max_positional}) for #{@name.inspect}"
      ]
      raise FilterArgumentError.new(
        message.join,
        @span,
        context.template.source,
        context.template.name
      )
    end
  end

  unless has_keyrest
    allowed_keys = required_keys + optional_keys
    unknown_keys = kwargs.keys - allowed_keys
    unless unknown_keys.empty?
      message = [
        "unknown keyword#{"s" if unknown_keys.length > 1}: ",
        "#{unknown_keys.map(&:inspect).join(", ")} for #{@name.inspect}"
      ]
      raise FilterArgumentError.new(
        message.join,
        @span,
        context.template.source,
        context.template.name
      )
    end
  end

  missing_keys = required_keys.reject { |key| kwargs.key?(key) }
  unless missing_keys.empty?
    message = [
      "missing keyword#{"s" if missing_keys.length > 1}: ",
      "#{missing_keys.map(&:inspect).join(", ")} for #{@name.inspect}"
    ]

    raise FilterArgumentError.new(
      message.join,
      @span,
      context.template.source,
      context.template.name
    )
  end
end