ludtwig_parser/grammar/twig/
literal.rs

1use crate::grammar::parse_many;
2use crate::grammar::twig::expression::{parse_twig_expression, TWIG_EXPRESSION_RECOVERY_SET};
3use crate::parser::event::CompletedMarker;
4use crate::parser::{ParseErrorBuilder, Parser};
5use crate::syntax::untyped::SyntaxKind;
6use crate::T;
7use regex::Regex;
8use std::sync::LazyLock;
9
10// TODO: maybe allow more here to partly support twig.js. Needs testing on real world templates
11pub static TWIG_NAME_REGEX: LazyLock<Regex> =
12    LazyLock::new(|| Regex::new(r"^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$").unwrap());
13
14pub(crate) fn parse_twig_literal(parser: &mut Parser) -> Option<CompletedMarker> {
15    if parser.at(T![number]) {
16        Some(parse_twig_number(parser))
17    } else if parser.at_set(&[T!["\""], T!["'"]]) {
18        Some(parse_twig_string(parser, true))
19    } else if parser.at(T!["["]) {
20        Some(parse_twig_array(parser))
21    } else if parser.at(T!["null"]) {
22        Some(parse_twig_null(parser))
23    } else if parser.at_set(&[T!["true"], T!["false"]]) {
24        Some(parse_twig_boolean(parser))
25    } else if parser.at(T!["{"]) {
26        Some(parse_twig_hash(parser))
27    } else {
28        // literal name
29        let mut node = parse_twig_name(parser)?;
30
31        // check for optional function call or arrow function
32        if parser.at(T!["("]) {
33            node = parse_twig_function(parser, node);
34        } else if parser.at(T!["=>"]) {
35            // wrap literal name node
36            let m = parser.precede(node);
37            let last_node = parser.complete(m, SyntaxKind::TWIG_ARGUMENTS);
38            node = parse_twig_arrow_function(parser, last_node);
39        }
40
41        Some(node)
42    }
43}
44
45/// Parses any amount of postfix operators like accessors (.), indexer ([]), function `calls()` or filters(|)
46pub(crate) fn parse_postfix_operators(
47    parser: &mut Parser,
48    mut node: CompletedMarker,
49) -> CompletedMarker {
50    parse_many(
51        parser,
52        |_| false,
53        |p| {
54            if p.at(T!["."]) {
55                node = parse_twig_accessor(p, node.clone());
56            } else if p.at(T!["["]) {
57                node = parse_twig_indexer(p, node.clone());
58            } else if p.at(T!["|"]) {
59                node = parse_twig_filter(p, node.clone());
60            }
61        },
62    );
63
64    node
65}
66
67fn parse_twig_number(parser: &mut Parser) -> CompletedMarker {
68    debug_assert!(parser.at(T![number]));
69    let m = parser.start();
70    parser.bump();
71
72    parser.complete(m, SyntaxKind::TWIG_LITERAL_NUMBER)
73}
74
75pub(crate) fn parse_twig_string(
76    parser: &mut Parser,
77    mut interpolation_allowed: bool,
78) -> CompletedMarker {
79    debug_assert!(parser.at_set(&[T!["\""], T!["'"]]));
80    let m = parser.start();
81    let starting_quote_token = parser.bump();
82    let quote_kind = starting_quote_token.kind;
83    interpolation_allowed = match quote_kind {
84        T!["\""] => interpolation_allowed,
85        _ => false, // interpolation only allowed in double quoted strings,
86    };
87
88    let m_inner = parser.start();
89    parse_many(
90        parser,
91        |p| p.at(quote_kind),
92        |p| {
93            if p.at_following(&[T!["\\"], quote_kind]) {
94                // escaped quote should be consumed
95                p.bump();
96                p.bump();
97            } else if p.at_following(&[T!["#"], T!["{"]]) {
98                if !interpolation_allowed {
99                    let opening_token = p.bump();
100                    let interpolation_error = ParseErrorBuilder::new(
101                        "no string interpolation, because it isn't allowed here",
102                    )
103                    .at_token(opening_token);
104                    p.add_error(interpolation_error);
105                    return;
106                }
107
108                // found twig expression in string (string interpolation)
109                p.explicitly_consume_trivia(); // keep trivia out of interpolation node (its part of the raw string)
110                let interpolation_m = p.start();
111                p.bump(); // bump both starting tokens
112                p.bump();
113                if parse_twig_expression(p).is_none() {
114                    p.add_error(ParseErrorBuilder::new("twig expression"));
115                }
116                p.expect(T!["}"], TWIG_EXPRESSION_RECOVERY_SET);
117                p.complete(
118                    interpolation_m,
119                    SyntaxKind::TWIG_LITERAL_STRING_INTERPOLATION,
120                );
121            } else {
122                // bump the token inside the string
123                p.bump();
124            }
125        },
126    );
127    parser.explicitly_consume_trivia(); // consume any trailing trivia inside the string
128    parser.complete(m_inner, SyntaxKind::TWIG_LITERAL_STRING_INNER);
129
130    parser.expect(quote_kind, TWIG_EXPRESSION_RECOVERY_SET);
131    parser.complete(m, SyntaxKind::TWIG_LITERAL_STRING)
132}
133
134fn parse_twig_array(parser: &mut Parser) -> CompletedMarker {
135    debug_assert!(parser.at(T!["["]));
136    let m = parser.start();
137    parser.bump();
138
139    // parse any amount of array values
140    let list_m = parser.start();
141    parse_many(
142        parser,
143        |p| p.at(T!["]"]),
144        |p| {
145            parse_twig_expression(p);
146
147            if p.at(T![","]) {
148                // consume separator
149                p.bump();
150            } else if !p.at(T!["]"]) {
151                p.add_error(ParseErrorBuilder::new(","));
152            }
153        },
154    );
155    parser.complete(list_m, SyntaxKind::TWIG_LITERAL_ARRAY_INNER);
156
157    parser.expect(T!["]"], TWIG_EXPRESSION_RECOVERY_SET);
158    parser.complete(m, SyntaxKind::TWIG_LITERAL_ARRAY)
159}
160
161fn parse_twig_null(parser: &mut Parser) -> CompletedMarker {
162    debug_assert!(parser.at(T!["null"]));
163    let m = parser.start();
164    parser.bump();
165
166    parser.complete(m, SyntaxKind::TWIG_LITERAL_NULL)
167}
168
169fn parse_twig_boolean(parser: &mut Parser) -> CompletedMarker {
170    debug_assert!(parser.at_set(&[T!["true"], T!["false"]]));
171    let m = parser.start();
172    parser.bump();
173
174    parser.complete(m, SyntaxKind::TWIG_LITERAL_BOOLEAN)
175}
176
177pub(crate) fn parse_twig_hash(parser: &mut Parser) -> CompletedMarker {
178    debug_assert!(parser.at(T!["{"]));
179    let m = parser.start();
180    parser.bump();
181
182    // parse any amount of hash pairs
183    let list_m = parser.start();
184    parse_many(
185        parser,
186        |p| p.at(T!["}"]),
187        |p| {
188            parse_twig_hash_pair(p);
189
190            if p.at(T![","]) {
191                // consume separator
192                p.bump();
193            } else if !p.at(T!["}"]) {
194                p.add_error(ParseErrorBuilder::new(","));
195            }
196        },
197    );
198    parser.complete(list_m, SyntaxKind::TWIG_LITERAL_HASH_ITEMS);
199
200    parser.expect(T!["}"], TWIG_EXPRESSION_RECOVERY_SET);
201    parser.complete(m, SyntaxKind::TWIG_LITERAL_HASH)
202}
203
204fn parse_twig_hash_pair(parser: &mut Parser) -> Option<CompletedMarker> {
205    let key = if parser.at(T![number]) {
206        let m = parse_twig_number(parser);
207        let preceded = parser.precede(m);
208        parser.complete(preceded, SyntaxKind::TWIG_LITERAL_HASH_KEY)
209    } else if parser.at_set(&[T!["'"], T!["\""]]) {
210        let m = parse_twig_string(parser, false); // no interpolation in keys
211        let preceded = parser.precede(m);
212        parser.complete(preceded, SyntaxKind::TWIG_LITERAL_HASH_KEY)
213    } else if parser.at(T!["("]) {
214        let m = parser.start();
215        parser.bump();
216        if parse_twig_expression(parser).is_none() {
217            parser.add_error(ParseErrorBuilder::new("twig expression"));
218            parser.recover(TWIG_EXPRESSION_RECOVERY_SET);
219        }
220        parser.expect(T![")"], TWIG_EXPRESSION_RECOVERY_SET);
221        parser.complete(m, SyntaxKind::TWIG_LITERAL_HASH_KEY)
222    } else {
223        let token_text = parser.peek_token()?.text;
224        if TWIG_NAME_REGEX.is_match(token_text) {
225            let m = parser.start();
226            parser.bump_as(SyntaxKind::TK_WORD);
227            parser.complete(m, SyntaxKind::TWIG_LITERAL_HASH_KEY)
228        } else {
229            return None;
230        }
231    };
232
233    // check if key exists
234    if parser.at(T![":"]) {
235        parser.bump();
236        if parse_twig_expression(parser).is_none() {
237            parser.add_error(ParseErrorBuilder::new("value as twig expression"));
238            parser.recover(TWIG_EXPRESSION_RECOVERY_SET);
239        }
240    }
241
242    let preceded = parser.precede(key);
243    Some(parser.complete(preceded, SyntaxKind::TWIG_LITERAL_HASH_PAIR))
244}
245
246pub(crate) fn parse_twig_filter(
247    parser: &mut Parser,
248    mut last_node: CompletedMarker,
249) -> CompletedMarker {
250    debug_assert!(parser.at(T!["|"]));
251
252    // wrap last_node in an operand and create outer marker
253    let m = parser.precede(last_node);
254    last_node = parser.complete(m, SyntaxKind::TWIG_OPERAND);
255    let outer = parser.precede(last_node);
256
257    // bump the operator
258    parser.bump();
259
260    // parse the rhs and wrap it also in an operand
261    let m = parser.start();
262    if parse_twig_name(parser).is_none() {
263        parser.add_error(ParseErrorBuilder::new("twig filter"));
264        parser.recover(TWIG_EXPRESSION_RECOVERY_SET);
265    } else if parser.at(T!["("]) {
266        // parse any amount of arguments
267        let arguments_m = parser.start();
268        parser.bump();
269        parse_many(
270            parser,
271            |p| p.at(T![")"]),
272            |p| {
273                parse_twig_function_argument(p);
274                if p.at(T![","]) {
275                    p.bump();
276                } else if !p.at(T![")"]) {
277                    p.add_error(ParseErrorBuilder::new(","));
278                }
279            },
280        );
281        parser.expect(T![")"], TWIG_EXPRESSION_RECOVERY_SET);
282        parser.complete(arguments_m, SyntaxKind::TWIG_ARGUMENTS);
283    }
284    parser.complete(m, SyntaxKind::TWIG_OPERAND);
285
286    // complete the outer marker
287    parser.complete(outer, SyntaxKind::TWIG_FILTER)
288}
289
290fn parse_twig_indexer(parser: &mut Parser, mut last_node: CompletedMarker) -> CompletedMarker {
291    debug_assert!(parser.at(T!["["]));
292
293    // wrap last_node in an operand and create outer marker
294    let m = parser.precede(last_node);
295    last_node = parser.complete(m, SyntaxKind::TWIG_OPERAND);
296    let outer = parser.precede(last_node);
297
298    // bump the opening '['
299    parser.bump();
300
301    let index_m = parser.start();
302
303    // try to parse the first and maybe only expression
304    let missing_lower_slice_bound = parse_twig_expression(parser).is_none();
305
306    // look for range syntax and try to parse the second and maybe only expression
307    let mut is_slice = false;
308    let missing_upper_slice_bound = if parser.at(T![":"]) {
309        parser.bump();
310        is_slice = true; // now it is a slice instead of a simple lookup
311        parse_twig_expression(parser).is_none()
312    } else {
313        true
314    };
315
316    if missing_lower_slice_bound && missing_upper_slice_bound {
317        parser.add_error(ParseErrorBuilder::new("twig expression"));
318        parser.recover(TWIG_EXPRESSION_RECOVERY_SET);
319    }
320
321    parser.complete(
322        index_m,
323        if is_slice {
324            SyntaxKind::TWIG_INDEX_RANGE
325        } else {
326            SyntaxKind::TWIG_INDEX
327        },
328    );
329
330    parser.expect(T!["]"], TWIG_EXPRESSION_RECOVERY_SET);
331
332    // complete the outer marker
333    parser.complete(outer, SyntaxKind::TWIG_INDEX_LOOKUP)
334}
335
336fn parse_twig_accessor(parser: &mut Parser, mut last_node: CompletedMarker) -> CompletedMarker {
337    debug_assert!(parser.at(T!["."]));
338
339    // wrap last_node in an operand and create outer marker
340    let m = parser.precede(last_node);
341    last_node = parser.complete(m, SyntaxKind::TWIG_OPERAND);
342    let outer = parser.precede(last_node);
343
344    // bump the operator
345    parser.bump();
346
347    // parse the rhs and wrap it also in an operand
348    let m = parser.start();
349    if parser.at(T![number]) {
350        // found an array index lookup instead of name accessor!
351        let n = parse_twig_number(parser);
352        let n = parser.precede(n);
353        parser.complete(n, SyntaxKind::TWIG_EXPRESSION);
354        parser.complete(m, SyntaxKind::TWIG_INDEX);
355        let node = parser.complete(outer, SyntaxKind::TWIG_INDEX_LOOKUP);
356        return node;
357    } else if parse_twig_name(parser).is_none() {
358        parser.add_error(ParseErrorBuilder::new(
359            "twig variable property, key or method",
360        ));
361        parser.recover(TWIG_EXPRESSION_RECOVERY_SET);
362    }
363    parser.complete(m, SyntaxKind::TWIG_OPERAND);
364
365    // complete the outer marker
366    let mut node = parser.complete(outer, SyntaxKind::TWIG_ACCESSOR);
367
368    // check for optional function call
369    if parser.at(T!["("]) {
370        node = parse_twig_function(parser, node);
371    }
372
373    node
374}
375
376pub(crate) fn parse_twig_function(
377    parser: &mut Parser,
378    mut last_node: CompletedMarker,
379) -> CompletedMarker {
380    debug_assert!(parser.at(T!["("]));
381
382    // wrap last_node in an operand and create outer marker
383    let m = parser.precede(last_node);
384    last_node = parser.complete(m, SyntaxKind::TWIG_OPERAND);
385    let outer = parser.precede(last_node);
386
387    // parse any amount of arguments
388    let arguments_m = parser.start();
389    // bump the opening '('
390    parser.bump();
391    parse_many(
392        parser,
393        |p| p.at(T![")"]),
394        |p| {
395            parse_twig_function_argument(p);
396            if p.at(T![","]) {
397                p.bump();
398            } else if !p.at(T![")"]) {
399                p.add_error(ParseErrorBuilder::new(","));
400            }
401        },
402    );
403    parser.expect(T![")"], TWIG_EXPRESSION_RECOVERY_SET);
404    parser.complete(arguments_m, SyntaxKind::TWIG_ARGUMENTS);
405
406    // complete the outer marker
407    parser.complete(outer, SyntaxKind::TWIG_FUNCTION_CALL)
408}
409
410pub(crate) fn parse_twig_arrow_function(
411    parser: &mut Parser,
412    last_node: CompletedMarker,
413) -> CompletedMarker {
414    debug_assert!(parser.at(T!["=>"]));
415
416    let outer = parser.precede(last_node);
417
418    // bump the arrow
419    parser.bump();
420
421    // parse closure expression
422    if parse_twig_expression(parser).is_none() {
423        parser.add_error(ParseErrorBuilder::new(
424            "single twig expression as the body of the closure",
425        ));
426        parser.recover(TWIG_EXPRESSION_RECOVERY_SET);
427    }
428
429    // complete the outer marker
430    parser.complete(outer, SyntaxKind::TWIG_ARROW_FUNCTION)
431}
432
433pub(crate) fn parse_twig_function_argument(parser: &mut Parser) -> Option<CompletedMarker> {
434    // must be specific here with word followed by equal, because otherwise it could
435    // be a normal variable or another function call or something else..
436    if parser.at_following(&[T![word], T!["="]]) {
437        let named_arg_m = parser.start();
438        parser.bump();
439        parser.expect(T!["="], TWIG_EXPRESSION_RECOVERY_SET);
440        parse_twig_expression(parser);
441        Some(parser.complete(named_arg_m, SyntaxKind::TWIG_NAMED_ARGUMENT))
442    } else {
443        parse_twig_expression(parser)
444    }
445}
446
447pub(crate) fn parse_twig_name(parser: &mut Parser) -> Option<CompletedMarker> {
448    // special case to allow for 'same as' and 'divisible by' twig test ('is' / 'is not' operator)
449    let is_at_special = parser.at_set(&[T!["same as"], T!["divisible by"]]);
450    let token_text = parser.peek_token()?.text;
451    if !is_at_special && !TWIG_NAME_REGEX.is_match(token_text) {
452        return None;
453    }
454
455    let m = parser.start();
456    parser.bump_as(SyntaxKind::TK_WORD);
457    let m = parser.complete(m, SyntaxKind::TWIG_LITERAL_NAME);
458    Some(m)
459}
460
461#[cfg(test)]
462mod tests {
463    use expect_test::expect;
464
465    use crate::parser::check_parse;
466
467    #[test]
468    fn parse_twig_string_single_quotes() {
469        check_parse(
470            r#"{{ 'hel"lo world' }}"#,
471            expect![[r#"
472                ROOT@0..20
473                  TWIG_VAR@0..20
474                    TK_OPEN_CURLY_CURLY@0..2 "{{"
475                    TWIG_EXPRESSION@2..17
476                      TWIG_LITERAL_STRING@2..17
477                        TK_WHITESPACE@2..3 " "
478                        TK_SINGLE_QUOTES@3..4 "'"
479                        TWIG_LITERAL_STRING_INNER@4..16
480                          TK_WORD@4..7 "hel"
481                          TK_DOUBLE_QUOTES@7..8 "\""
482                          TK_WORD@8..10 "lo"
483                          TK_WHITESPACE@10..11 " "
484                          TK_WORD@11..16 "world"
485                        TK_SINGLE_QUOTES@16..17 "'"
486                    TK_WHITESPACE@17..18 " "
487                    TK_CLOSE_CURLY_CURLY@18..20 "}}""#]],
488        );
489    }
490
491    #[test]
492    fn parse_twig_string_double_quotes() {
493        check_parse(
494            r#"{{ "hel'lo world" }}"#,
495            expect![[r#"
496                ROOT@0..20
497                  TWIG_VAR@0..20
498                    TK_OPEN_CURLY_CURLY@0..2 "{{"
499                    TWIG_EXPRESSION@2..17
500                      TWIG_LITERAL_STRING@2..17
501                        TK_WHITESPACE@2..3 " "
502                        TK_DOUBLE_QUOTES@3..4 "\""
503                        TWIG_LITERAL_STRING_INNER@4..16
504                          TK_WORD@4..7 "hel"
505                          TK_SINGLE_QUOTES@7..8 "'"
506                          TK_WORD@8..10 "lo"
507                          TK_WHITESPACE@10..11 " "
508                          TK_WORD@11..16 "world"
509                        TK_DOUBLE_QUOTES@16..17 "\""
510                    TK_WHITESPACE@17..18 " "
511                    TK_CLOSE_CURLY_CURLY@18..20 "}}""#]],
512        );
513    }
514
515    #[test]
516    fn parse_twig_string_escaped_double_quotes() {
517        check_parse(
518            r#"{{ "hel\"lo world" }}"#,
519            expect![[r#"
520                ROOT@0..21
521                  TWIG_VAR@0..21
522                    TK_OPEN_CURLY_CURLY@0..2 "{{"
523                    TWIG_EXPRESSION@2..18
524                      TWIG_LITERAL_STRING@2..18
525                        TK_WHITESPACE@2..3 " "
526                        TK_DOUBLE_QUOTES@3..4 "\""
527                        TWIG_LITERAL_STRING_INNER@4..17
528                          TK_WORD@4..7 "hel"
529                          TK_BACKWARD_SLASH@7..8 "\\"
530                          TK_DOUBLE_QUOTES@8..9 "\""
531                          TK_WORD@9..11 "lo"
532                          TK_WHITESPACE@11..12 " "
533                          TK_WORD@12..17 "world"
534                        TK_DOUBLE_QUOTES@17..18 "\""
535                    TK_WHITESPACE@18..19 " "
536                    TK_CLOSE_CURLY_CURLY@19..21 "}}""#]],
537        );
538    }
539
540    #[test]
541    fn parse_twig_string_with_leading_and_trailing_trivia() {
542        check_parse(
543            r#"{{ " , " }}"#,
544            expect![[r#"
545                ROOT@0..11
546                  TWIG_VAR@0..11
547                    TK_OPEN_CURLY_CURLY@0..2 "{{"
548                    TWIG_EXPRESSION@2..8
549                      TWIG_LITERAL_STRING@2..8
550                        TK_WHITESPACE@2..3 " "
551                        TK_DOUBLE_QUOTES@3..4 "\""
552                        TWIG_LITERAL_STRING_INNER@4..7
553                          TK_WHITESPACE@4..5 " "
554                          TK_COMMA@5..6 ","
555                          TK_WHITESPACE@6..7 " "
556                        TK_DOUBLE_QUOTES@7..8 "\""
557                    TK_WHITESPACE@8..9 " "
558                    TK_CLOSE_CURLY_CURLY@9..11 "}}""#]],
559        );
560    }
561
562    #[test]
563    fn parse_twig_string_interpolation() {
564        check_parse(
565            r#"{{ "foo #{1 + 2} baz" }}"#,
566            expect![[r##"
567                ROOT@0..24
568                  TWIG_VAR@0..24
569                    TK_OPEN_CURLY_CURLY@0..2 "{{"
570                    TWIG_EXPRESSION@2..21
571                      TWIG_LITERAL_STRING@2..21
572                        TK_WHITESPACE@2..3 " "
573                        TK_DOUBLE_QUOTES@3..4 "\""
574                        TWIG_LITERAL_STRING_INNER@4..20
575                          TK_WORD@4..7 "foo"
576                          TK_WHITESPACE@7..8 " "
577                          TWIG_LITERAL_STRING_INTERPOLATION@8..16
578                            TK_HASHTAG@8..9 "#"
579                            TK_OPEN_CURLY@9..10 "{"
580                            TWIG_EXPRESSION@10..15
581                              TWIG_BINARY_EXPRESSION@10..15
582                                TWIG_EXPRESSION@10..11
583                                  TWIG_LITERAL_NUMBER@10..11
584                                    TK_NUMBER@10..11 "1"
585                                TK_WHITESPACE@11..12 " "
586                                TK_PLUS@12..13 "+"
587                                TWIG_EXPRESSION@13..15
588                                  TWIG_LITERAL_NUMBER@13..15
589                                    TK_WHITESPACE@13..14 " "
590                                    TK_NUMBER@14..15 "2"
591                            TK_CLOSE_CURLY@15..16 "}"
592                          TK_WHITESPACE@16..17 " "
593                          TK_WORD@17..20 "baz"
594                        TK_DOUBLE_QUOTES@20..21 "\""
595                    TK_WHITESPACE@21..22 " "
596                    TK_CLOSE_CURLY_CURLY@22..24 "}}""##]],
597        );
598    }
599
600    #[test]
601    fn parse_twig_string_interpolation_missing_expression() {
602        check_parse(
603            r#"{{ "foo #{ } baz" }}"#,
604            expect![[r##"
605                ROOT@0..20
606                  TWIG_VAR@0..20
607                    TK_OPEN_CURLY_CURLY@0..2 "{{"
608                    TWIG_EXPRESSION@2..17
609                      TWIG_LITERAL_STRING@2..17
610                        TK_WHITESPACE@2..3 " "
611                        TK_DOUBLE_QUOTES@3..4 "\""
612                        TWIG_LITERAL_STRING_INNER@4..16
613                          TK_WORD@4..7 "foo"
614                          TK_WHITESPACE@7..8 " "
615                          TWIG_LITERAL_STRING_INTERPOLATION@8..12
616                            TK_HASHTAG@8..9 "#"
617                            TK_OPEN_CURLY@9..10 "{"
618                            TK_WHITESPACE@10..11 " "
619                            TK_CLOSE_CURLY@11..12 "}"
620                          TK_WHITESPACE@12..13 " "
621                          TK_WORD@13..16 "baz"
622                        TK_DOUBLE_QUOTES@16..17 "\""
623                    TK_WHITESPACE@17..18 " "
624                    TK_CLOSE_CURLY_CURLY@18..20 "}}"
625                error at 11..12: expected twig expression but found }"##]],
626        );
627    }
628
629    #[test]
630    fn parse_twig_integer_number() {
631        check_parse(
632            "{{ 42 }}",
633            expect![[r#"
634                ROOT@0..8
635                  TWIG_VAR@0..8
636                    TK_OPEN_CURLY_CURLY@0..2 "{{"
637                    TWIG_EXPRESSION@2..5
638                      TWIG_LITERAL_NUMBER@2..5
639                        TK_WHITESPACE@2..3 " "
640                        TK_NUMBER@3..5 "42"
641                    TK_WHITESPACE@5..6 " "
642                    TK_CLOSE_CURLY_CURLY@6..8 "}}""#]],
643        );
644    }
645
646    #[test]
647    fn parse_twig_floating_point_number() {
648        check_parse(
649            "{{ 0.3337 }}",
650            expect![[r#"
651                ROOT@0..12
652                  TWIG_VAR@0..12
653                    TK_OPEN_CURLY_CURLY@0..2 "{{"
654                    TWIG_EXPRESSION@2..9
655                      TWIG_LITERAL_NUMBER@2..9
656                        TK_WHITESPACE@2..3 " "
657                        TK_NUMBER@3..9 "0.3337"
658                    TK_WHITESPACE@9..10 " "
659                    TK_CLOSE_CURLY_CURLY@10..12 "}}""#]],
660        );
661    }
662
663    #[test]
664    fn parse_twig_number_array() {
665        check_parse(
666            "{{ [1, 2, 3] }}",
667            expect![[r#"
668                ROOT@0..15
669                  TWIG_VAR@0..15
670                    TK_OPEN_CURLY_CURLY@0..2 "{{"
671                    TWIG_EXPRESSION@2..12
672                      TWIG_LITERAL_ARRAY@2..12
673                        TK_WHITESPACE@2..3 " "
674                        TK_OPEN_SQUARE@3..4 "["
675                        TWIG_LITERAL_ARRAY_INNER@4..11
676                          TWIG_EXPRESSION@4..5
677                            TWIG_LITERAL_NUMBER@4..5
678                              TK_NUMBER@4..5 "1"
679                          TK_COMMA@5..6 ","
680                          TWIG_EXPRESSION@6..8
681                            TWIG_LITERAL_NUMBER@6..8
682                              TK_WHITESPACE@6..7 " "
683                              TK_NUMBER@7..8 "2"
684                          TK_COMMA@8..9 ","
685                          TWIG_EXPRESSION@9..11
686                            TWIG_LITERAL_NUMBER@9..11
687                              TK_WHITESPACE@9..10 " "
688                              TK_NUMBER@10..11 "3"
689                        TK_CLOSE_SQUARE@11..12 "]"
690                    TK_WHITESPACE@12..13 " "
691                    TK_CLOSE_CURLY_CURLY@13..15 "}}""#]],
692        );
693    }
694
695    #[test]
696    fn parse_twig_number_array_missing_comma() {
697        check_parse(
698            "{{ [1, 2 3] }}",
699            expect![[r#"
700                ROOT@0..14
701                  TWIG_VAR@0..14
702                    TK_OPEN_CURLY_CURLY@0..2 "{{"
703                    TWIG_EXPRESSION@2..11
704                      TWIG_LITERAL_ARRAY@2..11
705                        TK_WHITESPACE@2..3 " "
706                        TK_OPEN_SQUARE@3..4 "["
707                        TWIG_LITERAL_ARRAY_INNER@4..10
708                          TWIG_EXPRESSION@4..5
709                            TWIG_LITERAL_NUMBER@4..5
710                              TK_NUMBER@4..5 "1"
711                          TK_COMMA@5..6 ","
712                          TWIG_EXPRESSION@6..8
713                            TWIG_LITERAL_NUMBER@6..8
714                              TK_WHITESPACE@6..7 " "
715                              TK_NUMBER@7..8 "2"
716                          TWIG_EXPRESSION@8..10
717                            TWIG_LITERAL_NUMBER@8..10
718                              TK_WHITESPACE@8..9 " "
719                              TK_NUMBER@9..10 "3"
720                        TK_CLOSE_SQUARE@10..11 "]"
721                    TK_WHITESPACE@11..12 " "
722                    TK_CLOSE_CURLY_CURLY@12..14 "}}"
723                error at 9..10: expected , but found number"#]],
724        );
725    }
726
727    #[test]
728    fn parse_twig_string_array() {
729        check_parse(
730            r#"{{ ["hello", "trailing", "comma",] }}"#,
731            expect![[r#"
732                ROOT@0..37
733                  TWIG_VAR@0..37
734                    TK_OPEN_CURLY_CURLY@0..2 "{{"
735                    TWIG_EXPRESSION@2..34
736                      TWIG_LITERAL_ARRAY@2..34
737                        TK_WHITESPACE@2..3 " "
738                        TK_OPEN_SQUARE@3..4 "["
739                        TWIG_LITERAL_ARRAY_INNER@4..33
740                          TWIG_EXPRESSION@4..11
741                            TWIG_LITERAL_STRING@4..11
742                              TK_DOUBLE_QUOTES@4..5 "\""
743                              TWIG_LITERAL_STRING_INNER@5..10
744                                TK_WORD@5..10 "hello"
745                              TK_DOUBLE_QUOTES@10..11 "\""
746                          TK_COMMA@11..12 ","
747                          TWIG_EXPRESSION@12..23
748                            TWIG_LITERAL_STRING@12..23
749                              TK_WHITESPACE@12..13 " "
750                              TK_DOUBLE_QUOTES@13..14 "\""
751                              TWIG_LITERAL_STRING_INNER@14..22
752                                TK_WORD@14..22 "trailing"
753                              TK_DOUBLE_QUOTES@22..23 "\""
754                          TK_COMMA@23..24 ","
755                          TWIG_EXPRESSION@24..32
756                            TWIG_LITERAL_STRING@24..32
757                              TK_WHITESPACE@24..25 " "
758                              TK_DOUBLE_QUOTES@25..26 "\""
759                              TWIG_LITERAL_STRING_INNER@26..31
760                                TK_WORD@26..31 "comma"
761                              TK_DOUBLE_QUOTES@31..32 "\""
762                          TK_COMMA@32..33 ","
763                        TK_CLOSE_SQUARE@33..34 "]"
764                    TK_WHITESPACE@34..35 " "
765                    TK_CLOSE_CURLY_CURLY@35..37 "}}""#]],
766        );
767    }
768
769    #[test]
770    fn parse_twig_null() {
771        check_parse(
772            "{{ null }}",
773            expect![[r#"
774            ROOT@0..10
775              TWIG_VAR@0..10
776                TK_OPEN_CURLY_CURLY@0..2 "{{"
777                TWIG_EXPRESSION@2..7
778                  TWIG_LITERAL_NULL@2..7
779                    TK_WHITESPACE@2..3 " "
780                    TK_NULL@3..7 "null"
781                TK_WHITESPACE@7..8 " "
782                TK_CLOSE_CURLY_CURLY@8..10 "}}""#]],
783        );
784    }
785
786    #[test]
787    fn parse_twig_boolean_true() {
788        check_parse(
789            "{{ true }}",
790            expect![[r#"
791            ROOT@0..10
792              TWIG_VAR@0..10
793                TK_OPEN_CURLY_CURLY@0..2 "{{"
794                TWIG_EXPRESSION@2..7
795                  TWIG_LITERAL_BOOLEAN@2..7
796                    TK_WHITESPACE@2..3 " "
797                    TK_TRUE@3..7 "true"
798                TK_WHITESPACE@7..8 " "
799                TK_CLOSE_CURLY_CURLY@8..10 "}}""#]],
800        );
801    }
802
803    #[test]
804    fn parse_twig_boolean_false() {
805        check_parse(
806            "{{ false }}",
807            expect![[r#"
808            ROOT@0..11
809              TWIG_VAR@0..11
810                TK_OPEN_CURLY_CURLY@0..2 "{{"
811                TWIG_EXPRESSION@2..8
812                  TWIG_LITERAL_BOOLEAN@2..8
813                    TK_WHITESPACE@2..3 " "
814                    TK_FALSE@3..8 "false"
815                TK_WHITESPACE@8..9 " "
816                TK_CLOSE_CURLY_CURLY@9..11 "}}""#]],
817        );
818    }
819
820    #[test]
821    fn parse_twig_number_hash() {
822        check_parse(
823            "{{ { 1: 'hello', 2: 'world' } }}",
824            expect![[r#"
825                ROOT@0..32
826                  TWIG_VAR@0..32
827                    TK_OPEN_CURLY_CURLY@0..2 "{{"
828                    TWIG_EXPRESSION@2..29
829                      TWIG_LITERAL_HASH@2..29
830                        TK_WHITESPACE@2..3 " "
831                        TK_OPEN_CURLY@3..4 "{"
832                        TWIG_LITERAL_HASH_ITEMS@4..27
833                          TWIG_LITERAL_HASH_PAIR@4..15
834                            TWIG_LITERAL_HASH_KEY@4..6
835                              TWIG_LITERAL_NUMBER@4..6
836                                TK_WHITESPACE@4..5 " "
837                                TK_NUMBER@5..6 "1"
838                            TK_COLON@6..7 ":"
839                            TWIG_EXPRESSION@7..15
840                              TWIG_LITERAL_STRING@7..15
841                                TK_WHITESPACE@7..8 " "
842                                TK_SINGLE_QUOTES@8..9 "'"
843                                TWIG_LITERAL_STRING_INNER@9..14
844                                  TK_WORD@9..14 "hello"
845                                TK_SINGLE_QUOTES@14..15 "'"
846                          TK_COMMA@15..16 ","
847                          TWIG_LITERAL_HASH_PAIR@16..27
848                            TWIG_LITERAL_HASH_KEY@16..18
849                              TWIG_LITERAL_NUMBER@16..18
850                                TK_WHITESPACE@16..17 " "
851                                TK_NUMBER@17..18 "2"
852                            TK_COLON@18..19 ":"
853                            TWIG_EXPRESSION@19..27
854                              TWIG_LITERAL_STRING@19..27
855                                TK_WHITESPACE@19..20 " "
856                                TK_SINGLE_QUOTES@20..21 "'"
857                                TWIG_LITERAL_STRING_INNER@21..26
858                                  TK_WORD@21..26 "world"
859                                TK_SINGLE_QUOTES@26..27 "'"
860                        TK_WHITESPACE@27..28 " "
861                        TK_CLOSE_CURLY@28..29 "}"
862                    TK_WHITESPACE@29..30 " "
863                    TK_CLOSE_CURLY_CURLY@30..32 "}}""#]],
864        );
865    }
866
867    #[test]
868    fn parse_twig_string_hash() {
869        check_parse(
870            "{{ { 'hello': 42, 'world': 33 } }}",
871            expect![[r#"
872                ROOT@0..34
873                  TWIG_VAR@0..34
874                    TK_OPEN_CURLY_CURLY@0..2 "{{"
875                    TWIG_EXPRESSION@2..31
876                      TWIG_LITERAL_HASH@2..31
877                        TK_WHITESPACE@2..3 " "
878                        TK_OPEN_CURLY@3..4 "{"
879                        TWIG_LITERAL_HASH_ITEMS@4..29
880                          TWIG_LITERAL_HASH_PAIR@4..16
881                            TWIG_LITERAL_HASH_KEY@4..12
882                              TWIG_LITERAL_STRING@4..12
883                                TK_WHITESPACE@4..5 " "
884                                TK_SINGLE_QUOTES@5..6 "'"
885                                TWIG_LITERAL_STRING_INNER@6..11
886                                  TK_WORD@6..11 "hello"
887                                TK_SINGLE_QUOTES@11..12 "'"
888                            TK_COLON@12..13 ":"
889                            TWIG_EXPRESSION@13..16
890                              TWIG_LITERAL_NUMBER@13..16
891                                TK_WHITESPACE@13..14 " "
892                                TK_NUMBER@14..16 "42"
893                          TK_COMMA@16..17 ","
894                          TWIG_LITERAL_HASH_PAIR@17..29
895                            TWIG_LITERAL_HASH_KEY@17..25
896                              TWIG_LITERAL_STRING@17..25
897                                TK_WHITESPACE@17..18 " "
898                                TK_SINGLE_QUOTES@18..19 "'"
899                                TWIG_LITERAL_STRING_INNER@19..24
900                                  TK_WORD@19..24 "world"
901                                TK_SINGLE_QUOTES@24..25 "'"
902                            TK_COLON@25..26 ":"
903                            TWIG_EXPRESSION@26..29
904                              TWIG_LITERAL_NUMBER@26..29
905                                TK_WHITESPACE@26..27 " "
906                                TK_NUMBER@27..29 "33"
907                        TK_WHITESPACE@29..30 " "
908                        TK_CLOSE_CURLY@30..31 "}"
909                    TK_WHITESPACE@31..32 " "
910                    TK_CLOSE_CURLY_CURLY@32..34 "}}""#]],
911        );
912    }
913
914    #[test]
915    fn parse_twig_named_hash() {
916        check_parse(
917            "{{ { hello: 42, world: 33 } }}",
918            expect![[r#"
919                ROOT@0..30
920                  TWIG_VAR@0..30
921                    TK_OPEN_CURLY_CURLY@0..2 "{{"
922                    TWIG_EXPRESSION@2..27
923                      TWIG_LITERAL_HASH@2..27
924                        TK_WHITESPACE@2..3 " "
925                        TK_OPEN_CURLY@3..4 "{"
926                        TWIG_LITERAL_HASH_ITEMS@4..25
927                          TWIG_LITERAL_HASH_PAIR@4..14
928                            TWIG_LITERAL_HASH_KEY@4..10
929                              TK_WHITESPACE@4..5 " "
930                              TK_WORD@5..10 "hello"
931                            TK_COLON@10..11 ":"
932                            TWIG_EXPRESSION@11..14
933                              TWIG_LITERAL_NUMBER@11..14
934                                TK_WHITESPACE@11..12 " "
935                                TK_NUMBER@12..14 "42"
936                          TK_COMMA@14..15 ","
937                          TWIG_LITERAL_HASH_PAIR@15..25
938                            TWIG_LITERAL_HASH_KEY@15..21
939                              TK_WHITESPACE@15..16 " "
940                              TK_WORD@16..21 "world"
941                            TK_COLON@21..22 ":"
942                            TWIG_EXPRESSION@22..25
943                              TWIG_LITERAL_NUMBER@22..25
944                                TK_WHITESPACE@22..23 " "
945                                TK_NUMBER@23..25 "33"
946                        TK_WHITESPACE@25..26 " "
947                        TK_CLOSE_CURLY@26..27 "}"
948                    TK_WHITESPACE@27..28 " "
949                    TK_CLOSE_CURLY_CURLY@28..30 "}}""#]],
950        );
951    }
952
953    #[test]
954    fn parse_twig_unquoted_hash_with_only_underscore() {
955        check_parse(
956            "{{ { valid: 42, _: 99 } }}",
957            expect![[r#"
958                ROOT@0..26
959                  TWIG_VAR@0..26
960                    TK_OPEN_CURLY_CURLY@0..2 "{{"
961                    TWIG_EXPRESSION@2..23
962                      TWIG_LITERAL_HASH@2..23
963                        TK_WHITESPACE@2..3 " "
964                        TK_OPEN_CURLY@3..4 "{"
965                        TWIG_LITERAL_HASH_ITEMS@4..21
966                          TWIG_LITERAL_HASH_PAIR@4..14
967                            TWIG_LITERAL_HASH_KEY@4..10
968                              TK_WHITESPACE@4..5 " "
969                              TK_WORD@5..10 "valid"
970                            TK_COLON@10..11 ":"
971                            TWIG_EXPRESSION@11..14
972                              TWIG_LITERAL_NUMBER@11..14
973                                TK_WHITESPACE@11..12 " "
974                                TK_NUMBER@12..14 "42"
975                          TK_COMMA@14..15 ","
976                          TWIG_LITERAL_HASH_PAIR@15..21
977                            TWIG_LITERAL_HASH_KEY@15..17
978                              TK_WHITESPACE@15..16 " "
979                              TK_WORD@16..17 "_"
980                            TK_COLON@17..18 ":"
981                            TWIG_EXPRESSION@18..21
982                              TWIG_LITERAL_NUMBER@18..21
983                                TK_WHITESPACE@18..19 " "
984                                TK_NUMBER@19..21 "99"
985                        TK_WHITESPACE@21..22 " "
986                        TK_CLOSE_CURLY@22..23 "}"
987                    TK_WHITESPACE@23..24 " "
988                    TK_CLOSE_CURLY_CURLY@24..26 "}}""#]],
989        );
990    }
991
992    #[test]
993    fn parse_twig_expression_hash_missing_comma() {
994        check_parse(
995            "{{ { (15): 42 (60): 33 } }}",
996            expect![[r#"
997                ROOT@0..27
998                  TWIG_VAR@0..27
999                    TK_OPEN_CURLY_CURLY@0..2 "{{"
1000                    TWIG_EXPRESSION@2..24
1001                      TWIG_LITERAL_HASH@2..24
1002                        TK_WHITESPACE@2..3 " "
1003                        TK_OPEN_CURLY@3..4 "{"
1004                        TWIG_LITERAL_HASH_ITEMS@4..22
1005                          TWIG_LITERAL_HASH_PAIR@4..13
1006                            TWIG_LITERAL_HASH_KEY@4..9
1007                              TK_WHITESPACE@4..5 " "
1008                              TK_OPEN_PARENTHESIS@5..6 "("
1009                              TWIG_EXPRESSION@6..8
1010                                TWIG_LITERAL_NUMBER@6..8
1011                                  TK_NUMBER@6..8 "15"
1012                              TK_CLOSE_PARENTHESIS@8..9 ")"
1013                            TK_COLON@9..10 ":"
1014                            TWIG_EXPRESSION@10..13
1015                              TWIG_LITERAL_NUMBER@10..13
1016                                TK_WHITESPACE@10..11 " "
1017                                TK_NUMBER@11..13 "42"
1018                          TWIG_LITERAL_HASH_PAIR@13..22
1019                            TWIG_LITERAL_HASH_KEY@13..18
1020                              TK_WHITESPACE@13..14 " "
1021                              TK_OPEN_PARENTHESIS@14..15 "("
1022                              TWIG_EXPRESSION@15..17
1023                                TWIG_LITERAL_NUMBER@15..17
1024                                  TK_NUMBER@15..17 "60"
1025                              TK_CLOSE_PARENTHESIS@17..18 ")"
1026                            TK_COLON@18..19 ":"
1027                            TWIG_EXPRESSION@19..22
1028                              TWIG_LITERAL_NUMBER@19..22
1029                                TK_WHITESPACE@19..20 " "
1030                                TK_NUMBER@20..22 "33"
1031                        TK_WHITESPACE@22..23 " "
1032                        TK_CLOSE_CURLY@23..24 "}"
1033                    TK_WHITESPACE@24..25 " "
1034                    TK_CLOSE_CURLY_CURLY@25..27 "}}"
1035                error at 14..15: expected , but found ("#]],
1036        );
1037    }
1038
1039    #[test]
1040    fn parse_twig_expression_hash_missing_whitespace() {
1041        check_parse(
1042            "{{ { '%total%':reviews.totalReviews } }}",
1043            expect![[r#"
1044            ROOT@0..40
1045              TWIG_VAR@0..40
1046                TK_OPEN_CURLY_CURLY@0..2 "{{"
1047                TWIG_EXPRESSION@2..37
1048                  TWIG_LITERAL_HASH@2..37
1049                    TK_WHITESPACE@2..3 " "
1050                    TK_OPEN_CURLY@3..4 "{"
1051                    TWIG_LITERAL_HASH_ITEMS@4..35
1052                      TWIG_LITERAL_HASH_PAIR@4..35
1053                        TWIG_LITERAL_HASH_KEY@4..14
1054                          TWIG_LITERAL_STRING@4..14
1055                            TK_WHITESPACE@4..5 " "
1056                            TK_SINGLE_QUOTES@5..6 "'"
1057                            TWIG_LITERAL_STRING_INNER@6..13
1058                              TK_PERCENT@6..7 "%"
1059                              TK_WORD@7..12 "total"
1060                              TK_PERCENT@12..13 "%"
1061                            TK_SINGLE_QUOTES@13..14 "'"
1062                        TK_COLON@14..15 ":"
1063                        TWIG_EXPRESSION@15..35
1064                          TWIG_ACCESSOR@15..35
1065                            TWIG_OPERAND@15..22
1066                              TWIG_LITERAL_NAME@15..22
1067                                TK_WORD@15..22 "reviews"
1068                            TK_DOT@22..23 "."
1069                            TWIG_OPERAND@23..35
1070                              TWIG_LITERAL_NAME@23..35
1071                                TK_WORD@23..35 "totalReviews"
1072                    TK_WHITESPACE@35..36 " "
1073                    TK_CLOSE_CURLY@36..37 "}"
1074                TK_WHITESPACE@37..38 " "
1075                TK_CLOSE_CURLY_CURLY@38..40 "}}""#]],
1076        );
1077    }
1078
1079    #[test]
1080    fn parse_twig_complex_expression_hash() {
1081        check_parse(
1082            "{{ { (foo): 'foo', (1 + 1): 'bar', (foo ~ 'b'): 'baz' } }}",
1083            expect![[r#"
1084                ROOT@0..58
1085                  TWIG_VAR@0..58
1086                    TK_OPEN_CURLY_CURLY@0..2 "{{"
1087                    TWIG_EXPRESSION@2..55
1088                      TWIG_LITERAL_HASH@2..55
1089                        TK_WHITESPACE@2..3 " "
1090                        TK_OPEN_CURLY@3..4 "{"
1091                        TWIG_LITERAL_HASH_ITEMS@4..53
1092                          TWIG_LITERAL_HASH_PAIR@4..17
1093                            TWIG_LITERAL_HASH_KEY@4..10
1094                              TK_WHITESPACE@4..5 " "
1095                              TK_OPEN_PARENTHESIS@5..6 "("
1096                              TWIG_EXPRESSION@6..9
1097                                TWIG_LITERAL_NAME@6..9
1098                                  TK_WORD@6..9 "foo"
1099                              TK_CLOSE_PARENTHESIS@9..10 ")"
1100                            TK_COLON@10..11 ":"
1101                            TWIG_EXPRESSION@11..17
1102                              TWIG_LITERAL_STRING@11..17
1103                                TK_WHITESPACE@11..12 " "
1104                                TK_SINGLE_QUOTES@12..13 "'"
1105                                TWIG_LITERAL_STRING_INNER@13..16
1106                                  TK_WORD@13..16 "foo"
1107                                TK_SINGLE_QUOTES@16..17 "'"
1108                          TK_COMMA@17..18 ","
1109                          TWIG_LITERAL_HASH_PAIR@18..33
1110                            TWIG_LITERAL_HASH_KEY@18..26
1111                              TK_WHITESPACE@18..19 " "
1112                              TK_OPEN_PARENTHESIS@19..20 "("
1113                              TWIG_EXPRESSION@20..25
1114                                TWIG_BINARY_EXPRESSION@20..25
1115                                  TWIG_EXPRESSION@20..21
1116                                    TWIG_LITERAL_NUMBER@20..21
1117                                      TK_NUMBER@20..21 "1"
1118                                  TK_WHITESPACE@21..22 " "
1119                                  TK_PLUS@22..23 "+"
1120                                  TWIG_EXPRESSION@23..25
1121                                    TWIG_LITERAL_NUMBER@23..25
1122                                      TK_WHITESPACE@23..24 " "
1123                                      TK_NUMBER@24..25 "1"
1124                              TK_CLOSE_PARENTHESIS@25..26 ")"
1125                            TK_COLON@26..27 ":"
1126                            TWIG_EXPRESSION@27..33
1127                              TWIG_LITERAL_STRING@27..33
1128                                TK_WHITESPACE@27..28 " "
1129                                TK_SINGLE_QUOTES@28..29 "'"
1130                                TWIG_LITERAL_STRING_INNER@29..32
1131                                  TK_WORD@29..32 "bar"
1132                                TK_SINGLE_QUOTES@32..33 "'"
1133                          TK_COMMA@33..34 ","
1134                          TWIG_LITERAL_HASH_PAIR@34..53
1135                            TWIG_LITERAL_HASH_KEY@34..46
1136                              TK_WHITESPACE@34..35 " "
1137                              TK_OPEN_PARENTHESIS@35..36 "("
1138                              TWIG_EXPRESSION@36..45
1139                                TWIG_BINARY_EXPRESSION@36..45
1140                                  TWIG_EXPRESSION@36..39
1141                                    TWIG_LITERAL_NAME@36..39
1142                                      TK_WORD@36..39 "foo"
1143                                  TK_WHITESPACE@39..40 " "
1144                                  TK_TILDE@40..41 "~"
1145                                  TWIG_EXPRESSION@41..45
1146                                    TWIG_LITERAL_STRING@41..45
1147                                      TK_WHITESPACE@41..42 " "
1148                                      TK_SINGLE_QUOTES@42..43 "'"
1149                                      TWIG_LITERAL_STRING_INNER@43..44
1150                                        TK_WORD@43..44 "b"
1151                                      TK_SINGLE_QUOTES@44..45 "'"
1152                              TK_CLOSE_PARENTHESIS@45..46 ")"
1153                            TK_COLON@46..47 ":"
1154                            TWIG_EXPRESSION@47..53
1155                              TWIG_LITERAL_STRING@47..53
1156                                TK_WHITESPACE@47..48 " "
1157                                TK_SINGLE_QUOTES@48..49 "'"
1158                                TWIG_LITERAL_STRING_INNER@49..52
1159                                  TK_WORD@49..52 "baz"
1160                                TK_SINGLE_QUOTES@52..53 "'"
1161                        TK_WHITESPACE@53..54 " "
1162                        TK_CLOSE_CURLY@54..55 "}"
1163                    TK_WHITESPACE@55..56 " "
1164                    TK_CLOSE_CURLY_CURLY@56..58 "}}""#]],
1165        );
1166    }
1167
1168    #[test]
1169    fn parse_twig_nested_hash() {
1170        check_parse(
1171            "{{ { outer: { inner: 'hello' } } }}",
1172            expect![[r#"
1173                ROOT@0..35
1174                  TWIG_VAR@0..35
1175                    TK_OPEN_CURLY_CURLY@0..2 "{{"
1176                    TWIG_EXPRESSION@2..32
1177                      TWIG_LITERAL_HASH@2..32
1178                        TK_WHITESPACE@2..3 " "
1179                        TK_OPEN_CURLY@3..4 "{"
1180                        TWIG_LITERAL_HASH_ITEMS@4..30
1181                          TWIG_LITERAL_HASH_PAIR@4..30
1182                            TWIG_LITERAL_HASH_KEY@4..10
1183                              TK_WHITESPACE@4..5 " "
1184                              TK_WORD@5..10 "outer"
1185                            TK_COLON@10..11 ":"
1186                            TWIG_EXPRESSION@11..30
1187                              TWIG_LITERAL_HASH@11..30
1188                                TK_WHITESPACE@11..12 " "
1189                                TK_OPEN_CURLY@12..13 "{"
1190                                TWIG_LITERAL_HASH_ITEMS@13..28
1191                                  TWIG_LITERAL_HASH_PAIR@13..28
1192                                    TWIG_LITERAL_HASH_KEY@13..19
1193                                      TK_WHITESPACE@13..14 " "
1194                                      TK_WORD@14..19 "inner"
1195                                    TK_COLON@19..20 ":"
1196                                    TWIG_EXPRESSION@20..28
1197                                      TWIG_LITERAL_STRING@20..28
1198                                        TK_WHITESPACE@20..21 " "
1199                                        TK_SINGLE_QUOTES@21..22 "'"
1200                                        TWIG_LITERAL_STRING_INNER@22..27
1201                                          TK_WORD@22..27 "hello"
1202                                        TK_SINGLE_QUOTES@27..28 "'"
1203                                TK_WHITESPACE@28..29 " "
1204                                TK_CLOSE_CURLY@29..30 "}"
1205                        TK_WHITESPACE@30..31 " "
1206                        TK_CLOSE_CURLY@31..32 "}"
1207                    TK_WHITESPACE@32..33 " "
1208                    TK_CLOSE_CURLY_CURLY@33..35 "}}""#]],
1209        );
1210    }
1211
1212    #[test]
1213    fn parse_twig_hash_with_omitted_value() {
1214        check_parse(
1215            "{{ { value, is, same, as, key } }}",
1216            expect![[r#"
1217                ROOT@0..34
1218                  TWIG_VAR@0..34
1219                    TK_OPEN_CURLY_CURLY@0..2 "{{"
1220                    TWIG_EXPRESSION@2..31
1221                      TWIG_LITERAL_HASH@2..31
1222                        TK_WHITESPACE@2..3 " "
1223                        TK_OPEN_CURLY@3..4 "{"
1224                        TWIG_LITERAL_HASH_ITEMS@4..29
1225                          TWIG_LITERAL_HASH_PAIR@4..10
1226                            TWIG_LITERAL_HASH_KEY@4..10
1227                              TK_WHITESPACE@4..5 " "
1228                              TK_WORD@5..10 "value"
1229                          TK_COMMA@10..11 ","
1230                          TWIG_LITERAL_HASH_PAIR@11..14
1231                            TWIG_LITERAL_HASH_KEY@11..14
1232                              TK_WHITESPACE@11..12 " "
1233                              TK_WORD@12..14 "is"
1234                          TK_COMMA@14..15 ","
1235                          TWIG_LITERAL_HASH_PAIR@15..20
1236                            TWIG_LITERAL_HASH_KEY@15..20
1237                              TK_WHITESPACE@15..16 " "
1238                              TK_WORD@16..20 "same"
1239                          TK_COMMA@20..21 ","
1240                          TWIG_LITERAL_HASH_PAIR@21..24
1241                            TWIG_LITERAL_HASH_KEY@21..24
1242                              TK_WHITESPACE@21..22 " "
1243                              TK_WORD@22..24 "as"
1244                          TK_COMMA@24..25 ","
1245                          TWIG_LITERAL_HASH_PAIR@25..29
1246                            TWIG_LITERAL_HASH_KEY@25..29
1247                              TK_WHITESPACE@25..26 " "
1248                              TK_WORD@26..29 "key"
1249                        TK_WHITESPACE@29..30 " "
1250                        TK_CLOSE_CURLY@30..31 "}"
1251                    TK_WHITESPACE@31..32 " "
1252                    TK_CLOSE_CURLY_CURLY@32..34 "}}""#]],
1253        );
1254    }
1255
1256    #[test]
1257    fn parse_twig_array_with_hash_mixed() {
1258        check_parse(
1259            r#"{{ [1, {"foo": "bar"}] }}"#,
1260            expect![[r#"
1261                ROOT@0..25
1262                  TWIG_VAR@0..25
1263                    TK_OPEN_CURLY_CURLY@0..2 "{{"
1264                    TWIG_EXPRESSION@2..22
1265                      TWIG_LITERAL_ARRAY@2..22
1266                        TK_WHITESPACE@2..3 " "
1267                        TK_OPEN_SQUARE@3..4 "["
1268                        TWIG_LITERAL_ARRAY_INNER@4..21
1269                          TWIG_EXPRESSION@4..5
1270                            TWIG_LITERAL_NUMBER@4..5
1271                              TK_NUMBER@4..5 "1"
1272                          TK_COMMA@5..6 ","
1273                          TWIG_EXPRESSION@6..21
1274                            TWIG_LITERAL_HASH@6..21
1275                              TK_WHITESPACE@6..7 " "
1276                              TK_OPEN_CURLY@7..8 "{"
1277                              TWIG_LITERAL_HASH_ITEMS@8..20
1278                                TWIG_LITERAL_HASH_PAIR@8..20
1279                                  TWIG_LITERAL_HASH_KEY@8..13
1280                                    TWIG_LITERAL_STRING@8..13
1281                                      TK_DOUBLE_QUOTES@8..9 "\""
1282                                      TWIG_LITERAL_STRING_INNER@9..12
1283                                        TK_WORD@9..12 "foo"
1284                                      TK_DOUBLE_QUOTES@12..13 "\""
1285                                  TK_COLON@13..14 ":"
1286                                  TWIG_EXPRESSION@14..20
1287                                    TWIG_LITERAL_STRING@14..20
1288                                      TK_WHITESPACE@14..15 " "
1289                                      TK_DOUBLE_QUOTES@15..16 "\""
1290                                      TWIG_LITERAL_STRING_INNER@16..19
1291                                        TK_WORD@16..19 "bar"
1292                                      TK_DOUBLE_QUOTES@19..20 "\""
1293                              TK_CLOSE_CURLY@20..21 "}"
1294                        TK_CLOSE_SQUARE@21..22 "]"
1295                    TK_WHITESPACE@22..23 " "
1296                    TK_CLOSE_CURLY_CURLY@23..25 "}}""#]],
1297        );
1298    }
1299
1300    #[test]
1301    fn parse_twig_variable_name() {
1302        check_parse(
1303            "{{ my_variable }}",
1304            expect![[r#"
1305                ROOT@0..17
1306                  TWIG_VAR@0..17
1307                    TK_OPEN_CURLY_CURLY@0..2 "{{"
1308                    TWIG_EXPRESSION@2..14
1309                      TWIG_LITERAL_NAME@2..14
1310                        TK_WHITESPACE@2..3 " "
1311                        TK_WORD@3..14 "my_variable"
1312                    TK_WHITESPACE@14..15 " "
1313                    TK_CLOSE_CURLY_CURLY@15..17 "}}""#]],
1314        );
1315    }
1316
1317    #[test]
1318    fn parse_twig_token_variable_name() {
1319        check_parse(
1320            "{{ and }}",
1321            expect![[r#"
1322                ROOT@0..9
1323                  TWIG_VAR@0..9
1324                    TK_OPEN_CURLY_CURLY@0..2 "{{"
1325                    TWIG_EXPRESSION@2..6
1326                      TWIG_LITERAL_NAME@2..6
1327                        TK_WHITESPACE@2..3 " "
1328                        TK_WORD@3..6 "and"
1329                    TK_WHITESPACE@6..7 " "
1330                    TK_CLOSE_CURLY_CURLY@7..9 "}}""#]],
1331        );
1332    }
1333
1334    #[test]
1335    fn parse_twig_variable_get_attribute_expression() {
1336        check_parse(
1337            r"{{ product.prices.euro }}",
1338            expect![[r#"
1339                ROOT@0..25
1340                  TWIG_VAR@0..25
1341                    TK_OPEN_CURLY_CURLY@0..2 "{{"
1342                    TWIG_EXPRESSION@2..22
1343                      TWIG_ACCESSOR@2..22
1344                        TWIG_OPERAND@2..17
1345                          TWIG_ACCESSOR@2..17
1346                            TWIG_OPERAND@2..10
1347                              TWIG_LITERAL_NAME@2..10
1348                                TK_WHITESPACE@2..3 " "
1349                                TK_WORD@3..10 "product"
1350                            TK_DOT@10..11 "."
1351                            TWIG_OPERAND@11..17
1352                              TWIG_LITERAL_NAME@11..17
1353                                TK_WORD@11..17 "prices"
1354                        TK_DOT@17..18 "."
1355                        TWIG_OPERAND@18..22
1356                          TWIG_LITERAL_NAME@18..22
1357                            TK_WORD@18..22 "euro"
1358                    TK_WHITESPACE@22..23 " "
1359                    TK_CLOSE_CURLY_CURLY@23..25 "}}""#]],
1360        );
1361    }
1362
1363    #[test]
1364    fn parse_twig_variable_with_filters() {
1365        check_parse(
1366            r"{{ product.price|striptags|title }}",
1367            expect![[r#"
1368                ROOT@0..35
1369                  TWIG_VAR@0..35
1370                    TK_OPEN_CURLY_CURLY@0..2 "{{"
1371                    TWIG_EXPRESSION@2..32
1372                      TWIG_FILTER@2..32
1373                        TWIG_OPERAND@2..26
1374                          TWIG_FILTER@2..26
1375                            TWIG_OPERAND@2..16
1376                              TWIG_ACCESSOR@2..16
1377                                TWIG_OPERAND@2..10
1378                                  TWIG_LITERAL_NAME@2..10
1379                                    TK_WHITESPACE@2..3 " "
1380                                    TK_WORD@3..10 "product"
1381                                TK_DOT@10..11 "."
1382                                TWIG_OPERAND@11..16
1383                                  TWIG_LITERAL_NAME@11..16
1384                                    TK_WORD@11..16 "price"
1385                            TK_SINGLE_PIPE@16..17 "|"
1386                            TWIG_OPERAND@17..26
1387                              TWIG_LITERAL_NAME@17..26
1388                                TK_WORD@17..26 "striptags"
1389                        TK_SINGLE_PIPE@26..27 "|"
1390                        TWIG_OPERAND@27..32
1391                          TWIG_LITERAL_NAME@27..32
1392                            TK_WORD@27..32 "title"
1393                    TK_WHITESPACE@32..33 " "
1394                    TK_CLOSE_CURLY_CURLY@33..35 "}}""#]],
1395        );
1396    }
1397
1398    #[test]
1399    fn parse_twig_variable_array_accessor() {
1400        check_parse(
1401            r"{{ product.prices['eur'] }}",
1402            expect![[r#"
1403                ROOT@0..27
1404                  TWIG_VAR@0..27
1405                    TK_OPEN_CURLY_CURLY@0..2 "{{"
1406                    TWIG_EXPRESSION@2..24
1407                      TWIG_INDEX_LOOKUP@2..24
1408                        TWIG_OPERAND@2..17
1409                          TWIG_ACCESSOR@2..17
1410                            TWIG_OPERAND@2..10
1411                              TWIG_LITERAL_NAME@2..10
1412                                TK_WHITESPACE@2..3 " "
1413                                TK_WORD@3..10 "product"
1414                            TK_DOT@10..11 "."
1415                            TWIG_OPERAND@11..17
1416                              TWIG_LITERAL_NAME@11..17
1417                                TK_WORD@11..17 "prices"
1418                        TK_OPEN_SQUARE@17..18 "["
1419                        TWIG_INDEX@18..23
1420                          TWIG_EXPRESSION@18..23
1421                            TWIG_LITERAL_STRING@18..23
1422                              TK_SINGLE_QUOTES@18..19 "'"
1423                              TWIG_LITERAL_STRING_INNER@19..22
1424                                TK_WORD@19..22 "eur"
1425                              TK_SINGLE_QUOTES@22..23 "'"
1426                        TK_CLOSE_SQUARE@23..24 "]"
1427                    TK_WHITESPACE@24..25 " "
1428                    TK_CLOSE_CURLY_CURLY@25..27 "}}""#]],
1429        );
1430    }
1431
1432    #[test]
1433    fn parse_twig_variable_nested_array_accessor() {
1434        check_parse(
1435            r"{{ product.prices['eur'][0] }}",
1436            expect![[r#"
1437                ROOT@0..30
1438                  TWIG_VAR@0..30
1439                    TK_OPEN_CURLY_CURLY@0..2 "{{"
1440                    TWIG_EXPRESSION@2..27
1441                      TWIG_INDEX_LOOKUP@2..27
1442                        TWIG_OPERAND@2..24
1443                          TWIG_INDEX_LOOKUP@2..24
1444                            TWIG_OPERAND@2..17
1445                              TWIG_ACCESSOR@2..17
1446                                TWIG_OPERAND@2..10
1447                                  TWIG_LITERAL_NAME@2..10
1448                                    TK_WHITESPACE@2..3 " "
1449                                    TK_WORD@3..10 "product"
1450                                TK_DOT@10..11 "."
1451                                TWIG_OPERAND@11..17
1452                                  TWIG_LITERAL_NAME@11..17
1453                                    TK_WORD@11..17 "prices"
1454                            TK_OPEN_SQUARE@17..18 "["
1455                            TWIG_INDEX@18..23
1456                              TWIG_EXPRESSION@18..23
1457                                TWIG_LITERAL_STRING@18..23
1458                                  TK_SINGLE_QUOTES@18..19 "'"
1459                                  TWIG_LITERAL_STRING_INNER@19..22
1460                                    TK_WORD@19..22 "eur"
1461                                  TK_SINGLE_QUOTES@22..23 "'"
1462                            TK_CLOSE_SQUARE@23..24 "]"
1463                        TK_OPEN_SQUARE@24..25 "["
1464                        TWIG_INDEX@25..26
1465                          TWIG_EXPRESSION@25..26
1466                            TWIG_LITERAL_NUMBER@25..26
1467                              TK_NUMBER@25..26 "0"
1468                        TK_CLOSE_SQUARE@26..27 "]"
1469                    TK_WHITESPACE@27..28 " "
1470                    TK_CLOSE_CURLY_CURLY@28..30 "}}""#]],
1471        );
1472    }
1473
1474    #[test]
1475    fn parse_twig_variable_nested_array_accessor_with_dot() {
1476        check_parse(
1477            r"{{ product.prices['eur'].0 }}",
1478            expect![[r#"
1479            ROOT@0..29
1480              TWIG_VAR@0..29
1481                TK_OPEN_CURLY_CURLY@0..2 "{{"
1482                TWIG_EXPRESSION@2..26
1483                  TWIG_INDEX_LOOKUP@2..26
1484                    TWIG_OPERAND@2..24
1485                      TWIG_INDEX_LOOKUP@2..24
1486                        TWIG_OPERAND@2..17
1487                          TWIG_ACCESSOR@2..17
1488                            TWIG_OPERAND@2..10
1489                              TWIG_LITERAL_NAME@2..10
1490                                TK_WHITESPACE@2..3 " "
1491                                TK_WORD@3..10 "product"
1492                            TK_DOT@10..11 "."
1493                            TWIG_OPERAND@11..17
1494                              TWIG_LITERAL_NAME@11..17
1495                                TK_WORD@11..17 "prices"
1496                        TK_OPEN_SQUARE@17..18 "["
1497                        TWIG_INDEX@18..23
1498                          TWIG_EXPRESSION@18..23
1499                            TWIG_LITERAL_STRING@18..23
1500                              TK_SINGLE_QUOTES@18..19 "'"
1501                              TWIG_LITERAL_STRING_INNER@19..22
1502                                TK_WORD@19..22 "eur"
1503                              TK_SINGLE_QUOTES@22..23 "'"
1504                        TK_CLOSE_SQUARE@23..24 "]"
1505                    TK_DOT@24..25 "."
1506                    TWIG_INDEX@25..26
1507                      TWIG_EXPRESSION@25..26
1508                        TWIG_LITERAL_NUMBER@25..26
1509                          TK_NUMBER@25..26 "0"
1510                TK_WHITESPACE@26..27 " "
1511                TK_CLOSE_CURLY_CURLY@27..29 "}}""#]],
1512        );
1513    }
1514
1515    #[test]
1516    fn parse_twig_variable_array_dot_accessor() {
1517        check_parse(
1518            r"{{ product.tags.0.name }}",
1519            expect![[r#"
1520            ROOT@0..25
1521              TWIG_VAR@0..25
1522                TK_OPEN_CURLY_CURLY@0..2 "{{"
1523                TWIG_EXPRESSION@2..22
1524                  TWIG_ACCESSOR@2..22
1525                    TWIG_OPERAND@2..17
1526                      TWIG_INDEX_LOOKUP@2..17
1527                        TWIG_OPERAND@2..15
1528                          TWIG_ACCESSOR@2..15
1529                            TWIG_OPERAND@2..10
1530                              TWIG_LITERAL_NAME@2..10
1531                                TK_WHITESPACE@2..3 " "
1532                                TK_WORD@3..10 "product"
1533                            TK_DOT@10..11 "."
1534                            TWIG_OPERAND@11..15
1535                              TWIG_LITERAL_NAME@11..15
1536                                TK_WORD@11..15 "tags"
1537                        TK_DOT@15..16 "."
1538                        TWIG_INDEX@16..17
1539                          TWIG_EXPRESSION@16..17
1540                            TWIG_LITERAL_NUMBER@16..17
1541                              TK_NUMBER@16..17 "0"
1542                    TK_DOT@17..18 "."
1543                    TWIG_OPERAND@18..22
1544                      TWIG_LITERAL_NAME@18..22
1545                        TK_WORD@18..22 "name"
1546                TK_WHITESPACE@22..23 " "
1547                TK_CLOSE_CURLY_CURLY@23..25 "}}""#]],
1548        );
1549    }
1550
1551    #[test]
1552    fn parse_twig_variable_array_range_accessor() {
1553        check_parse(
1554            r"{{ prices[0:10] }}",
1555            expect![[r#"
1556                ROOT@0..18
1557                  TWIG_VAR@0..18
1558                    TK_OPEN_CURLY_CURLY@0..2 "{{"
1559                    TWIG_EXPRESSION@2..15
1560                      TWIG_INDEX_LOOKUP@2..15
1561                        TWIG_OPERAND@2..9
1562                          TWIG_LITERAL_NAME@2..9
1563                            TK_WHITESPACE@2..3 " "
1564                            TK_WORD@3..9 "prices"
1565                        TK_OPEN_SQUARE@9..10 "["
1566                        TWIG_INDEX_RANGE@10..14
1567                          TWIG_EXPRESSION@10..11
1568                            TWIG_LITERAL_NUMBER@10..11
1569                              TK_NUMBER@10..11 "0"
1570                          TK_COLON@11..12 ":"
1571                          TWIG_EXPRESSION@12..14
1572                            TWIG_LITERAL_NUMBER@12..14
1573                              TK_NUMBER@12..14 "10"
1574                        TK_CLOSE_SQUARE@14..15 "]"
1575                    TK_WHITESPACE@15..16 " "
1576                    TK_CLOSE_CURLY_CURLY@16..18 "}}""#]],
1577        );
1578    }
1579
1580    #[test]
1581    fn parse_twig_variable_array_range_left_accessor() {
1582        check_parse(
1583            r"{{ prices[10:] }}",
1584            expect![[r#"
1585                ROOT@0..17
1586                  TWIG_VAR@0..17
1587                    TK_OPEN_CURLY_CURLY@0..2 "{{"
1588                    TWIG_EXPRESSION@2..14
1589                      TWIG_INDEX_LOOKUP@2..14
1590                        TWIG_OPERAND@2..9
1591                          TWIG_LITERAL_NAME@2..9
1592                            TK_WHITESPACE@2..3 " "
1593                            TK_WORD@3..9 "prices"
1594                        TK_OPEN_SQUARE@9..10 "["
1595                        TWIG_INDEX_RANGE@10..13
1596                          TWIG_EXPRESSION@10..12
1597                            TWIG_LITERAL_NUMBER@10..12
1598                              TK_NUMBER@10..12 "10"
1599                          TK_COLON@12..13 ":"
1600                        TK_CLOSE_SQUARE@13..14 "]"
1601                    TK_WHITESPACE@14..15 " "
1602                    TK_CLOSE_CURLY_CURLY@15..17 "}}""#]],
1603        );
1604    }
1605
1606    #[test]
1607    fn parse_twig_variable_array_range_right_accessor() {
1608        check_parse(
1609            r"{{ prices[:10] }}",
1610            expect![[r#"
1611                ROOT@0..17
1612                  TWIG_VAR@0..17
1613                    TK_OPEN_CURLY_CURLY@0..2 "{{"
1614                    TWIG_EXPRESSION@2..14
1615                      TWIG_INDEX_LOOKUP@2..14
1616                        TWIG_OPERAND@2..9
1617                          TWIG_LITERAL_NAME@2..9
1618                            TK_WHITESPACE@2..3 " "
1619                            TK_WORD@3..9 "prices"
1620                        TK_OPEN_SQUARE@9..10 "["
1621                        TWIG_INDEX_RANGE@10..13
1622                          TK_COLON@10..11 ":"
1623                          TWIG_EXPRESSION@11..13
1624                            TWIG_LITERAL_NUMBER@11..13
1625                              TK_NUMBER@11..13 "10"
1626                        TK_CLOSE_SQUARE@13..14 "]"
1627                    TK_WHITESPACE@14..15 " "
1628                    TK_CLOSE_CURLY_CURLY@15..17 "}}""#]],
1629        );
1630    }
1631
1632    #[test]
1633    fn parse_twig_variable_array_range_right_accessor_negative() {
1634        check_parse(
1635            r"{{ prices[:-2] }}",
1636            expect![[r#"
1637            ROOT@0..17
1638              TWIG_VAR@0..17
1639                TK_OPEN_CURLY_CURLY@0..2 "{{"
1640                TWIG_EXPRESSION@2..14
1641                  TWIG_INDEX_LOOKUP@2..14
1642                    TWIG_OPERAND@2..9
1643                      TWIG_LITERAL_NAME@2..9
1644                        TK_WHITESPACE@2..3 " "
1645                        TK_WORD@3..9 "prices"
1646                    TK_OPEN_SQUARE@9..10 "["
1647                    TWIG_INDEX_RANGE@10..13
1648                      TK_COLON@10..11 ":"
1649                      TWIG_EXPRESSION@11..13
1650                        TWIG_UNARY_EXPRESSION@11..13
1651                          TK_MINUS@11..12 "-"
1652                          TWIG_EXPRESSION@12..13
1653                            TWIG_LITERAL_NUMBER@12..13
1654                              TK_NUMBER@12..13 "2"
1655                    TK_CLOSE_SQUARE@13..14 "]"
1656                TK_WHITESPACE@14..15 " "
1657                TK_CLOSE_CURLY_CURLY@15..17 "}}""#]],
1658        );
1659    }
1660
1661    #[test]
1662    fn parse_twig_variable_array_range_right_accessor_variable() {
1663        check_parse(
1664            r"{{ prices[:upperLimit] }}",
1665            expect![[r#"
1666            ROOT@0..25
1667              TWIG_VAR@0..25
1668                TK_OPEN_CURLY_CURLY@0..2 "{{"
1669                TWIG_EXPRESSION@2..22
1670                  TWIG_INDEX_LOOKUP@2..22
1671                    TWIG_OPERAND@2..9
1672                      TWIG_LITERAL_NAME@2..9
1673                        TK_WHITESPACE@2..3 " "
1674                        TK_WORD@3..9 "prices"
1675                    TK_OPEN_SQUARE@9..10 "["
1676                    TWIG_INDEX_RANGE@10..21
1677                      TK_COLON@10..11 ":"
1678                      TWIG_EXPRESSION@11..21
1679                        TWIG_LITERAL_NAME@11..21
1680                          TK_WORD@11..21 "upperLimit"
1681                    TK_CLOSE_SQUARE@21..22 "]"
1682                TK_WHITESPACE@22..23 " "
1683                TK_CLOSE_CURLY_CURLY@23..25 "}}""#]],
1684        );
1685    }
1686
1687    #[test]
1688    fn parse_twig_variable_array_range_left_accessor_variable() {
1689        check_parse(
1690            r"{{ prices[upperLimit:] }}",
1691            expect![[r#"
1692            ROOT@0..25
1693              TWIG_VAR@0..25
1694                TK_OPEN_CURLY_CURLY@0..2 "{{"
1695                TWIG_EXPRESSION@2..22
1696                  TWIG_INDEX_LOOKUP@2..22
1697                    TWIG_OPERAND@2..9
1698                      TWIG_LITERAL_NAME@2..9
1699                        TK_WHITESPACE@2..3 " "
1700                        TK_WORD@3..9 "prices"
1701                    TK_OPEN_SQUARE@9..10 "["
1702                    TWIG_INDEX_RANGE@10..21
1703                      TWIG_EXPRESSION@10..20
1704                        TWIG_LITERAL_NAME@10..20
1705                          TK_WORD@10..20 "upperLimit"
1706                      TK_COLON@20..21 ":"
1707                    TK_CLOSE_SQUARE@21..22 "]"
1708                TK_WHITESPACE@22..23 " "
1709                TK_CLOSE_CURLY_CURLY@23..25 "}}""#]],
1710        );
1711    }
1712
1713    #[test]
1714    fn parse_twig_variable_array_index_missing_expression() {
1715        check_parse(
1716            r"{{ prices[] }}",
1717            expect![[r#"
1718            ROOT@0..14
1719              TWIG_VAR@0..14
1720                TK_OPEN_CURLY_CURLY@0..2 "{{"
1721                TWIG_EXPRESSION@2..11
1722                  TWIG_INDEX_LOOKUP@2..11
1723                    TWIG_OPERAND@2..9
1724                      TWIG_LITERAL_NAME@2..9
1725                        TK_WHITESPACE@2..3 " "
1726                        TK_WORD@3..9 "prices"
1727                    TK_OPEN_SQUARE@9..10 "["
1728                    TWIG_INDEX@10..10
1729                    TK_CLOSE_SQUARE@10..11 "]"
1730                TK_WHITESPACE@11..12 " "
1731                TK_CLOSE_CURLY_CURLY@12..14 "}}"
1732            error at 10..11: expected twig expression but found ]"#]],
1733        );
1734    }
1735
1736    #[test]
1737    fn parse_twig_variable_accessor_indexer_and_filter() {
1738        check_parse(
1739            r"{{ product.prices['eur'][0]|title }}",
1740            expect![[r#"
1741                ROOT@0..36
1742                  TWIG_VAR@0..36
1743                    TK_OPEN_CURLY_CURLY@0..2 "{{"
1744                    TWIG_EXPRESSION@2..33
1745                      TWIG_FILTER@2..33
1746                        TWIG_OPERAND@2..27
1747                          TWIG_INDEX_LOOKUP@2..27
1748                            TWIG_OPERAND@2..24
1749                              TWIG_INDEX_LOOKUP@2..24
1750                                TWIG_OPERAND@2..17
1751                                  TWIG_ACCESSOR@2..17
1752                                    TWIG_OPERAND@2..10
1753                                      TWIG_LITERAL_NAME@2..10
1754                                        TK_WHITESPACE@2..3 " "
1755                                        TK_WORD@3..10 "product"
1756                                    TK_DOT@10..11 "."
1757                                    TWIG_OPERAND@11..17
1758                                      TWIG_LITERAL_NAME@11..17
1759                                        TK_WORD@11..17 "prices"
1760                                TK_OPEN_SQUARE@17..18 "["
1761                                TWIG_INDEX@18..23
1762                                  TWIG_EXPRESSION@18..23
1763                                    TWIG_LITERAL_STRING@18..23
1764                                      TK_SINGLE_QUOTES@18..19 "'"
1765                                      TWIG_LITERAL_STRING_INNER@19..22
1766                                        TK_WORD@19..22 "eur"
1767                                      TK_SINGLE_QUOTES@22..23 "'"
1768                                TK_CLOSE_SQUARE@23..24 "]"
1769                            TK_OPEN_SQUARE@24..25 "["
1770                            TWIG_INDEX@25..26
1771                              TWIG_EXPRESSION@25..26
1772                                TWIG_LITERAL_NUMBER@25..26
1773                                  TK_NUMBER@25..26 "0"
1774                            TK_CLOSE_SQUARE@26..27 "]"
1775                        TK_SINGLE_PIPE@27..28 "|"
1776                        TWIG_OPERAND@28..33
1777                          TWIG_LITERAL_NAME@28..33
1778                            TK_WORD@28..33 "title"
1779                    TK_WHITESPACE@33..34 " "
1780                    TK_CLOSE_CURLY_CURLY@34..36 "}}""#]],
1781        );
1782    }
1783
1784    #[test]
1785    fn parse_twig_variable_function_accessor() {
1786        check_parse(
1787            r"{{ product.prices('eur').gross }}",
1788            expect![[r#"
1789                ROOT@0..33
1790                  TWIG_VAR@0..33
1791                    TK_OPEN_CURLY_CURLY@0..2 "{{"
1792                    TWIG_EXPRESSION@2..30
1793                      TWIG_ACCESSOR@2..30
1794                        TWIG_OPERAND@2..24
1795                          TWIG_FUNCTION_CALL@2..24
1796                            TWIG_OPERAND@2..17
1797                              TWIG_ACCESSOR@2..17
1798                                TWIG_OPERAND@2..10
1799                                  TWIG_LITERAL_NAME@2..10
1800                                    TK_WHITESPACE@2..3 " "
1801                                    TK_WORD@3..10 "product"
1802                                TK_DOT@10..11 "."
1803                                TWIG_OPERAND@11..17
1804                                  TWIG_LITERAL_NAME@11..17
1805                                    TK_WORD@11..17 "prices"
1806                            TWIG_ARGUMENTS@17..24
1807                              TK_OPEN_PARENTHESIS@17..18 "("
1808                              TWIG_EXPRESSION@18..23
1809                                TWIG_LITERAL_STRING@18..23
1810                                  TK_SINGLE_QUOTES@18..19 "'"
1811                                  TWIG_LITERAL_STRING_INNER@19..22
1812                                    TK_WORD@19..22 "eur"
1813                                  TK_SINGLE_QUOTES@22..23 "'"
1814                              TK_CLOSE_PARENTHESIS@23..24 ")"
1815                        TK_DOT@24..25 "."
1816                        TWIG_OPERAND@25..30
1817                          TWIG_LITERAL_NAME@25..30
1818                            TK_WORD@25..30 "gross"
1819                    TK_WHITESPACE@30..31 " "
1820                    TK_CLOSE_CURLY_CURLY@31..33 "}}""#]],
1821        );
1822    }
1823
1824    #[test]
1825    fn parse_twig_variable_deep_function_accessor() {
1826        check_parse(
1827            r"{{ product.prices.gross('eur').gross }}",
1828            expect![[r#"
1829                ROOT@0..39
1830                  TWIG_VAR@0..39
1831                    TK_OPEN_CURLY_CURLY@0..2 "{{"
1832                    TWIG_EXPRESSION@2..36
1833                      TWIG_ACCESSOR@2..36
1834                        TWIG_OPERAND@2..30
1835                          TWIG_FUNCTION_CALL@2..30
1836                            TWIG_OPERAND@2..23
1837                              TWIG_ACCESSOR@2..23
1838                                TWIG_OPERAND@2..17
1839                                  TWIG_ACCESSOR@2..17
1840                                    TWIG_OPERAND@2..10
1841                                      TWIG_LITERAL_NAME@2..10
1842                                        TK_WHITESPACE@2..3 " "
1843                                        TK_WORD@3..10 "product"
1844                                    TK_DOT@10..11 "."
1845                                    TWIG_OPERAND@11..17
1846                                      TWIG_LITERAL_NAME@11..17
1847                                        TK_WORD@11..17 "prices"
1848                                TK_DOT@17..18 "."
1849                                TWIG_OPERAND@18..23
1850                                  TWIG_LITERAL_NAME@18..23
1851                                    TK_WORD@18..23 "gross"
1852                            TWIG_ARGUMENTS@23..30
1853                              TK_OPEN_PARENTHESIS@23..24 "("
1854                              TWIG_EXPRESSION@24..29
1855                                TWIG_LITERAL_STRING@24..29
1856                                  TK_SINGLE_QUOTES@24..25 "'"
1857                                  TWIG_LITERAL_STRING_INNER@25..28
1858                                    TK_WORD@25..28 "eur"
1859                                  TK_SINGLE_QUOTES@28..29 "'"
1860                              TK_CLOSE_PARENTHESIS@29..30 ")"
1861                        TK_DOT@30..31 "."
1862                        TWIG_OPERAND@31..36
1863                          TWIG_LITERAL_NAME@31..36
1864                            TK_WORD@31..36 "gross"
1865                    TK_WHITESPACE@36..37 " "
1866                    TK_CLOSE_CURLY_CURLY@37..39 "}}""#]],
1867        );
1868    }
1869
1870    #[test]
1871    fn parse_twig_function() {
1872        check_parse(
1873            r"{{ doIt() }}",
1874            expect![[r#"
1875                ROOT@0..12
1876                  TWIG_VAR@0..12
1877                    TK_OPEN_CURLY_CURLY@0..2 "{{"
1878                    TWIG_EXPRESSION@2..9
1879                      TWIG_FUNCTION_CALL@2..9
1880                        TWIG_OPERAND@2..7
1881                          TWIG_LITERAL_NAME@2..7
1882                            TK_WHITESPACE@2..3 " "
1883                            TK_WORD@3..7 "doIt"
1884                        TWIG_ARGUMENTS@7..9
1885                          TK_OPEN_PARENTHESIS@7..8 "("
1886                          TK_CLOSE_PARENTHESIS@8..9 ")"
1887                    TK_WHITESPACE@9..10 " "
1888                    TK_CLOSE_CURLY_CURLY@10..12 "}}""#]],
1889        );
1890    }
1891
1892    #[test]
1893    fn parse_twig_function_arguments() {
1894        check_parse(
1895            r"{{ sum(1, 2) }}",
1896            expect![[r#"
1897                ROOT@0..15
1898                  TWIG_VAR@0..15
1899                    TK_OPEN_CURLY_CURLY@0..2 "{{"
1900                    TWIG_EXPRESSION@2..12
1901                      TWIG_FUNCTION_CALL@2..12
1902                        TWIG_OPERAND@2..6
1903                          TWIG_LITERAL_NAME@2..6
1904                            TK_WHITESPACE@2..3 " "
1905                            TK_WORD@3..6 "sum"
1906                        TWIG_ARGUMENTS@6..12
1907                          TK_OPEN_PARENTHESIS@6..7 "("
1908                          TWIG_EXPRESSION@7..8
1909                            TWIG_LITERAL_NUMBER@7..8
1910                              TK_NUMBER@7..8 "1"
1911                          TK_COMMA@8..9 ","
1912                          TWIG_EXPRESSION@9..11
1913                            TWIG_LITERAL_NUMBER@9..11
1914                              TK_WHITESPACE@9..10 " "
1915                              TK_NUMBER@10..11 "2"
1916                          TK_CLOSE_PARENTHESIS@11..12 ")"
1917                    TK_WHITESPACE@12..13 " "
1918                    TK_CLOSE_CURLY_CURLY@13..15 "}}""#]],
1919        );
1920    }
1921
1922    #[test]
1923    fn parse_twig_function_named_arguments() {
1924        check_parse(
1925            r"{{ sum(a=1, b=2) }}",
1926            expect![[r#"
1927                ROOT@0..19
1928                  TWIG_VAR@0..19
1929                    TK_OPEN_CURLY_CURLY@0..2 "{{"
1930                    TWIG_EXPRESSION@2..16
1931                      TWIG_FUNCTION_CALL@2..16
1932                        TWIG_OPERAND@2..6
1933                          TWIG_LITERAL_NAME@2..6
1934                            TK_WHITESPACE@2..3 " "
1935                            TK_WORD@3..6 "sum"
1936                        TWIG_ARGUMENTS@6..16
1937                          TK_OPEN_PARENTHESIS@6..7 "("
1938                          TWIG_NAMED_ARGUMENT@7..10
1939                            TK_WORD@7..8 "a"
1940                            TK_EQUAL@8..9 "="
1941                            TWIG_EXPRESSION@9..10
1942                              TWIG_LITERAL_NUMBER@9..10
1943                                TK_NUMBER@9..10 "1"
1944                          TK_COMMA@10..11 ","
1945                          TWIG_NAMED_ARGUMENT@11..15
1946                            TK_WHITESPACE@11..12 " "
1947                            TK_WORD@12..13 "b"
1948                            TK_EQUAL@13..14 "="
1949                            TWIG_EXPRESSION@14..15
1950                              TWIG_LITERAL_NUMBER@14..15
1951                                TK_NUMBER@14..15 "2"
1952                          TK_CLOSE_PARENTHESIS@15..16 ")"
1953                    TK_WHITESPACE@16..17 " "
1954                    TK_CLOSE_CURLY_CURLY@17..19 "}}""#]],
1955        );
1956    }
1957
1958    #[test]
1959    fn parse_twig_function_mixed_named_arguments() {
1960        check_parse(
1961            r"{{ sum(1, b=my_number) }}",
1962            expect![[r#"
1963                ROOT@0..25
1964                  TWIG_VAR@0..25
1965                    TK_OPEN_CURLY_CURLY@0..2 "{{"
1966                    TWIG_EXPRESSION@2..22
1967                      TWIG_FUNCTION_CALL@2..22
1968                        TWIG_OPERAND@2..6
1969                          TWIG_LITERAL_NAME@2..6
1970                            TK_WHITESPACE@2..3 " "
1971                            TK_WORD@3..6 "sum"
1972                        TWIG_ARGUMENTS@6..22
1973                          TK_OPEN_PARENTHESIS@6..7 "("
1974                          TWIG_EXPRESSION@7..8
1975                            TWIG_LITERAL_NUMBER@7..8
1976                              TK_NUMBER@7..8 "1"
1977                          TK_COMMA@8..9 ","
1978                          TWIG_NAMED_ARGUMENT@9..21
1979                            TK_WHITESPACE@9..10 " "
1980                            TK_WORD@10..11 "b"
1981                            TK_EQUAL@11..12 "="
1982                            TWIG_EXPRESSION@12..21
1983                              TWIG_LITERAL_NAME@12..21
1984                                TK_WORD@12..21 "my_number"
1985                          TK_CLOSE_PARENTHESIS@21..22 ")"
1986                    TK_WHITESPACE@22..23 " "
1987                    TK_CLOSE_CURLY_CURLY@23..25 "}}""#]],
1988        );
1989    }
1990
1991    #[test]
1992    fn parse_twig_function_nested_call() {
1993        check_parse(
1994            r"{{ sum(1, sin(1)) }}",
1995            expect![[r#"
1996                ROOT@0..20
1997                  TWIG_VAR@0..20
1998                    TK_OPEN_CURLY_CURLY@0..2 "{{"
1999                    TWIG_EXPRESSION@2..17
2000                      TWIG_FUNCTION_CALL@2..17
2001                        TWIG_OPERAND@2..6
2002                          TWIG_LITERAL_NAME@2..6
2003                            TK_WHITESPACE@2..3 " "
2004                            TK_WORD@3..6 "sum"
2005                        TWIG_ARGUMENTS@6..17
2006                          TK_OPEN_PARENTHESIS@6..7 "("
2007                          TWIG_EXPRESSION@7..8
2008                            TWIG_LITERAL_NUMBER@7..8
2009                              TK_NUMBER@7..8 "1"
2010                          TK_COMMA@8..9 ","
2011                          TWIG_EXPRESSION@9..16
2012                            TWIG_FUNCTION_CALL@9..16
2013                              TWIG_OPERAND@9..13
2014                                TWIG_LITERAL_NAME@9..13
2015                                  TK_WHITESPACE@9..10 " "
2016                                  TK_WORD@10..13 "sin"
2017                              TWIG_ARGUMENTS@13..16
2018                                TK_OPEN_PARENTHESIS@13..14 "("
2019                                TWIG_EXPRESSION@14..15
2020                                  TWIG_LITERAL_NUMBER@14..15
2021                                    TK_NUMBER@14..15 "1"
2022                                TK_CLOSE_PARENTHESIS@15..16 ")"
2023                          TK_CLOSE_PARENTHESIS@16..17 ")"
2024                    TK_WHITESPACE@17..18 " "
2025                    TK_CLOSE_CURLY_CURLY@18..20 "}}""#]],
2026        );
2027    }
2028
2029    #[test]
2030    fn parse_twig_arrow_function_simple() {
2031        check_parse(
2032            r"{% set my_arrow_function = i => i % 2 %}",
2033            expect![[r#"
2034                ROOT@0..40
2035                  TWIG_SET@0..40
2036                    TWIG_SET_BLOCK@0..40
2037                      TK_CURLY_PERCENT@0..2 "{%"
2038                      TK_WHITESPACE@2..3 " "
2039                      TK_SET@3..6 "set"
2040                      TWIG_ASSIGNMENT@6..37
2041                        TWIG_LITERAL_NAME@6..24
2042                          TK_WHITESPACE@6..7 " "
2043                          TK_WORD@7..24 "my_arrow_function"
2044                        TK_WHITESPACE@24..25 " "
2045                        TK_EQUAL@25..26 "="
2046                        TWIG_EXPRESSION@26..37
2047                          TWIG_ARROW_FUNCTION@26..37
2048                            TWIG_ARGUMENTS@26..28
2049                              TWIG_LITERAL_NAME@26..28
2050                                TK_WHITESPACE@26..27 " "
2051                                TK_WORD@27..28 "i"
2052                            TK_WHITESPACE@28..29 " "
2053                            TK_EQUAL_GREATER_THAN@29..31 "=>"
2054                            TWIG_EXPRESSION@31..37
2055                              TWIG_BINARY_EXPRESSION@31..37
2056                                TWIG_EXPRESSION@31..33
2057                                  TWIG_LITERAL_NAME@31..33
2058                                    TK_WHITESPACE@31..32 " "
2059                                    TK_WORD@32..33 "i"
2060                                TK_WHITESPACE@33..34 " "
2061                                TK_PERCENT@34..35 "%"
2062                                TWIG_EXPRESSION@35..37
2063                                  TWIG_LITERAL_NUMBER@35..37
2064                                    TK_WHITESPACE@35..36 " "
2065                                    TK_NUMBER@36..37 "2"
2066                      TK_WHITESPACE@37..38 " "
2067                      TK_PERCENT_CURLY@38..40 "%}""#]],
2068        );
2069    }
2070
2071    #[test]
2072    fn parse_twig_arrow_function_simple_brackets() {
2073        check_parse(
2074            r"{% set my_arrow_function = (i) => i % 2 %}",
2075            expect![[r#"
2076                ROOT@0..42
2077                  TWIG_SET@0..42
2078                    TWIG_SET_BLOCK@0..42
2079                      TK_CURLY_PERCENT@0..2 "{%"
2080                      TK_WHITESPACE@2..3 " "
2081                      TK_SET@3..6 "set"
2082                      TWIG_ASSIGNMENT@6..39
2083                        TWIG_LITERAL_NAME@6..24
2084                          TK_WHITESPACE@6..7 " "
2085                          TK_WORD@7..24 "my_arrow_function"
2086                        TK_WHITESPACE@24..25 " "
2087                        TK_EQUAL@25..26 "="
2088                        TWIG_EXPRESSION@26..39
2089                          TWIG_ARROW_FUNCTION@26..39
2090                            TWIG_ARGUMENTS@26..30
2091                              TK_WHITESPACE@26..27 " "
2092                              TK_OPEN_PARENTHESIS@27..28 "("
2093                              TWIG_LITERAL_NAME@28..29
2094                                TK_WORD@28..29 "i"
2095                              TK_CLOSE_PARENTHESIS@29..30 ")"
2096                            TK_WHITESPACE@30..31 " "
2097                            TK_EQUAL_GREATER_THAN@31..33 "=>"
2098                            TWIG_EXPRESSION@33..39
2099                              TWIG_BINARY_EXPRESSION@33..39
2100                                TWIG_EXPRESSION@33..35
2101                                  TWIG_LITERAL_NAME@33..35
2102                                    TK_WHITESPACE@33..34 " "
2103                                    TK_WORD@34..35 "i"
2104                                TK_WHITESPACE@35..36 " "
2105                                TK_PERCENT@36..37 "%"
2106                                TWIG_EXPRESSION@37..39
2107                                  TWIG_LITERAL_NUMBER@37..39
2108                                    TK_WHITESPACE@37..38 " "
2109                                    TK_NUMBER@38..39 "2"
2110                      TK_WHITESPACE@39..40 " "
2111                      TK_PERCENT_CURLY@40..42 "%}""#]],
2112        );
2113    }
2114
2115    #[test]
2116    fn parse_twig_arrow_function_advanced() {
2117        check_parse(
2118            r"{% set my_arrow_function = (a, b) => a >= b %}",
2119            expect![[r#"
2120                ROOT@0..46
2121                  TWIG_SET@0..46
2122                    TWIG_SET_BLOCK@0..46
2123                      TK_CURLY_PERCENT@0..2 "{%"
2124                      TK_WHITESPACE@2..3 " "
2125                      TK_SET@3..6 "set"
2126                      TWIG_ASSIGNMENT@6..43
2127                        TWIG_LITERAL_NAME@6..24
2128                          TK_WHITESPACE@6..7 " "
2129                          TK_WORD@7..24 "my_arrow_function"
2130                        TK_WHITESPACE@24..25 " "
2131                        TK_EQUAL@25..26 "="
2132                        TWIG_EXPRESSION@26..43
2133                          TWIG_ARROW_FUNCTION@26..43
2134                            TWIG_ARGUMENTS@26..33
2135                              TK_WHITESPACE@26..27 " "
2136                              TK_OPEN_PARENTHESIS@27..28 "("
2137                              TWIG_LITERAL_NAME@28..29
2138                                TK_WORD@28..29 "a"
2139                              TK_COMMA@29..30 ","
2140                              TWIG_LITERAL_NAME@30..32
2141                                TK_WHITESPACE@30..31 " "
2142                                TK_WORD@31..32 "b"
2143                              TK_CLOSE_PARENTHESIS@32..33 ")"
2144                            TK_WHITESPACE@33..34 " "
2145                            TK_EQUAL_GREATER_THAN@34..36 "=>"
2146                            TWIG_EXPRESSION@36..43
2147                              TWIG_BINARY_EXPRESSION@36..43
2148                                TWIG_EXPRESSION@36..38
2149                                  TWIG_LITERAL_NAME@36..38
2150                                    TK_WHITESPACE@36..37 " "
2151                                    TK_WORD@37..38 "a"
2152                                TK_WHITESPACE@38..39 " "
2153                                TK_GREATER_THAN_EQUAL@39..41 ">="
2154                                TWIG_EXPRESSION@41..43
2155                                  TWIG_LITERAL_NAME@41..43
2156                                    TK_WHITESPACE@41..42 " "
2157                                    TK_WORD@42..43 "b"
2158                      TK_WHITESPACE@43..44 " "
2159                      TK_PERCENT_CURLY@44..46 "%}""#]],
2160        );
2161    }
2162
2163    #[test]
2164    fn parse_twig_arrow_function_as_filer_argument() {
2165        check_parse(
2166            r"{% for item in crossSellings|filter(item => item.total > 0 and item.crossSelling.active == true) %} {{ item }} {% endfor %}",
2167            expect![[r#"
2168                ROOT@0..123
2169                  TWIG_FOR@0..123
2170                    TWIG_FOR_BLOCK@0..99
2171                      TK_CURLY_PERCENT@0..2 "{%"
2172                      TK_WHITESPACE@2..3 " "
2173                      TK_FOR@3..6 "for"
2174                      TWIG_LITERAL_NAME@6..11
2175                        TK_WHITESPACE@6..7 " "
2176                        TK_WORD@7..11 "item"
2177                      TK_WHITESPACE@11..12 " "
2178                      TK_IN@12..14 "in"
2179                      TWIG_EXPRESSION@14..96
2180                        TWIG_FILTER@14..96
2181                          TWIG_OPERAND@14..28
2182                            TWIG_LITERAL_NAME@14..28
2183                              TK_WHITESPACE@14..15 " "
2184                              TK_WORD@15..28 "crossSellings"
2185                          TK_SINGLE_PIPE@28..29 "|"
2186                          TWIG_OPERAND@29..96
2187                            TWIG_LITERAL_NAME@29..35
2188                              TK_WORD@29..35 "filter"
2189                            TWIG_ARGUMENTS@35..96
2190                              TK_OPEN_PARENTHESIS@35..36 "("
2191                              TWIG_EXPRESSION@36..95
2192                                TWIG_ARROW_FUNCTION@36..95
2193                                  TWIG_ARGUMENTS@36..40
2194                                    TWIG_LITERAL_NAME@36..40
2195                                      TK_WORD@36..40 "item"
2196                                  TK_WHITESPACE@40..41 " "
2197                                  TK_EQUAL_GREATER_THAN@41..43 "=>"
2198                                  TWIG_EXPRESSION@43..95
2199                                    TWIG_BINARY_EXPRESSION@43..95
2200                                      TWIG_BINARY_EXPRESSION@43..58
2201                                        TWIG_EXPRESSION@43..54
2202                                          TWIG_ACCESSOR@43..54
2203                                            TWIG_OPERAND@43..48
2204                                              TWIG_LITERAL_NAME@43..48
2205                                                TK_WHITESPACE@43..44 " "
2206                                                TK_WORD@44..48 "item"
2207                                            TK_DOT@48..49 "."
2208                                            TWIG_OPERAND@49..54
2209                                              TWIG_LITERAL_NAME@49..54
2210                                                TK_WORD@49..54 "total"
2211                                        TK_WHITESPACE@54..55 " "
2212                                        TK_GREATER_THAN@55..56 ">"
2213                                        TWIG_EXPRESSION@56..58
2214                                          TWIG_LITERAL_NUMBER@56..58
2215                                            TK_WHITESPACE@56..57 " "
2216                                            TK_NUMBER@57..58 "0"
2217                                      TK_WHITESPACE@58..59 " "
2218                                      TK_AND@59..62 "and"
2219                                      TWIG_EXPRESSION@62..95
2220                                        TWIG_BINARY_EXPRESSION@62..95
2221                                          TWIG_EXPRESSION@62..87
2222                                            TWIG_ACCESSOR@62..87
2223                                              TWIG_OPERAND@62..80
2224                                                TWIG_ACCESSOR@62..80
2225                                                  TWIG_OPERAND@62..67
2226                                                    TWIG_LITERAL_NAME@62..67
2227                                                      TK_WHITESPACE@62..63 " "
2228                                                      TK_WORD@63..67 "item"
2229                                                  TK_DOT@67..68 "."
2230                                                  TWIG_OPERAND@68..80
2231                                                    TWIG_LITERAL_NAME@68..80
2232                                                      TK_WORD@68..80 "crossSelling"
2233                                              TK_DOT@80..81 "."
2234                                              TWIG_OPERAND@81..87
2235                                                TWIG_LITERAL_NAME@81..87
2236                                                  TK_WORD@81..87 "active"
2237                                          TK_WHITESPACE@87..88 " "
2238                                          TK_DOUBLE_EQUAL@88..90 "=="
2239                                          TWIG_EXPRESSION@90..95
2240                                            TWIG_LITERAL_BOOLEAN@90..95
2241                                              TK_WHITESPACE@90..91 " "
2242                                              TK_TRUE@91..95 "true"
2243                              TK_CLOSE_PARENTHESIS@95..96 ")"
2244                      TK_WHITESPACE@96..97 " "
2245                      TK_PERCENT_CURLY@97..99 "%}"
2246                    BODY@99..110
2247                      TWIG_VAR@99..110
2248                        TK_WHITESPACE@99..100 " "
2249                        TK_OPEN_CURLY_CURLY@100..102 "{{"
2250                        TWIG_EXPRESSION@102..107
2251                          TWIG_LITERAL_NAME@102..107
2252                            TK_WHITESPACE@102..103 " "
2253                            TK_WORD@103..107 "item"
2254                        TK_WHITESPACE@107..108 " "
2255                        TK_CLOSE_CURLY_CURLY@108..110 "}}"
2256                    TWIG_ENDFOR_BLOCK@110..123
2257                      TK_WHITESPACE@110..111 " "
2258                      TK_CURLY_PERCENT@111..113 "{%"
2259                      TK_WHITESPACE@113..114 " "
2260                      TK_ENDFOR@114..120 "endfor"
2261                      TK_WHITESPACE@120..121 " "
2262                      TK_PERCENT_CURLY@121..123 "%}""#]],
2263        );
2264    }
2265
2266    #[test]
2267    fn parse_twig_filter_arguments() {
2268        check_parse(
2269            r"{{ list|join(', ') }}",
2270            expect![[r#"
2271                ROOT@0..21
2272                  TWIG_VAR@0..21
2273                    TK_OPEN_CURLY_CURLY@0..2 "{{"
2274                    TWIG_EXPRESSION@2..18
2275                      TWIG_FILTER@2..18
2276                        TWIG_OPERAND@2..7
2277                          TWIG_LITERAL_NAME@2..7
2278                            TK_WHITESPACE@2..3 " "
2279                            TK_WORD@3..7 "list"
2280                        TK_SINGLE_PIPE@7..8 "|"
2281                        TWIG_OPERAND@8..18
2282                          TWIG_LITERAL_NAME@8..12
2283                            TK_WORD@8..12 "join"
2284                          TWIG_ARGUMENTS@12..18
2285                            TK_OPEN_PARENTHESIS@12..13 "("
2286                            TWIG_EXPRESSION@13..17
2287                              TWIG_LITERAL_STRING@13..17
2288                                TK_SINGLE_QUOTES@13..14 "'"
2289                                TWIG_LITERAL_STRING_INNER@14..16
2290                                  TK_COMMA@14..15 ","
2291                                  TK_WHITESPACE@15..16 " "
2292                                TK_SINGLE_QUOTES@16..17 "'"
2293                            TK_CLOSE_PARENTHESIS@17..18 ")"
2294                    TK_WHITESPACE@18..19 " "
2295                    TK_CLOSE_CURLY_CURLY@19..21 "}}""#]],
2296        );
2297    }
2298
2299    #[test]
2300    fn parse_twig_double_filter_arguments() {
2301        check_parse(
2302            r"{{ list|join(', ')|trim }}",
2303            expect![[r#"
2304                ROOT@0..26
2305                  TWIG_VAR@0..26
2306                    TK_OPEN_CURLY_CURLY@0..2 "{{"
2307                    TWIG_EXPRESSION@2..23
2308                      TWIG_FILTER@2..23
2309                        TWIG_OPERAND@2..18
2310                          TWIG_FILTER@2..18
2311                            TWIG_OPERAND@2..7
2312                              TWIG_LITERAL_NAME@2..7
2313                                TK_WHITESPACE@2..3 " "
2314                                TK_WORD@3..7 "list"
2315                            TK_SINGLE_PIPE@7..8 "|"
2316                            TWIG_OPERAND@8..18
2317                              TWIG_LITERAL_NAME@8..12
2318                                TK_WORD@8..12 "join"
2319                              TWIG_ARGUMENTS@12..18
2320                                TK_OPEN_PARENTHESIS@12..13 "("
2321                                TWIG_EXPRESSION@13..17
2322                                  TWIG_LITERAL_STRING@13..17
2323                                    TK_SINGLE_QUOTES@13..14 "'"
2324                                    TWIG_LITERAL_STRING_INNER@14..16
2325                                      TK_COMMA@14..15 ","
2326                                      TK_WHITESPACE@15..16 " "
2327                                    TK_SINGLE_QUOTES@16..17 "'"
2328                                TK_CLOSE_PARENTHESIS@17..18 ")"
2329                        TK_SINGLE_PIPE@18..19 "|"
2330                        TWIG_OPERAND@19..23
2331                          TWIG_LITERAL_NAME@19..23
2332                            TK_WORD@19..23 "trim"
2333                    TK_WHITESPACE@23..24 " "
2334                    TK_CLOSE_CURLY_CURLY@24..26 "}}""#]],
2335        );
2336    }
2337
2338    #[test]
2339    fn parse_twig_filter_after_string_with_named_argument() {
2340        check_parse(
2341            r#"{{ "now"|date('d/m/Y H:i', timezone="Europe/Paris") }}"#,
2342            expect![[r#"
2343                ROOT@0..54
2344                  TWIG_VAR@0..54
2345                    TK_OPEN_CURLY_CURLY@0..2 "{{"
2346                    TWIG_EXPRESSION@2..51
2347                      TWIG_FILTER@2..51
2348                        TWIG_OPERAND@2..8
2349                          TWIG_LITERAL_STRING@2..8
2350                            TK_WHITESPACE@2..3 " "
2351                            TK_DOUBLE_QUOTES@3..4 "\""
2352                            TWIG_LITERAL_STRING_INNER@4..7
2353                              TK_WORD@4..7 "now"
2354                            TK_DOUBLE_QUOTES@7..8 "\""
2355                        TK_SINGLE_PIPE@8..9 "|"
2356                        TWIG_OPERAND@9..51
2357                          TWIG_LITERAL_NAME@9..13
2358                            TK_WORD@9..13 "date"
2359                          TWIG_ARGUMENTS@13..51
2360                            TK_OPEN_PARENTHESIS@13..14 "("
2361                            TWIG_EXPRESSION@14..25
2362                              TWIG_LITERAL_STRING@14..25
2363                                TK_SINGLE_QUOTES@14..15 "'"
2364                                TWIG_LITERAL_STRING_INNER@15..24
2365                                  TK_WORD@15..16 "d"
2366                                  TK_FORWARD_SLASH@16..17 "/"
2367                                  TK_WORD@17..18 "m"
2368                                  TK_FORWARD_SLASH@18..19 "/"
2369                                  TK_WORD@19..20 "Y"
2370                                  TK_WHITESPACE@20..21 " "
2371                                  TK_WORD@21..22 "H"
2372                                  TK_COLON@22..23 ":"
2373                                  TK_WORD@23..24 "i"
2374                                TK_SINGLE_QUOTES@24..25 "'"
2375                            TK_COMMA@25..26 ","
2376                            TWIG_NAMED_ARGUMENT@26..50
2377                              TK_WHITESPACE@26..27 " "
2378                              TK_WORD@27..35 "timezone"
2379                              TK_EQUAL@35..36 "="
2380                              TWIG_EXPRESSION@36..50
2381                                TWIG_LITERAL_STRING@36..50
2382                                  TK_DOUBLE_QUOTES@36..37 "\""
2383                                  TWIG_LITERAL_STRING_INNER@37..49
2384                                    TK_WORD@37..43 "Europe"
2385                                    TK_FORWARD_SLASH@43..44 "/"
2386                                    TK_WORD@44..49 "Paris"
2387                                  TK_DOUBLE_QUOTES@49..50 "\""
2388                            TK_CLOSE_PARENTHESIS@50..51 ")"
2389                    TK_WHITESPACE@51..52 " "
2390                    TK_CLOSE_CURLY_CURLY@52..54 "}}""#]],
2391        );
2392    }
2393
2394    #[test]
2395    fn parse_twig_filter_within_binary_comparison() {
2396        check_parse(
2397            r"{{ users|length > 0 }}",
2398            expect![[r#"
2399                ROOT@0..22
2400                  TWIG_VAR@0..22
2401                    TK_OPEN_CURLY_CURLY@0..2 "{{"
2402                    TWIG_EXPRESSION@2..19
2403                      TWIG_BINARY_EXPRESSION@2..19
2404                        TWIG_EXPRESSION@2..15
2405                          TWIG_FILTER@2..15
2406                            TWIG_OPERAND@2..8
2407                              TWIG_LITERAL_NAME@2..8
2408                                TK_WHITESPACE@2..3 " "
2409                                TK_WORD@3..8 "users"
2410                            TK_SINGLE_PIPE@8..9 "|"
2411                            TWIG_OPERAND@9..15
2412                              TWIG_LITERAL_NAME@9..15
2413                                TK_WORD@9..15 "length"
2414                        TK_WHITESPACE@15..16 " "
2415                        TK_GREATER_THAN@16..17 ">"
2416                        TWIG_EXPRESSION@17..19
2417                          TWIG_LITERAL_NUMBER@17..19
2418                            TK_WHITESPACE@17..18 " "
2419                            TK_NUMBER@18..19 "0"
2420                    TK_WHITESPACE@19..20 " "
2421                    TK_CLOSE_CURLY_CURLY@20..22 "}}""#]],
2422        );
2423    }
2424
2425    #[test]
2426    fn parse_twig_include_function_call() {
2427        check_parse(
2428            r"{{ include('sections/articles/sidebar.html') }}",
2429            expect![[r#"
2430                ROOT@0..47
2431                  TWIG_VAR@0..47
2432                    TK_OPEN_CURLY_CURLY@0..2 "{{"
2433                    TWIG_EXPRESSION@2..44
2434                      TWIG_FUNCTION_CALL@2..44
2435                        TWIG_OPERAND@2..10
2436                          TWIG_LITERAL_NAME@2..10
2437                            TK_WHITESPACE@2..3 " "
2438                            TK_WORD@3..10 "include"
2439                        TWIG_ARGUMENTS@10..44
2440                          TK_OPEN_PARENTHESIS@10..11 "("
2441                          TWIG_EXPRESSION@11..43
2442                            TWIG_LITERAL_STRING@11..43
2443                              TK_SINGLE_QUOTES@11..12 "'"
2444                              TWIG_LITERAL_STRING_INNER@12..42
2445                                TK_WORD@12..20 "sections"
2446                                TK_FORWARD_SLASH@20..21 "/"
2447                                TK_WORD@21..29 "articles"
2448                                TK_FORWARD_SLASH@29..30 "/"
2449                                TK_WORD@30..37 "sidebar"
2450                                TK_DOT@37..38 "."
2451                                TK_WORD@38..42 "html"
2452                              TK_SINGLE_QUOTES@42..43 "'"
2453                          TK_CLOSE_PARENTHESIS@43..44 ")"
2454                    TK_WHITESPACE@44..45 " "
2455                    TK_CLOSE_CURLY_CURLY@45..47 "}}""#]],
2456        );
2457    }
2458}