Class: Twig::ExpressionParser

Inherits:
Object
  • Object
show all
Defined in:
lib/twig/expression_parser.rb

Constant Summary collapse

OPERATOR_LEFT =
1
OPERATOR_RIGHT =
2

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(parser, environment) ⇒ ExpressionParser

Returns a new instance of ExpressionParser.

Parameters:



14
15
16
17
# File 'lib/twig/expression_parser.rb', line 14

def initialize(parser, environment)
  @parser = parser
  @environment = environment
end

Instance Attribute Details

#environmentEnvironment (readonly)

Returns:



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
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
230
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
277
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
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
# File 'lib/twig/expression_parser.rb', line 6

class ExpressionParser
  attr_reader :environment

  OPERATOR_LEFT = 1
  OPERATOR_RIGHT = 2

  # @param [Parser] parser
  # @param [Environment] environment
  def initialize(parser, environment)
    @parser = parser
    @environment = environment
  end

  # @return [Node::Expression::Base]
  def parse_expression(precedence = 0)
    # @todo parse arrow

    expr = primary
    token = parser.current_token

    while binary?(token) && binary_operators[token.value.to_sym][:precedence] >= precedence
      operator = binary_operators[token.value.to_sym]
      parser.stream.next

      next_precedence = operator[:precedence]
      next_precedence += 1 if operator[:associativity] == OPERATOR_LEFT

      expr1 = parse_expression(next_precedence)

      # @type [Node::Expression::Binary::Base]
      expr = operator[:class].new(expr, expr1, token.lineno)
      expr.attributes[:operator] = "binary_#{token.value}"

      token = parser.current_token
    end

    return parse_ternary_expression(expr) if precedence.zero?

    expr
  end

  # @return [Node::Expression::Base]
  def parse_primary_expression
    token = parser.current_token

    case token.type
    when Token::SYMBOL_TYPE
      parser.stream.next

      node = Node::Expression::Constant.new(token.value.to_sym, token.lineno)
    when Token::NAME_TYPE
      parser.stream.next

      node = case token.value
             when 'true', 'TRUE'
               Node::Expression::Constant.new(true, token.lineno)
             when 'false', 'FALSE'
               Node::Expression::Constant.new(false, token.lineno)
             when 'null', 'NULL', 'nil'
               Node::Expression::Constant.new(nil, token.lineno)
             else
               if parser.current_token.value == '('
                 get_function_node(token.value, token.lineno)
               else
                 # @todo should be a context variable
                 Node::Expression::Name.new(token.value, token.lineno)
               end
             end
    when Token::NUMBER_TYPE
      parser.stream.next

      node = Node::Expression::Constant.new(token.value, token.lineno)
    when Token::STRING_TYPE, Token::INTERPOLATION_START_TYPE
      node = parse_string_expression
    when Token::PUNCTUATION_TYPE
      case token.value
      when '['
        node = parse_sequence_expression
      when '{'
        node = parse_mapping_expression
      else
        Error::Syntax.new(
          "Unexpected token #{token.type} of value #{token.value}",
          token.lineno,
          parser.stream.source
        )
      end
    else
      raise Error::Syntax.new(
        "Unexpected token '#{token.type}' of value '#{token.value}'",
        token.lineno,
        parser.stream.source
      )
    end

    parse_post_fix_expression(node)
  end

  # @param [Node::Expression::Base] node
  # @return [Node::Expression::Base]
  def parse_post_fix_expression(node)
    loop do
      token = parser.current_token

      unless token.type == Token::PUNCTUATION_TYPE
        break
      end

      case token.value
      when '.', '['
        node = parse_subscript_expression(node)
      when '|'
        node = parse_filter_expression(node)
      else
        break
      end
    end

    node
  end

  def parse_subscript_expression(node)
    if parser.stream.next.value == '.'
      return parse_subscript_expression_dot(node)
    end

    parse_subscript_expression_array(node)
  end

  def parse_subscript_expression_dot(node)
    stream = parser.stream
    token = stream.current
    lineno = token.lineno
    arguments = Node::Expression::Array.new({}, lineno)
    token = stream.next

    if token.type == Token::NAME_TYPE
      attribute = Node::Expression::Constant.new(token.value, token.lineno)
    end

    Node::Expression::GetAttribute.new(node, attribute, arguments, nil, token.lineno)
  end

  def parse_sequence_expression
    stream = parser.stream
    stream.expect(Token::PUNCTUATION_TYPE, '[', 'A sequence element was expected')

    node = Node::Expression::Array.new({}, stream.current.lineno)
    first = true

    # raise stream.debug

    until stream.test(Token::PUNCTUATION_TYPE, ']')
      unless first
        stream.expect(Token::PUNCTUATION_TYPE, ',', 'A sequence element must be followed by a comma')

        # trailing comma
        break if stream.test(Token::PUNCTUATION_TYPE, ']')
      end

      first = false

      if stream.next_if(Token::SPREAD_TYPE)
        expr = parse_expression
        expr.attributes[:spread] = true
        node.add_element(expr)
      else
        node.add_element(parse_expression)
      end
    end

    stream.expect(Token::PUNCTUATION_TYPE, ']', 'An opened sequence is not properly closed')

    node
  end

  def parse_mapping_expression
    stream = parser.stream
    stream.expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected')

    node = Node::Expression::Array.new({}, stream.current.lineno)
    first = true

    until stream.test(Token::PUNCTUATION_TYPE, '}')
      unless first
        stream.expect(Token::PUNCTUATION_TYPE, ',', 'A mapping value must be followed by a comma')

        # trailing comma
        if stream.test(Token::PUNCTUATION_TYPE, '}')
          break
        end
      end

      first = false

      if stream.next_if(Token::SPREAD_TYPE)
        value = parse_expression
        value.attributes[:spread] = true
        node.add_element(value)

        next
      end

      # a mapping key can be:
      #
      #  * a number -- 12
      #  * a string -- 'a'
      #  * a name, which is equivalent to a symbol -- a
      #  * an expression, which must be enclosed in parentheses -- (1 + 2)
      if (token = stream.next_if(Token::NAME_TYPE))
        key = Node::Expression::Constant.new(token.value.to_sym, token.lineno)

        # {a} is a shortcut for {a: a}
        if stream.test(Token::PUNCTUATION_TYPE, %w[, }])
          value = Node::Expression::Variable::Context.new(key.attributes[:value], key.lineno)
          node.add_element(value, key.to_sym)

          next
        end
      elsif (token = stream.next_if(Token::STRING_TYPE)) || (token = stream.next_if(Token::NUMBER_TYPE))
        key = Node::Expression::Constant.new(token.value, token.lineno)
      elsif stream.test(Token::PUNCTUATION_TYPE, '(')
        key = parse_expression
      else
        current = stream.current

        raise Error::Syntax.new(
          'A mapping key must be a quoted string, number, name, or expression in parentheses ' \
          "expected token '#{current.type}' of value '#{current.value}'",
          current.lineno,
          stream.source
        )
      end

      stream.expect(Token::PUNCTUATION_TYPE, ':', 'A mapping key must be followed by a colon (:)')
      value = parse_expression

      node.add_element(value, key)
    end

    stream.expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed')

    node
  end

  def get_function_node(name, line)
    # @todo lots of stuff in this method
    args = parse_only_arguments

    if environment.helper_method?(name.to_sym)
      Node::Expression::HelperMethod.new(name, args, line)
    else
      raise Error::Syntax.new("Unknown function '#{name}'", line, parser.stream.source)
    end
  end

  def parse_only_arguments
    args = AutoHash.new
    stream = parser.stream
    stream.expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis')
    has_spread = false

    until stream.test(Token::PUNCTUATION_TYPE, ')')
      unless args.empty?
        stream.expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma')

        # if above was trailing comma, exit early
        break if stream.test(Token::PUNCTUATION_TYPE, ')')
      end

      if stream.next_if(Token::SPREAD_TYPE)
        has_spread = true
        value = Node::Expression::Unary::Spread.new(parse_expression, stream.current.lineno)
      elsif has_spread
        raise Error::Syntax.new(
          'Normal arguments must be placed before argument unpacking.',
          stream.current.lineno,
          stream.source
        )
      else
        value = parse_expression
      end

      name = nil
      if (token = stream.next_if(Token::OPERATOR_TYPE, '=')) ||
         (token = stream.next_if(Token::PUNCTUATION_TYPE, ':'))
        unless value.class <= Node::Expression::Name
          raise Error::Syntax.new(
            "A parameter name must be a string, #{value.class.name} given.",
            token.lineno,
            stream.source
          )
        end

        name = value.attributes[:name]
        value = parse_expression
      end

      if name.nil?
        args.add(value)
      else
        args[name] = value
      end
    end

    stream.expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis')

    Node::Nodes.new(args)
  end

  # @return [Parser]
  attr_reader :parser

  # @return [Node::Expression::Base]
  def primary
    token = parser.current_token

    if unary?(token)
      operator = unary_operators[token.value.to_sym]
      parser.stream.next

      expr = parse_expression(operator[:precedence])
      expr = operator[:class].new(expr, token.lineno)
      expr.attributes[:operator] = "unary_#{token.value}"

      return parse_post_fix_expression(expr)
    elsif token.test(Token::PUNCTUATION_TYPE, '(')
      parser.stream.next
      expr = parse_expression.set_explicit_parentheses

      parser.stream.expect(Token::PUNCTUATION_TYPE, ')', 'Open parenthesis not closed')

      return parse_post_fix_expression(expr)
    end

    parse_primary_expression
  end

  # @param [Node::Expression] expr
  # @return [Node::Expression]
  def parse_ternary_expression(expr)
    while parser.stream.next_if(Token::PUNCTUATION_TYPE, '?')
      expr2 = parse_expression
      expr3 = if parser.stream.next_if(Token::PUNCTUATION_TYPE, ':')
                parse_expression
              else
                Node::Expression::Constant.new('', parser.current_token.lineno)
              end

      expr = Node::Expression::Ternary.new(expr, expr2, expr3, parser.current_token.lineno)
    end

    expr
  end

  def parse_filter_expression(node)
    parser.stream.next

    parse_filter_expression_raw(node)
  end

  def parse_filter_expression_raw(node)
    loop do
      token = parser.stream.expect(Token::NAME_TYPE)

      arguments = if parser.stream.test(Token::PUNCTUATION_TYPE, '(')
                    parse_only_arguments
                  else
                    Node::Empty.new
                  end

      filter = get_filter(token.value, token.lineno)
      node = filter.node_class.new(node, filter, arguments, token.lineno)

      unless parser.stream.test(Token::PUNCTUATION_TYPE, '|')
        break
      end

      parser.stream.next
    end

    node
  end

  def parse_arguments
    raise NotImplementedError
  end

  # @return [Node::Nodes]
  def parse_assignment_expression
    stream = parser.stream
    targets = AutoHash.new

    loop do
      token = parser.current_token

      if stream.test(Token::OPERATOR_TYPE) && token.value.match(Lexer::REGEX_NAME)
        # in this context, string operators are variables names
        parser.stream.next
      else
        stream.expect(Token::NAME_TYPE, nil, 'Only variables can be assigned to')
      end

      targets << Node::Expression::Variable::AssignContext.new(token.value, token.lineno)

      unless stream.next_if(Token::PUNCTUATION_TYPE, ',')
        break
      end
    end

    Node::Nodes.new(targets)
  end

  def parse_string_expression
    stream = parser.stream
    nodes = []

    # a string cannot be followed by another string in a single expression
    next_can_be_string = true

    loop do
      if next_can_be_string && (token = stream.next_if(Token::STRING_TYPE))
        nodes << Node::Expression::Constant.new(token.value, token.lineno)
        next_can_be_string = false
      elsif stream.next_if(Token::INTERPOLATION_START_TYPE)
        nodes << parse_expression
        stream.expect(Token::INTERPOLATION_END_TYPE)
        next_can_be_string = true
      else
        break
      end
    end

    expr = nodes.shift
    nodes.each do |node|
      expr = Node::Expression::Binary::Concat(expr, node, node.lineno)
    end

    expr
  end

  # @param [Node::Base]
  # @return [Node::Expression::Base]
  def parse_subscript_expression_array(node)
    stream = parser.stream
    token = stream.current
    lineno = token.lineno
    arguments = Node::Expression::Array.new({}, lineno)

    slice = false
    if stream.test(Token::PUNCTUATION_TYPE, ':')
      slice = true
      attribute = Node::Expression::Constant.new(0, token.lineno)
    else
      attribute = parse_expression
    end

    if stream.next_if(Token::PUNCTUATION_TYPE, ':')
      slice = true
    end

    if slice
      length = if stream.test(Token::PUNCTUATION_TYPE, ']')
                 Node::Expression::Constant.new(nil, token.lineno)
               else
                 parse_expression
               end

      filter = get_filter('slice', token.lineno)
      arguments = Node::Nodes.new(AutoHash.new.add(attribute, length))
      filter = filter.node_class.new(node, filter, arguments, token.lineno)

      stream.expect(Token::PUNCTUATION_TYPE, ']')

      return filter
    end

    stream.expect(Token::PUNCTUATION_TYPE, ']')

    Node::Expression::GetAttribute.new(node, attribute, arguments, Template::ARRAY_CALL, lineno)
  end

  # @return [Hash]
  def unary_operators
    @unary_operators ||= environment.operators[0]
  end

  # @return [Hash]
  def binary_operators
    @binary_operators ||= environment.operators[1]
  end

  # @param [Token] token
  def unary?(token)
    token.test(Token::OPERATOR_TYPE) && unary_operators.key?(token.value.to_sym)
  end

  # @param [Token] token
  def binary?(token)
    token.test(Token::OPERATOR_TYPE) && binary_operators.key?(token.value.to_sym)
  end

  # @return [Filter]
  def get_filter(name, lineno)
    unless (filter = environment.filter(name))
      raise Error::Syntax.new("Unknown '#{name}' filter", lineno, parser.stream.source)
    end

    filter
  end
end

#parserParser (readonly)

Returns:



317
318
319
# File 'lib/twig/expression_parser.rb', line 317

def parser
  @parser
end

Instance Method Details

#binary?(token) ⇒ Boolean

Parameters:

Returns:

  • (Boolean)


504
505
506
# File 'lib/twig/expression_parser.rb', line 504

def binary?(token)
  token.test(Token::OPERATOR_TYPE) && binary_operators.key?(token.value.to_sym)
end

#binary_operatorsHash

Returns:

  • (Hash)


494
495
496
# File 'lib/twig/expression_parser.rb', line 494

def binary_operators
  @binary_operators ||= environment.operators[1]
end

#get_filter(name, lineno) ⇒ Filter

Returns:

  • (Filter)


509
510
511
512
513
514
515
# File 'lib/twig/expression_parser.rb', line 509

def get_filter(name, lineno)
  unless (filter = environment.filter(name))
    raise Error::Syntax.new("Unknown '#{name}' filter", lineno, parser.stream.source)
  end

  filter
end

#get_function_node(name, line) ⇒ Object



251
252
253
254
255
256
257
258
259
260
# File 'lib/twig/expression_parser.rb', line 251

def get_function_node(name, line)
  # @todo lots of stuff in this method
  args = parse_only_arguments

  if environment.helper_method?(name.to_sym)
    Node::Expression::HelperMethod.new(name, args, line)
  else
    raise Error::Syntax.new("Unknown function '#{name}'", line, parser.stream.source)
  end
end

#parse_argumentsObject

Raises:

  • (NotImplementedError)


390
391
392
# File 'lib/twig/expression_parser.rb', line 390

def parse_arguments
  raise NotImplementedError
end

#parse_assignment_expressionNode::Nodes

Returns:



395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
# File 'lib/twig/expression_parser.rb', line 395

def parse_assignment_expression
  stream = parser.stream
  targets = AutoHash.new

  loop do
    token = parser.current_token

    if stream.test(Token::OPERATOR_TYPE) && token.value.match(Lexer::REGEX_NAME)
      # in this context, string operators are variables names
      parser.stream.next
    else
      stream.expect(Token::NAME_TYPE, nil, 'Only variables can be assigned to')
    end

    targets << Node::Expression::Variable::AssignContext.new(token.value, token.lineno)

    unless stream.next_if(Token::PUNCTUATION_TYPE, ',')
      break
    end
  end

  Node::Nodes.new(targets)
end

#parse_expression(precedence = 0) ⇒ Node::Expression::Base



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/twig/expression_parser.rb', line 20

def parse_expression(precedence = 0)
  # @todo parse arrow

  expr = primary
  token = parser.current_token

  while binary?(token) && binary_operators[token.value.to_sym][:precedence] >= precedence
    operator = binary_operators[token.value.to_sym]
    parser.stream.next

    next_precedence = operator[:precedence]
    next_precedence += 1 if operator[:associativity] == OPERATOR_LEFT

    expr1 = parse_expression(next_precedence)

    # @type [Node::Expression::Binary::Base]
    expr = operator[:class].new(expr, expr1, token.lineno)
    expr.attributes[:operator] = "binary_#{token.value}"

    token = parser.current_token
  end

  return parse_ternary_expression(expr) if precedence.zero?

  expr
end

#parse_filter_expression(node) ⇒ Object



361
362
363
364
365
# File 'lib/twig/expression_parser.rb', line 361

def parse_filter_expression(node)
  parser.stream.next

  parse_filter_expression_raw(node)
end

#parse_filter_expression_raw(node) ⇒ Object



367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/twig/expression_parser.rb', line 367

def parse_filter_expression_raw(node)
  loop do
    token = parser.stream.expect(Token::NAME_TYPE)

    arguments = if parser.stream.test(Token::PUNCTUATION_TYPE, '(')
                  parse_only_arguments
                else
                  Node::Empty.new
                end

    filter = get_filter(token.value, token.lineno)
    node = filter.node_class.new(node, filter, arguments, token.lineno)

    unless parser.stream.test(Token::PUNCTUATION_TYPE, '|')
      break
    end

    parser.stream.next
  end

  node
end

#parse_mapping_expressionObject



182
183
184
185
186
187
188
189
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
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/twig/expression_parser.rb', line 182

def parse_mapping_expression
  stream = parser.stream
  stream.expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected')

  node = Node::Expression::Array.new({}, stream.current.lineno)
  first = true

  until stream.test(Token::PUNCTUATION_TYPE, '}')
    unless first
      stream.expect(Token::PUNCTUATION_TYPE, ',', 'A mapping value must be followed by a comma')

      # trailing comma
      if stream.test(Token::PUNCTUATION_TYPE, '}')
        break
      end
    end

    first = false

    if stream.next_if(Token::SPREAD_TYPE)
      value = parse_expression
      value.attributes[:spread] = true
      node.add_element(value)

      next
    end

    # a mapping key can be:
    #
    #  * a number -- 12
    #  * a string -- 'a'
    #  * a name, which is equivalent to a symbol -- a
    #  * an expression, which must be enclosed in parentheses -- (1 + 2)
    if (token = stream.next_if(Token::NAME_TYPE))
      key = Node::Expression::Constant.new(token.value.to_sym, token.lineno)

      # {a} is a shortcut for {a: a}
      if stream.test(Token::PUNCTUATION_TYPE, %w[, }])
        value = Node::Expression::Variable::Context.new(key.attributes[:value], key.lineno)
        node.add_element(value, key.to_sym)

        next
      end
    elsif (token = stream.next_if(Token::STRING_TYPE)) || (token = stream.next_if(Token::NUMBER_TYPE))
      key = Node::Expression::Constant.new(token.value, token.lineno)
    elsif stream.test(Token::PUNCTUATION_TYPE, '(')
      key = parse_expression
    else
      current = stream.current

      raise Error::Syntax.new(
        'A mapping key must be a quoted string, number, name, or expression in parentheses ' \
        "expected token '#{current.type}' of value '#{current.value}'",
        current.lineno,
        stream.source
      )
    end

    stream.expect(Token::PUNCTUATION_TYPE, ':', 'A mapping key must be followed by a colon (:)')
    value = parse_expression

    node.add_element(value, key)
  end

  stream.expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed')

  node
end

#parse_only_argumentsObject



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
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
# File 'lib/twig/expression_parser.rb', line 262

def parse_only_arguments
  args = AutoHash.new
  stream = parser.stream
  stream.expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis')
  has_spread = false

  until stream.test(Token::PUNCTUATION_TYPE, ')')
    unless args.empty?
      stream.expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma')

      # if above was trailing comma, exit early
      break if stream.test(Token::PUNCTUATION_TYPE, ')')
    end

    if stream.next_if(Token::SPREAD_TYPE)
      has_spread = true
      value = Node::Expression::Unary::Spread.new(parse_expression, stream.current.lineno)
    elsif has_spread
      raise Error::Syntax.new(
        'Normal arguments must be placed before argument unpacking.',
        stream.current.lineno,
        stream.source
      )
    else
      value = parse_expression
    end

    name = nil
    if (token = stream.next_if(Token::OPERATOR_TYPE, '=')) ||
       (token = stream.next_if(Token::PUNCTUATION_TYPE, ':'))
      unless value.class <= Node::Expression::Name
        raise Error::Syntax.new(
          "A parameter name must be a string, #{value.class.name} given.",
          token.lineno,
          stream.source
        )
      end

      name = value.attributes[:name]
      value = parse_expression
    end

    if name.nil?
      args.add(value)
    else
      args[name] = value
    end
  end

  stream.expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis')

  Node::Nodes.new(args)
end

#parse_post_fix_expression(node) ⇒ Node::Expression::Base

Parameters:

Returns:



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/twig/expression_parser.rb', line 106

def parse_post_fix_expression(node)
  loop do
    token = parser.current_token

    unless token.type == Token::PUNCTUATION_TYPE
      break
    end

    case token.value
    when '.', '['
      node = parse_subscript_expression(node)
    when '|'
      node = parse_filter_expression(node)
    else
      break
    end
  end

  node
end

#parse_primary_expressionNode::Expression::Base



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/twig/expression_parser.rb', line 48

def parse_primary_expression
  token = parser.current_token

  case token.type
  when Token::SYMBOL_TYPE
    parser.stream.next

    node = Node::Expression::Constant.new(token.value.to_sym, token.lineno)
  when Token::NAME_TYPE
    parser.stream.next

    node = case token.value
           when 'true', 'TRUE'
             Node::Expression::Constant.new(true, token.lineno)
           when 'false', 'FALSE'
             Node::Expression::Constant.new(false, token.lineno)
           when 'null', 'NULL', 'nil'
             Node::Expression::Constant.new(nil, token.lineno)
           else
             if parser.current_token.value == '('
               get_function_node(token.value, token.lineno)
             else
               # @todo should be a context variable
               Node::Expression::Name.new(token.value, token.lineno)
             end
           end
  when Token::NUMBER_TYPE
    parser.stream.next

    node = Node::Expression::Constant.new(token.value, token.lineno)
  when Token::STRING_TYPE, Token::INTERPOLATION_START_TYPE
    node = parse_string_expression
  when Token::PUNCTUATION_TYPE
    case token.value
    when '['
      node = parse_sequence_expression
    when '{'
      node = parse_mapping_expression
    else
      Error::Syntax.new(
        "Unexpected token #{token.type} of value #{token.value}",
        token.lineno,
        parser.stream.source
      )
    end
  else
    raise Error::Syntax.new(
      "Unexpected token '#{token.type}' of value '#{token.value}'",
      token.lineno,
      parser.stream.source
    )
  end

  parse_post_fix_expression(node)
end

#parse_sequence_expressionObject



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/twig/expression_parser.rb', line 149

def parse_sequence_expression
  stream = parser.stream
  stream.expect(Token::PUNCTUATION_TYPE, '[', 'A sequence element was expected')

  node = Node::Expression::Array.new({}, stream.current.lineno)
  first = true

  # raise stream.debug

  until stream.test(Token::PUNCTUATION_TYPE, ']')
    unless first
      stream.expect(Token::PUNCTUATION_TYPE, ',', 'A sequence element must be followed by a comma')

      # trailing comma
      break if stream.test(Token::PUNCTUATION_TYPE, ']')
    end

    first = false

    if stream.next_if(Token::SPREAD_TYPE)
      expr = parse_expression
      expr.attributes[:spread] = true
      node.add_element(expr)
    else
      node.add_element(parse_expression)
    end
  end

  stream.expect(Token::PUNCTUATION_TYPE, ']', 'An opened sequence is not properly closed')

  node
end

#parse_string_expressionObject



419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
# File 'lib/twig/expression_parser.rb', line 419

def parse_string_expression
  stream = parser.stream
  nodes = []

  # a string cannot be followed by another string in a single expression
  next_can_be_string = true

  loop do
    if next_can_be_string && (token = stream.next_if(Token::STRING_TYPE))
      nodes << Node::Expression::Constant.new(token.value, token.lineno)
      next_can_be_string = false
    elsif stream.next_if(Token::INTERPOLATION_START_TYPE)
      nodes << parse_expression
      stream.expect(Token::INTERPOLATION_END_TYPE)
      next_can_be_string = true
    else
      break
    end
  end

  expr = nodes.shift
  nodes.each do |node|
    expr = Node::Expression::Binary::Concat(expr, node, node.lineno)
  end

  expr
end

#parse_subscript_expression(node) ⇒ Object



127
128
129
130
131
132
133
# File 'lib/twig/expression_parser.rb', line 127

def parse_subscript_expression(node)
  if parser.stream.next.value == '.'
    return parse_subscript_expression_dot(node)
  end

  parse_subscript_expression_array(node)
end

#parse_subscript_expression_array(node) ⇒ Node::Expression::Base

Parameters:

Returns:



449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
# File 'lib/twig/expression_parser.rb', line 449

def parse_subscript_expression_array(node)
  stream = parser.stream
  token = stream.current
  lineno = token.lineno
  arguments = Node::Expression::Array.new({}, lineno)

  slice = false
  if stream.test(Token::PUNCTUATION_TYPE, ':')
    slice = true
    attribute = Node::Expression::Constant.new(0, token.lineno)
  else
    attribute = parse_expression
  end

  if stream.next_if(Token::PUNCTUATION_TYPE, ':')
    slice = true
  end

  if slice
    length = if stream.test(Token::PUNCTUATION_TYPE, ']')
               Node::Expression::Constant.new(nil, token.lineno)
             else
               parse_expression
             end

    filter = get_filter('slice', token.lineno)
    arguments = Node::Nodes.new(AutoHash.new.add(attribute, length))
    filter = filter.node_class.new(node, filter, arguments, token.lineno)

    stream.expect(Token::PUNCTUATION_TYPE, ']')

    return filter
  end

  stream.expect(Token::PUNCTUATION_TYPE, ']')

  Node::Expression::GetAttribute.new(node, attribute, arguments, Template::ARRAY_CALL, lineno)
end

#parse_subscript_expression_dot(node) ⇒ Object



135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/twig/expression_parser.rb', line 135

def parse_subscript_expression_dot(node)
  stream = parser.stream
  token = stream.current
  lineno = token.lineno
  arguments = Node::Expression::Array.new({}, lineno)
  token = stream.next

  if token.type == Token::NAME_TYPE
    attribute = Node::Expression::Constant.new(token.value, token.lineno)
  end

  Node::Expression::GetAttribute.new(node, attribute, arguments, nil, token.lineno)
end

#parse_ternary_expression(expr) ⇒ Node::Expression

Parameters:

Returns:



346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/twig/expression_parser.rb', line 346

def parse_ternary_expression(expr)
  while parser.stream.next_if(Token::PUNCTUATION_TYPE, '?')
    expr2 = parse_expression
    expr3 = if parser.stream.next_if(Token::PUNCTUATION_TYPE, ':')
              parse_expression
            else
              Node::Expression::Constant.new('', parser.current_token.lineno)
            end

    expr = Node::Expression::Ternary.new(expr, expr2, expr3, parser.current_token.lineno)
  end

  expr
end

#primaryNode::Expression::Base



320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/twig/expression_parser.rb', line 320

def primary
  token = parser.current_token

  if unary?(token)
    operator = unary_operators[token.value.to_sym]
    parser.stream.next

    expr = parse_expression(operator[:precedence])
    expr = operator[:class].new(expr, token.lineno)
    expr.attributes[:operator] = "unary_#{token.value}"

    return parse_post_fix_expression(expr)
  elsif token.test(Token::PUNCTUATION_TYPE, '(')
    parser.stream.next
    expr = parse_expression.set_explicit_parentheses

    parser.stream.expect(Token::PUNCTUATION_TYPE, ')', 'Open parenthesis not closed')

    return parse_post_fix_expression(expr)
  end

  parse_primary_expression
end

#unary?(token) ⇒ Boolean

Parameters:

Returns:

  • (Boolean)


499
500
501
# File 'lib/twig/expression_parser.rb', line 499

def unary?(token)
  token.test(Token::OPERATOR_TYPE) && unary_operators.key?(token.value.to_sym)
end

#unary_operatorsHash

Returns:

  • (Hash)


489
490
491
# File 'lib/twig/expression_parser.rb', line 489

def unary_operators
  @unary_operators ||= environment.operators[0]
end