Skip to main content

i_slint_compiler/parser/
expressions.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4use super::document::parse_qualified_name;
5use super::prelude::*;
6
7#[cfg_attr(test, parser_test)]
8/// ```test,Expression
9/// something
10/// "something"
11/// 0.3
12/// 42
13/// 42px
14/// #aabbcc
15/// (something)
16/// (something).something
17/// @image-url("something")
18/// @image_url("something")
19/// some_id.some_property
20/// function_call()
21/// function_call(hello, world)
22/// cond ? first : second
23/// call_cond() ? first : second
24/// (nested()) ? (ok) : (other.ko)
25/// 4 + 4
26/// 4 + 8 * 7 / 5 + 3 - 7 - 7 * 8
27/// -0.3px + 0.3px - 3.pt+3pt
28/// aa == cc && bb && (xxx || fff) && 3 + aaa == bbb
29/// [array]
30/// array[index]
31/// {object:42}
32/// "foo".bar.something().something.xx({a: 1.foo}.a)
33/// (x) => x > 0
34/// ```
35pub fn parse_expression(p: &mut impl Parser) -> bool {
36    p.peek(); // consume the whitespace so they aren't part of the Expression node
37    parse_expression_helper(p, OperatorPrecedence::Default)
38}
39
40#[derive(Eq, PartialEq, Ord, PartialOrd)]
41#[repr(u8)]
42enum OperatorPrecedence {
43    /// ` ?: `
44    Default,
45    /// `||`, `&&`
46    Logical,
47    /// `==` `!=` `>=` `<=` `<` `>`
48    Equality,
49    /// `+ -`
50    Add,
51    /// `* /`
52    Mul,
53    Unary,
54}
55
56fn parse_expression_helper(p: &mut impl Parser, precedence: OperatorPrecedence) -> bool {
57    let mut p = p.start_node(SyntaxKind::Expression);
58    let checkpoint = p.checkpoint();
59    let mut possible_range = false;
60    match p.nth(0).kind() {
61        SyntaxKind::Identifier => {
62            parse_qualified_name(&mut *p);
63        }
64        SyntaxKind::StringLiteral => {
65            if p.nth(0).as_str().ends_with('{') {
66                parse_template_string(&mut *p)
67            } else {
68                p.consume()
69            }
70        }
71        SyntaxKind::NumberLiteral => {
72            if p.nth(0).as_str().ends_with('.') {
73                possible_range = true;
74            }
75            p.consume()
76        }
77        SyntaxKind::ColorLiteral => p.consume(),
78        SyntaxKind::LParent => {
79            if p.nth(1).kind() == SyntaxKind::Identifier
80                && p.nth(2).kind() == SyntaxKind::RParent
81                && p.nth(3).kind() == SyntaxKind::FatArrow
82            {
83                parse_closure(&mut *p);
84            } else {
85                p.consume();
86                parse_expression(&mut *p);
87                p.expect(SyntaxKind::RParent);
88            }
89        }
90        SyntaxKind::LBracket => parse_array(&mut *p),
91        SyntaxKind::LBrace => parse_object_notation(&mut *p),
92        SyntaxKind::Plus | SyntaxKind::Minus | SyntaxKind::Bang => {
93            let mut p = p.start_node(SyntaxKind::UnaryOpExpression);
94            p.consume();
95            parse_expression_helper(&mut *p, OperatorPrecedence::Unary);
96        }
97        SyntaxKind::At => {
98            parse_at_keyword(&mut *p);
99        }
100        _ => {
101            p.error("invalid expression");
102            return false;
103        }
104    }
105
106    loop {
107        match p.nth(0).kind() {
108            SyntaxKind::Dot => {
109                {
110                    let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
111                }
112                let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::MemberAccess);
113                p.consume(); // '.'
114                if possible_range && p.peek().kind() == SyntaxKind::NumberLiteral {
115                    let error = format!(
116                        "Parse error. Range expressions are not supported in Slint. You can use an integer as a model to repeat something multiple time. Eg: `for i in {} : ...`",
117                        p.peek().as_str()
118                    );
119                    p.error(error);
120                    p.consume();
121                    return false;
122                }
123                if !p.expect(SyntaxKind::Identifier) {
124                    return false;
125                }
126            }
127            SyntaxKind::LParent => {
128                {
129                    let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
130                }
131                let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::FunctionCallExpression);
132                parse_function_arguments(&mut *p);
133            }
134            SyntaxKind::LBracket => {
135                {
136                    let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
137                }
138                let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::IndexExpression);
139                p.expect(SyntaxKind::LBracket);
140                parse_expression(&mut *p);
141                p.expect(SyntaxKind::RBracket);
142            }
143            _ => break,
144        }
145        possible_range = false;
146    }
147
148    if precedence >= OperatorPrecedence::Mul {
149        return true;
150    }
151
152    while matches!(p.nth(0).kind(), SyntaxKind::Star | SyntaxKind::Div) {
153        {
154            let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
155        }
156        let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::BinaryExpression);
157        p.consume();
158        parse_expression_helper(&mut *p, OperatorPrecedence::Mul);
159    }
160
161    if p.nth(0).kind() == SyntaxKind::Percent {
162        p.error("Unexpected '%'. For the unit, it should be attached to the number. If you're looking for the modulo operator, use the 'Math.mod(x, y)' function");
163        p.consume();
164        return false;
165    }
166
167    if precedence >= OperatorPrecedence::Add {
168        return true;
169    }
170
171    while matches!(p.nth(0).kind(), SyntaxKind::Plus | SyntaxKind::Minus) {
172        {
173            let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
174        }
175        let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::BinaryExpression);
176        p.consume();
177        parse_expression_helper(&mut *p, OperatorPrecedence::Add);
178    }
179
180    if precedence > OperatorPrecedence::Equality {
181        return true;
182    }
183
184    if matches!(
185        p.nth(0).kind(),
186        SyntaxKind::LessEqual
187            | SyntaxKind::GreaterEqual
188            | SyntaxKind::EqualEqual
189            | SyntaxKind::NotEqual
190            | SyntaxKind::LAngle
191            | SyntaxKind::RAngle
192    ) {
193        if precedence == OperatorPrecedence::Equality {
194            p.error("Use parentheses to disambiguate equality expression on the same level");
195        }
196
197        {
198            let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
199        }
200        let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::BinaryExpression);
201        p.consume();
202        parse_expression_helper(&mut *p, OperatorPrecedence::Equality);
203    }
204
205    if precedence >= OperatorPrecedence::Logical {
206        return true;
207    }
208
209    let mut prev_logical_op = None;
210    while matches!(p.nth(0).kind(), SyntaxKind::AndAnd | SyntaxKind::OrOr) {
211        if let Some(prev) = prev_logical_op {
212            if prev != p.nth(0).kind() {
213                p.error("Use parentheses to disambiguate between && and ||");
214                prev_logical_op = None;
215            }
216        } else {
217            prev_logical_op = Some(p.nth(0).kind());
218        }
219
220        {
221            let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
222        }
223        let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::BinaryExpression);
224        p.consume();
225        parse_expression_helper(&mut *p, OperatorPrecedence::Logical);
226    }
227
228    if p.nth(0).kind() == SyntaxKind::Question {
229        {
230            let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
231        }
232        let mut p = p.start_node_at(checkpoint, SyntaxKind::ConditionalExpression);
233        p.consume();
234        parse_expression(&mut *p);
235        p.expect(SyntaxKind::Colon);
236        parse_expression(&mut *p);
237    }
238    true
239}
240
241#[cfg_attr(test, parser_test)]
242/// ```test
243/// (x) => x > 0
244/// (y) => y == 42
245/// (z) => true
246/// ```
247fn parse_closure(p: &mut impl Parser) {
248    let mut p = p.start_node(SyntaxKind::Closure);
249
250    p.expect(SyntaxKind::LParent);
251
252    {
253        let mut p = p.start_node(SyntaxKind::DeclaredIdentifier);
254        p.expect(SyntaxKind::Identifier);
255    }
256
257    p.expect(SyntaxKind::RParent);
258
259    p.expect(SyntaxKind::FatArrow);
260
261    parse_expression(&mut *p);
262}
263
264#[cfg_attr(test, parser_test)]
265/// ```test
266/// @image-url("/foo/bar.png")
267/// @linear-gradient(0deg, blue, red)
268/// @conic-gradient(blue 0deg, red 180deg)
269/// @tr("foo", bar)
270/// ```
271fn parse_at_keyword(p: &mut impl Parser) {
272    debug_assert_eq!(p.peek().kind(), SyntaxKind::At);
273    match p.nth(1).as_str() {
274        "image-url" | "image_url" => {
275            parse_image_url(p);
276        }
277        "linear-gradient" | "linear_gradient" => {
278            parse_gradient(p);
279        }
280        "radial-gradient" | "radial_gradient" => {
281            parse_gradient(p);
282        }
283        "conic-gradient" | "conic_gradient" => {
284            parse_gradient(p);
285        }
286        "tr" => {
287            parse_tr(p);
288        }
289        "markdown" => {
290            parse_markdown(p);
291        }
292        "keys" => {
293            parse_keys(p);
294        }
295        _ => {
296            p.consume();
297            p.test(SyntaxKind::Identifier); // consume the identifier, so that autocomplete works
298            p.error("Expected 'image-url', 'tr', 'keys', 'markdown' 'conic-gradient', 'linear-gradient', or 'radial-gradient' after '@'");
299        }
300    }
301}
302
303#[cfg_attr(test, parser_test)]
304/// ```test,Array
305/// [ a, b, c , d]
306/// []
307/// [a,]
308/// [ [], [] ]
309/// ```
310fn parse_array(p: &mut impl Parser) {
311    let mut p = p.start_node(SyntaxKind::Array);
312    p.expect(SyntaxKind::LBracket);
313
314    while p.nth(0).kind() != SyntaxKind::RBracket {
315        parse_expression(&mut *p);
316        if !p.test(SyntaxKind::Comma) {
317            break;
318        }
319    }
320    p.expect(SyntaxKind::RBracket);
321}
322
323#[cfg_attr(test, parser_test)]
324/// ```test,ObjectLiteral
325/// {}
326/// {a:b}
327/// { a: "foo" , }
328/// {a:b, c: 4 + 4, d: [a,] }
329/// ```
330fn parse_object_notation(p: &mut impl Parser) {
331    let mut p = p.start_node(SyntaxKind::ObjectLiteral);
332    p.expect(SyntaxKind::LBrace);
333
334    while p.nth(0).kind() != SyntaxKind::RBrace {
335        let mut p = p.start_node(SyntaxKind::ObjectMember);
336        p.expect(SyntaxKind::Identifier);
337        p.expect(SyntaxKind::Colon);
338        parse_expression(&mut *p);
339        if !p.test(SyntaxKind::Comma) {
340            break;
341        }
342    }
343    p.expect(SyntaxKind::RBrace);
344}
345
346#[cfg_attr(test, parser_test)]
347/// ```test
348/// ()
349/// (foo)
350/// (foo, bar, foo)
351/// (foo, bar(), xx+xx,)
352/// ```
353fn parse_function_arguments(p: &mut impl Parser) {
354    p.expect(SyntaxKind::LParent);
355
356    while p.nth(0).kind() != SyntaxKind::RParent {
357        parse_expression(&mut *p);
358        if !p.test(SyntaxKind::Comma) {
359            break;
360        }
361    }
362    p.expect(SyntaxKind::RParent);
363}
364
365#[cfg_attr(test, parser_test)]
366/// ```test,StringTemplate
367/// "foo\{bar}"
368/// "foo\{4 + 5}foo"
369/// ```
370fn parse_template_string(p: &mut impl Parser) {
371    let mut p = p.start_node(SyntaxKind::StringTemplate);
372    debug_assert!(p.nth(0).as_str().ends_with("\\{"));
373    p.expect(SyntaxKind::StringLiteral);
374    loop {
375        parse_expression(&mut *p);
376        let peek = p.peek();
377        if peek.kind != SyntaxKind::StringLiteral || !peek.as_str().starts_with('}') {
378            p.error("Error while parsing string template")
379        }
380        let cont = peek.as_str().ends_with('{');
381        p.consume();
382        if !cont {
383            break;
384        }
385    }
386}
387
388#[cfg_attr(test, parser_test)]
389/// ```test,AtGradient
390/// @linear-gradient(#e66465, #9198e5)
391/// @linear-gradient(0.25turn, #3f87a6, #ebf8e1, #f69d3c)
392/// @linear-gradient(to left, #333, #333 50%, #eee 75%, #333 75%)
393/// @linear-gradient(217deg, rgba(255,0,0,0.8), rgba(255,0,0,0) 70.71%)
394/// @linear_gradient(217deg, rgba(255,0,0,0.8), rgba(255,0,0,0) 70.71%)
395/// @radial-gradient(circle, #e66465, blue 50%, #9198e5)
396/// @conic-gradient(#e66465 0deg, #9198e5 180deg, #e66465 360deg)
397/// @conic-gradient(red 0deg, green 120deg, blue 240deg, red 360deg)
398/// @conic-gradient(#fff 0turn, #000 0.5turn, #fff 1turn)
399/// @conic_gradient(red 0rad, blue 3.14159rad, red 6.28318rad)
400/// ```
401fn parse_gradient(p: &mut impl Parser) {
402    let mut p = p.start_node(SyntaxKind::AtGradient);
403    p.expect(SyntaxKind::At);
404    debug_assert!(p.peek().as_str().ends_with("gradient"));
405    p.expect(SyntaxKind::Identifier); //eg "linear-gradient"
406
407    p.expect(SyntaxKind::LParent);
408
409    while !p.test(SyntaxKind::RParent) {
410        if !parse_expression(&mut *p) {
411            return;
412        }
413        p.test(SyntaxKind::Comma);
414    }
415}
416
417#[cfg_attr(test, parser_test)]
418/// ```test,AtTr
419/// @tr("foo")
420/// @tr("foo{0}", bar(42))
421/// @tr("context" => "ccc{}", 0)
422/// @tr("xxx" => "ccc{n}" | "ddd{}" % 42, 45)
423/// ```
424fn parse_tr(p: &mut impl Parser) {
425    let mut p = p.start_node(SyntaxKind::AtTr);
426    p.expect(SyntaxKind::At);
427    debug_assert_eq!(p.peek().as_str(), "tr");
428    p.expect(SyntaxKind::Identifier); //"tr"
429    p.expect(SyntaxKind::LParent);
430
431    let checkpoint = p.checkpoint();
432
433    fn consume_literal(p: &mut impl Parser) -> bool {
434        let peek = p.peek();
435        if peek.kind() != SyntaxKind::StringLiteral
436            || !peek.as_str().starts_with('"')
437            || !peek.as_str().ends_with('"')
438        {
439            p.error("Expected plain string literal");
440            return false;
441        }
442        p.expect(SyntaxKind::StringLiteral)
443    }
444
445    if !consume_literal(&mut *p) {
446        return;
447    }
448
449    if p.test(SyntaxKind::FatArrow) {
450        drop(p.start_node_at(checkpoint, SyntaxKind::TrContext));
451        if !consume_literal(&mut *p) {
452            return;
453        }
454    }
455
456    if p.peek().kind() == SyntaxKind::Pipe {
457        let mut p = p.start_node(SyntaxKind::TrPlural);
458        p.consume();
459        if !consume_literal(&mut *p) || !p.expect(SyntaxKind::Percent) {
460            let _ = p.start_node(SyntaxKind::Expression);
461            return;
462        }
463        parse_expression(&mut *p);
464    }
465
466    while p.test(SyntaxKind::Comma) {
467        if !parse_expression(&mut *p) {
468            break;
469        }
470    }
471    p.expect(SyntaxKind::RParent);
472}
473
474/// ```test,AtTr
475/// @markdown("foo")
476/// @markdown("foo\{bar(42)} xx")
477/// @markdown("foo\n" "bar")
478/// ```
479fn parse_markdown(p: &mut impl Parser) {
480    let mut p = p.start_node(SyntaxKind::AtMarkdown);
481    p.expect(SyntaxKind::At);
482    debug_assert!(p.peek().as_str().ends_with("markdown"));
483    p.expect(SyntaxKind::Identifier); //eg "markdown"
484    p.expect(SyntaxKind::LParent);
485
486    let mut has_content = false;
487    loop {
488        let peek = p.peek();
489        if peek.kind() != SyntaxKind::StringLiteral {
490            break;
491        }
492        if peek.as_str().ends_with('{') {
493            parse_template_string(&mut *p)
494        } else {
495            p.consume()
496        }
497        has_content = true;
498    }
499
500    if !has_content {
501        p.error("Expected string literal");
502        p.until(SyntaxKind::RParent);
503        return;
504    }
505
506    if !p.expect(SyntaxKind::RParent) {
507        p.until(SyntaxKind::RParent);
508    }
509}
510
511#[cfg_attr(test, parser_test)]
512/// ```test,AtKeys
513/// @keys()
514/// @keys("x")
515/// @keys(Control +Shift + Alt+Meta+"A")
516/// @keys(Control +Shift + Alt+Meta+Return)
517/// @keys(Control +Shift? + Alt+Meta+Return)
518/// @keys(Control +Shift + Alt?+Meta+Return)
519/// ```
520fn parse_keys(p: &mut impl Parser) {
521    let mut p = p.start_node(SyntaxKind::AtKeys);
522    p.expect(SyntaxKind::At);
523    debug_assert_eq!(p.peek().as_str(), "keys");
524    p.expect(SyntaxKind::Identifier); //"keys"
525    p.expect(SyntaxKind::LParent);
526
527    // Parse custom syntax here...
528    let mut key_count = 0_u32;
529
530    let mut alt_count = 0_u32;
531    let mut control_count = 0_u32;
532    let mut shift_count = 0_u32;
533    let mut meta_count = 0_u32;
534    let mut ignore_shift_count = 0_u32;
535    let mut ignore_alt_count = 0_u32;
536
537    #[derive(Eq, PartialEq)]
538    enum State {
539        Start,
540        NeedPlus,
541        NeedKey,
542    }
543    let mut state = State::Start;
544
545    fn bail(p: &mut crate::parser::Node<'_, impl Parser>, message: &str) {
546        p.error(message);
547        p.until(SyntaxKind::RParent);
548    }
549
550    loop {
551        match p.peek().kind() {
552            SyntaxKind::RParent => {
553                assert!(key_count <= 1);
554                // Trailing plus
555                if state == State::NeedKey {
556                    p.error("Expected another identifier or string literal");
557                } else if key_count == 0
558                    && (alt_count + control_count + shift_count + meta_count) > 0
559                {
560                    p.error("A keyboard shortcut must be empty or contain exactly one key (with modifiers)");
561                }
562                p.consume();
563                break;
564            }
565            SyntaxKind::Plus => {
566                if state == State::NeedPlus {
567                    state = State::NeedKey;
568                    p.consume();
569                } else {
570                    bail(
571                        &mut p,
572                        "Unexpected '+' in keyboard shortcut (use Plus to refer to the key)",
573                    );
574                    break;
575                }
576                continue;
577            }
578            SyntaxKind::Identifier | SyntaxKind::StringLiteral => {
579                if state == State::NeedPlus {
580                    bail(&mut p, "Expected '+' to separate parts of a keyboard shortcut");
581                    break;
582                }
583
584                let token = p.peek();
585                let mut consume_count = 1;
586                // Modifiers must be identifiers, not string literals
587                if token.kind() == SyntaxKind::Identifier {
588                    let text = token.as_str();
589
590                    let mut try_consume_question = || -> bool {
591                        let next_token = p.nth(1);
592                        if next_token.kind() == SyntaxKind::Question {
593                            consume_count += 1;
594                            true
595                        } else {
596                            false
597                        }
598                    };
599
600                    match text {
601                        "Ctrl" => {
602                            bail(&mut p, "Ctrl is not in the Key namespace (Use Control instead)");
603                            break;
604                        }
605                        "Control" => control_count += 1,
606                        "Meta" => meta_count += 1,
607                        "Alt" => {
608                            if try_consume_question() {
609                                ignore_alt_count += 1;
610                            } else {
611                                alt_count += 1
612                            }
613                        }
614                        "Shift" => {
615                            if try_consume_question() {
616                                ignore_shift_count += 1;
617                            } else {
618                                shift_count += 1;
619                            }
620                        }
621                        "AltR" | "ShiftR" | "MetaR" | "ControlR" => {
622                            bail(&mut p, "Right-side modifiers are not supported");
623                            break;
624                        }
625                        "AltGr" => {
626                            bail(&mut p, "AltGr cannot be used as a modifier");
627                            break;
628                        }
629                        "Command" | "Cmd" => {
630                            bail(
631                                &mut p,
632                                // \x20 equals to a space (needed to avoid the trailing \ eating
633                                // the indentation)
634                                &format!(
635                                    "{text} is not a cross-platform modifier\n\
636                                    Use cross-platform modifier names instead:\n\
637                                    \x20   ⌘ command -> Control\n\
638                                    \x20   ⌥ option -> Alt\n\
639                                    \x20   ^ control -> Meta\n\
640                                    \x20   ⇧ shift -> Shift"
641                                ),
642                            );
643                            break;
644                        }
645                        "Win" | "Windows" => {
646                            bail(
647                                &mut p,
648                                &format!(
649                                    "{text} is not a cross-platform modifier (Use `Meta` instead)"
650                                ),
651                            );
652                            break;
653                        }
654                        _ => key_count += 1,
655                    }
656                } else {
657                    key_count += 1;
658                }
659
660                state = State::NeedPlus;
661
662                if [
663                    alt_count,
664                    control_count,
665                    meta_count,
666                    shift_count,
667                    ignore_shift_count,
668                    ignore_alt_count,
669                ]
670                .into_iter()
671                .max()
672                .unwrap_or_default()
673                    > 1
674                {
675                    bail(&mut p, "Duplicated modifier in keyboard shortcut");
676                    break;
677                }
678                if shift_count > 0 && ignore_shift_count > 0 {
679                    bail(&mut p, "Cannot use both Shift and Shift? (remove one of them)");
680                    break;
681                }
682                if alt_count > 0 && ignore_alt_count > 0 {
683                    bail(&mut p, "Cannot use both Alt and Alt? (remove one of them)");
684                    break;
685                }
686                if key_count > 1 {
687                    bail(&mut p, "A keyboard shortcut can only contain one key (with modifiers)");
688                    break;
689                }
690
691                for _ in 0..consume_count {
692                    p.consume();
693                }
694                continue;
695            }
696            _ => {
697                let hint = if state == State::NeedKey {
698                    format!("\n(Consider using \"{}\")", p.peek().as_str())
699                } else {
700                    "".into()
701                };
702                bail(
703                    &mut p,
704                    &format!(
705                        "Expected '+', a string literal, or an identifier in the Keys namespace{hint}"
706                    ),
707                );
708                break;
709            }
710        }
711    }
712}
713
714#[cfg_attr(test, parser_test)]
715/// ```test,AtImageUrl
716/// @image-url("foo.png")
717/// @image-url("foo.png",)
718/// @image-url("foo.png", nine-slice(1 2 3 4))
719/// @image-url("foo.png", nine-slice(1))
720/// ```
721fn parse_image_url(p: &mut impl Parser) {
722    let mut p = p.start_node(SyntaxKind::AtImageUrl);
723    p.consume(); // "@"
724    p.consume(); // "image-url"
725    if !(p.expect(SyntaxKind::LParent)) {
726        return;
727    }
728    let peek = p.peek();
729    if peek.kind() != SyntaxKind::StringLiteral {
730        p.error("@image-url must contain a plain path as a string literal");
731        p.until(SyntaxKind::RParent);
732        return;
733    }
734    if !peek.as_str().starts_with('"') || !peek.as_str().ends_with('"') {
735        p.error("@image-url must contain a plain path as a string literal, without any '\\{}' expressions");
736        p.until(SyntaxKind::RParent);
737        return;
738    }
739    p.expect(SyntaxKind::StringLiteral);
740    if !p.test(SyntaxKind::Comma) {
741        if !p.test(SyntaxKind::RParent) {
742            p.error("Expected ')' or ','");
743            p.until(SyntaxKind::RParent);
744        }
745        return;
746    }
747    if p.test(SyntaxKind::RParent) {
748        return;
749    }
750    if p.peek().as_str() != "nine-slice" {
751        p.error("Expected 'nine-slice(...)' argument");
752        p.until(SyntaxKind::RParent);
753        return;
754    }
755    p.consume();
756    if !p.expect(SyntaxKind::LParent) {
757        p.until(SyntaxKind::RParent);
758        return;
759    }
760    let mut count = 0;
761    loop {
762        match p.peek().kind() {
763            SyntaxKind::RParent => {
764                if count != 1 && count != 2 && count != 4 {
765                    p.error("Expected 1 or 2 or 4 numbers");
766                }
767                p.consume();
768                break;
769            }
770            SyntaxKind::NumberLiteral => {
771                count += 1;
772                p.consume();
773            }
774            SyntaxKind::Comma | SyntaxKind::Colon => {
775                p.error("Arguments of nine-slice need to be separated by spaces");
776                p.until(SyntaxKind::RParent);
777                break;
778            }
779            _ => {
780                p.error("Expected number literal or ')'");
781                p.until(SyntaxKind::RParent);
782                break;
783            }
784        }
785    }
786    if !p.expect(SyntaxKind::RParent) {
787        p.until(SyntaxKind::RParent);
788    }
789}