runjucks_core/parser/
expr.rs

1//! Expression parsing for `{{ … }}` bodies (Nunjucks-style precedence).
2//!
3//! Reference: `nunjucks/nunjucks/src/parser.js` (`parseExpression` → `parseOr` → … → `parsePrimary`).
4
5use crate::ast::{BinOp, CompareOp, Expr, MacroParam, UnaryOp};
6use crate::errors::{Result, RunjucksError};
7use crate::parser::split::split_top_level_commas;
8use nom::branch::alt;
9use nom::character::complete::{char, digit1};
10use nom::combinator::{all_consuming, map_res, opt, recognize};
11use nom::IResult;
12use nom::Parser;
13use serde_json::{json, Value};
14
15/// Keyword arguments in a `foo(a, b, k=v)` call.
16type CallKwArgs = Vec<(String, Expr)>;
17/// Positional and keyword pieces of a `foo(a, b, k=v)` call.
18type CallArgParts = (Vec<Expr>, CallKwArgs);
19
20fn trim_start(s: &str) -> &str {
21    s.trim_start()
22}
23
24/// True if `s` has a full ASCII keyword of length `kw_len` at the start (not a prefix of a longer identifier).
25fn keyword_boundary(s: &str, kw_len: usize) -> bool {
26    s.as_bytes()
27        .get(kw_len)
28        .is_none_or(|&b| !b.is_ascii_alphanumeric() && b != b'_')
29}
30
31fn parse_keyword<'a>(input: &'a str, kw: &str) -> Option<&'a str> {
32    let input = trim_start(input);
33    if input.starts_with(kw) && keyword_boundary(input, kw.len()) {
34        Some(&input[kw.len()..])
35    } else {
36        None
37    }
38}
39
40fn parse_string(input: &str) -> IResult<&str, String> {
41    let input = trim_start(input);
42    let mut chars = input.chars();
43    let quote = match chars.next() {
44        Some('"') => '"',
45        Some('\'') => '\'',
46        _ => {
47            return Err(nom::Err::Error(nom::error::Error::new(
48                input,
49                nom::error::ErrorKind::Tag,
50            )));
51        }
52    };
53    let rest = &input[quote.len_utf8()..];
54    let mut out = String::new();
55    let mut i = 0usize;
56    let rest_bytes = rest.as_bytes();
57    while i < rest_bytes.len() {
58        let c = rest[i..].chars().next().unwrap();
59        if c == quote {
60            return Ok((&rest[i + c.len_utf8()..], out));
61        }
62        if c == '\\' {
63            i += 1;
64            let Some(esc) = rest.get(i..).and_then(|s| s.chars().next()) else {
65                return Err(nom::Err::Failure(nom::error::Error::new(
66                    input,
67                    nom::error::ErrorKind::Escaped,
68                )));
69            };
70            match esc {
71                'n' => out.push('\n'),
72                'r' => out.push('\r'),
73                't' => out.push('\t'),
74                '\\' => out.push('\\'),
75                q if q == quote => out.push(q),
76                _ => out.push(esc),
77            }
78            i += esc.len_utf8();
79            continue;
80        }
81        out.push(c);
82        i += c.len_utf8();
83    }
84    Err(nom::Err::Failure(nom::error::Error::new(
85        input,
86        nom::error::ErrorKind::Tag,
87    )))
88}
89
90fn parse_number(input: &str) -> IResult<&str, Value> {
91    let input = trim_start(input);
92    map_res(
93        recognize((
94            opt(char('-')),
95            alt((recognize((digit1, char('.'), digit1)), digit1)),
96        )),
97        |s: &str| -> std::result::Result<Value, ()> {
98            if s.contains('.') {
99                s.parse::<f64>().map(|x| json!(x)).map_err(|_| ())
100            } else if let Ok(n) = s.parse::<i64>() {
101                Ok(json!(n))
102            } else {
103                s.parse::<f64>().map(|x| json!(x)).map_err(|_| ())
104            }
105        },
106    )
107    .parse(input)
108}
109
110fn parse_bool_or_none(input: &str) -> IResult<&str, Value> {
111    let input = trim_start(input);
112    if input.starts_with("true") && keyword_boundary(input, 4) {
113        return Ok((&input[4..], json!(true)));
114    }
115    if input.starts_with("false") && keyword_boundary(input, 5) {
116        return Ok((&input[5..], json!(false)));
117    }
118    if input.starts_with("none") && keyword_boundary(input, 4) {
119        return Ok((&input[4..], Value::Null));
120    }
121    if input.starts_with("null") && keyword_boundary(input, 4) {
122        return Ok((&input[4..], Value::Null));
123    }
124    Err(nom::Err::Error(nom::error::Error::new(
125        input,
126        nom::error::ErrorKind::Tag,
127    )))
128}
129
130fn parse_identifier(input: &str) -> IResult<&str, String> {
131    let input = trim_start(input);
132    let mut chars = input.chars();
133    let first = match chars.next() {
134        Some(c) if c.is_ascii_alphabetic() || c == '_' => c,
135        _ => {
136            return Err(nom::Err::Error(nom::error::Error::new(
137                input,
138                nom::error::ErrorKind::Tag,
139            )));
140        }
141    };
142    let len_first = first.len_utf8();
143    let take = input[len_first..]
144        .chars()
145        .take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
146        .map(|c| c.len_utf8())
147        .sum::<usize>();
148    let end = len_first + take;
149    Ok((&input[end..], input[..end].to_string()))
150}
151
152/// `ident` or `foo.bar` filter names (Nunjucks [`parseFilterName`](https://github.com/mozilla/nunjucks/blob/master/nunjucks/src/parser.js)).
153fn parse_filter_name(input: &str) -> IResult<&str, String> {
154    let (mut rest, mut name) = parse_identifier(input)?;
155    loop {
156        let r = trim_start(rest);
157        if let Some(r2) = r.strip_prefix('.') {
158            let (r3, part) = parse_identifier(trim_start(r2))?;
159            name.push('.');
160            name.push_str(&part);
161            rest = r3;
162        } else {
163            break;
164        }
165    }
166    Ok((rest, name))
167}
168
169fn simple_ident_str(s: &str) -> bool {
170    let mut ch = s.chars();
171    let Some(first) = ch.next() else {
172        return false;
173    };
174    if !first.is_ascii_alphabetic() && first != '_' {
175        return false;
176    }
177    ch.all(|c| c.is_ascii_alphanumeric() || c == '_')
178}
179
180/// If `seg` is `name = expr` at depth 0 (not `==`), returns `(name, expr source)`.
181fn split_call_kw_seg(seg: &str) -> Option<(&str, &str)> {
182    let seg = seg.trim();
183    let bytes = seg.as_bytes();
184    let mut depth = 0i32;
185    let mut in_string: Option<u8> = None;
186    let mut escaped = false;
187    let mut i = 0usize;
188    while i < bytes.len() {
189        let c = bytes[i];
190        if let Some(q) = in_string {
191            if escaped {
192                escaped = false;
193                i += 1;
194                continue;
195            }
196            if c == b'\\' {
197                escaped = true;
198                i += 1;
199                continue;
200            }
201            if c == q {
202                in_string = None;
203            }
204            i += 1;
205            continue;
206        }
207        if c == b'"' || c == b'\'' {
208            in_string = Some(c);
209            i += 1;
210            continue;
211        }
212        match c {
213            b'(' => depth += 1,
214            b')' => depth -= 1,
215            b'=' if depth == 0 => {
216                if i + 1 < bytes.len() && bytes[i + 1] == b'=' {
217                    i += 2;
218                    continue;
219                }
220                if i > 0 && bytes[i - 1] == b'=' {
221                    i += 1;
222                    continue;
223                }
224                let left = seg[..i].trim();
225                let right = seg[i + 1..].trim();
226                if simple_ident_str(left) {
227                    return Some((left, right));
228                }
229            }
230            _ => {}
231        }
232        i += 1;
233    }
234    None
235}
236
237/// After `(` of a call, splits at the matching `)` and returns `(rest_after_close, inner)`.
238fn split_call_inner_rest(rest_after_open_paren: &str) -> IResult<&str, &str> {
239    let s = trim_start(rest_after_open_paren);
240    let bytes = s.as_bytes();
241    let mut depth = 0i32;
242    let mut in_string: Option<u8> = None;
243    let mut escaped = false;
244    let mut i = 0usize;
245    while i < bytes.len() {
246        let c = bytes[i];
247        if let Some(q) = in_string {
248            if escaped {
249                escaped = false;
250                i += 1;
251                continue;
252            }
253            if c == b'\\' {
254                escaped = true;
255                i += 1;
256                continue;
257            }
258            if c == q {
259                in_string = None;
260            }
261            i += 1;
262            continue;
263        }
264        if c == b'"' || c == b'\'' {
265            in_string = Some(c);
266            i += 1;
267            continue;
268        }
269        match c {
270            b'(' => depth += 1,
271            b')' if depth == 0 => {
272                return Ok((&s[i + 1..], &s[..i]));
273            }
274            b')' => depth -= 1,
275            _ => {}
276        }
277        i += 1;
278    }
279    Err(nom::Err::Failure(nom::error::Error::new(
280        s,
281        nom::error::ErrorKind::Tag,
282    )))
283}
284
285fn parse_call_argument_list_inner(inner: &str) -> Result<CallArgParts> {
286    let inner = inner.trim();
287    if inner.is_empty() {
288        return Ok((vec![], vec![]));
289    }
290    let segs = split_top_level_commas(inner);
291    let mut pos = Vec::new();
292    let mut kw = Vec::new();
293    for seg in segs {
294        let seg = seg.trim();
295        if seg.is_empty() {
296            continue;
297        }
298        if let Some((name, rhs)) = split_call_kw_seg(seg) {
299            kw.push((name.to_string(), parse_expression(rhs)?));
300        } else {
301            pos.push(parse_expression(seg)?);
302        }
303    }
304    Ok((pos, kw))
305}
306
307fn parse_call_argument_list(input: &str) -> IResult<&str, CallArgParts> {
308    let input = trim_start(input);
309    if let Some(r) = input.strip_prefix(')') {
310        return Ok((r, (vec![], vec![])));
311    }
312    let (rest, inner) = split_call_inner_rest(input)?;
313    match parse_call_argument_list_inner(inner) {
314        Ok(pair) => Ok((rest, pair)),
315        Err(_) => Err(nom::Err::Failure(nom::error::Error::new(
316            inner,
317            nom::error::ErrorKind::Verify,
318        ))),
319    }
320}
321
322/// Index of the closing `]` for subscript content (caller has already consumed the opening `[`).
323fn bracket_content_end(s: &str) -> Option<usize> {
324    let mut depth = 0i32;
325    for (i, c) in s.char_indices() {
326        match c {
327            '[' => depth += 1,
328            ']' => {
329                if depth == 0 {
330                    return Some(i);
331                }
332                depth -= 1;
333            }
334            _ => {}
335        }
336    }
337    None
338}
339
340fn has_top_level_colon(body: &str) -> bool {
341    let mut d_paren = 0i32;
342    let mut d_bracket = 0i32;
343    for c in body.chars() {
344        match c {
345            '(' => d_paren += 1,
346            ')' => d_paren -= 1,
347            '[' => d_bracket += 1,
348            ']' => d_bracket -= 1,
349            ':' if d_paren == 0 && d_bracket == 0 => return true,
350            _ => {}
351        }
352    }
353    false
354}
355
356fn split_top_level_colon(body: &str) -> Vec<&str> {
357    let mut parts = Vec::new();
358    let mut start = 0usize;
359    let mut d_paren = 0i32;
360    let mut d_bracket = 0i32;
361    for (i, c) in body.char_indices() {
362        match c {
363            '(' => d_paren += 1,
364            ')' => d_paren -= 1,
365            '[' => d_bracket += 1,
366            ']' => d_bracket -= 1,
367            ':' if d_paren == 0 && d_bracket == 0 => {
368                parts.push(body[start..i].trim());
369                start = i + 1;
370            }
371            _ => {}
372        }
373    }
374    parts.push(body[start..].trim());
375    parts
376}
377
378fn parse_optional_slice_segment(
379    seg: &str,
380) -> std::result::Result<Option<Expr>, nom::Err<nom::error::Error<&str>>> {
381    let seg = seg.trim();
382    if seg.is_empty() {
383        return Ok(None);
384    }
385    all_consuming(parse_inline_if)
386        .parse(seg)
387        .map(|(_, e)| Some(e))
388}
389
390fn parse_subscript(input: &str) -> IResult<&str, Expr> {
391    let end = bracket_content_end(input).ok_or_else(|| {
392        nom::Err::Failure(nom::error::Error::new(input, nom::error::ErrorKind::Tag))
393    })?;
394    let body = trim_start(&input[..end]);
395    let rest = &input[end + 1..];
396
397    if !has_top_level_colon(body) {
398        let (_, e) = all_consuming(parse_inline_if).parse(body)?;
399        return Ok((rest, e));
400    }
401
402    let segs = split_top_level_colon(body);
403    if segs.len() > 3 {
404        return Err(nom::Err::Failure(nom::error::Error::new(
405            body,
406            nom::error::ErrorKind::TooLarge,
407        )));
408    }
409    let start_e = parse_optional_slice_segment(segs.first().copied().unwrap_or(""))?;
410    let stop_e = parse_optional_slice_segment(segs.get(1).copied().unwrap_or(""))?;
411    let step_e = parse_optional_slice_segment(segs.get(2).copied().unwrap_or(""))?;
412    let start = start_e.map(Box::new);
413    let stop = stop_e.map(Box::new);
414    let step = step_e.map(Box::new);
415    Ok((rest, Expr::Slice { start, stop, step }))
416}
417
418fn parse_postfix(input: &str, mut node: Expr) -> IResult<&str, Expr> {
419    let mut rest = input;
420    loop {
421        let r = trim_start(rest);
422        if let Some(r2) = r.strip_prefix('.') {
423            let (r3, attr) = parse_identifier(trim_start(r2))?;
424            node = Expr::GetAttr {
425                base: Box::new(node),
426                attr,
427            };
428            rest = r3;
429            continue;
430        }
431        if let Some(r2) = r.strip_prefix('[') {
432            let (r4, idx) = parse_subscript(trim_start(r2))?;
433            node = Expr::GetItem {
434                base: Box::new(node),
435                index: Box::new(idx),
436            };
437            rest = r4;
438            continue;
439        }
440        if let Some(r2) = r.strip_prefix('(') {
441            let (r3, (args, kwargs)) = parse_call_argument_list(r2)?;
442            node = Expr::Call {
443                callee: Box::new(node),
444                args,
445                kwargs,
446            };
447            rest = r3;
448            continue;
449        }
450        break;
451    }
452    Ok((rest, node))
453}
454
455fn parse_list_literal(input: &str) -> IResult<&str, Expr> {
456    let input = trim_start(input);
457    let after = input.strip_prefix('[').ok_or_else(|| {
458        nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Tag))
459    })?;
460    let mut rest = trim_start(after);
461    if let Some(r) = rest.strip_prefix(']') {
462        return Ok((r, Expr::List(vec![])));
463    }
464    let mut items = Vec::new();
465    loop {
466        let (r, e) = parse_inline_if(rest)?;
467        items.push(e);
468        let r = trim_start(r);
469        if let Some(r2) = r.strip_prefix(']') {
470            return Ok((r2, Expr::List(items)));
471        }
472        let r = r.strip_prefix(',').ok_or_else(|| {
473            nom::Err::Failure(nom::error::Error::new(r, nom::error::ErrorKind::Tag))
474        })?;
475        rest = trim_start(r);
476    }
477}
478
479fn parse_dict_literal(input: &str) -> IResult<&str, Expr> {
480    let input = trim_start(input);
481    let after = input.strip_prefix('{').ok_or_else(|| {
482        nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Tag))
483    })?;
484    let mut rest = trim_start(after);
485    if let Some(r) = rest.strip_prefix('}') {
486        return Ok((r, Expr::Dict(vec![])));
487    }
488    let mut pairs = Vec::new();
489    loop {
490        let (r, k) = parse_inline_if(rest)?;
491        let r = trim_start(r);
492        let r = r.strip_prefix(':').ok_or_else(|| {
493            nom::Err::Failure(nom::error::Error::new(r, nom::error::ErrorKind::Tag))
494        })?;
495        let (r, v) = parse_inline_if(trim_start(r))?;
496        pairs.push((k, v));
497        let r = trim_start(r);
498        if let Some(r2) = r.strip_prefix('}') {
499            return Ok((r2, Expr::Dict(pairs)));
500        }
501        let r = r.strip_prefix(',').ok_or_else(|| {
502            nom::Err::Failure(nom::error::Error::new(r, nom::error::ErrorKind::Tag))
503        })?;
504        rest = trim_start(r);
505    }
506}
507
508fn parse_atom(input: &str) -> IResult<&str, Expr> {
509    let input = trim_start(input);
510    if let Some(after) = input.strip_prefix('(') {
511        let (rest, e) = parse_inline_if(after)?;
512        let rest = trim_start(rest);
513        let r = rest.strip_prefix(')').ok_or_else(|| {
514            nom::Err::Failure(nom::error::Error::new(rest, nom::error::ErrorKind::Tag))
515        })?;
516        return Ok((r, e));
517    }
518    if input.starts_with('[') {
519        return parse_list_literal(input);
520    }
521    if input.starts_with('{') {
522        return parse_dict_literal(input);
523    }
524    if let Ok((rest, v)) = parse_string(input) {
525        return Ok((rest, Expr::Literal(json!(v))));
526    }
527    if let Ok((rest, v)) = parse_bool_or_none(input) {
528        return Ok((rest, Expr::Literal(v)));
529    }
530    if let Ok((rest, v)) = parse_number(input) {
531        return Ok((rest, Expr::Literal(v)));
532    }
533    if let Ok((rest, rx)) = parse_regex_literal(input) {
534        return Ok((rest, rx));
535    }
536    let (rest, name) = parse_identifier(input)?;
537    Ok((rest, Expr::Variable(name)))
538}
539
540/// `r/pattern/flags` — see [Nunjucks lexer](https://github.com/mozilla/nunjucks/blob/master/nunjucks/src/lexer.js).
541fn parse_regex_literal(input: &str) -> IResult<&str, Expr> {
542    let input = trim_start(input);
543    let Some(after_r_slash) = input.strip_prefix("r/") else {
544        return Err(nom::Err::Error(nom::error::Error::new(
545            input,
546            nom::error::ErrorKind::Tag,
547        )));
548    };
549    let mut prev = None::<char>;
550    let mut body = String::new();
551    let mut end_slash = None::<usize>;
552    for (i, c) in after_r_slash.char_indices() {
553        if c == '/' && prev != Some('\\') {
554            end_slash = Some(i);
555            break;
556        }
557        body.push(c);
558        prev = Some(c);
559    }
560    let end_idx = end_slash.ok_or_else(|| {
561        nom::Err::Failure(nom::error::Error::new(input, nom::error::ErrorKind::Eof))
562    })?;
563    let after_slash = &after_r_slash[end_idx + 1..];
564    let mut flags = String::new();
565    for ch in after_slash.chars() {
566        if matches!(ch, 'g' | 'i' | 'm' | 'y') {
567            flags.push(ch);
568        } else {
569            break;
570        }
571    }
572    let rest = &after_slash[flags.len()..];
573    Ok((
574        rest,
575        Expr::RegexLiteral {
576            pattern: body,
577            flags,
578        },
579    ))
580}
581
582fn parse_atom_with_postfix(input: &str) -> IResult<&str, Expr> {
583    let (rest, atom) = parse_atom(input)?;
584    parse_postfix(rest, atom)
585}
586
587fn parse_filter_chain(input: &str, mut node: Expr) -> IResult<&str, Expr> {
588    let mut rest = input;
589    loop {
590        let r = trim_start(rest);
591        if !r.starts_with('|') {
592            break;
593        }
594        let after = &r[1..];
595        let after = trim_start(after);
596        let (r2, name) = parse_filter_name(after)?;
597        let after_name = trim_start(r2);
598        let (r3, (extra_args, filter_kw)) = if let Some(inner) = after_name.strip_prefix('(') {
599            parse_call_argument_list(inner)?
600        } else {
601            (after_name, (vec![], vec![]))
602        };
603        if !filter_kw.is_empty() {
604            return Err(nom::Err::Failure(nom::error::Error::new(
605                after_name,
606                nom::error::ErrorKind::Verify,
607            )));
608        }
609        node = Expr::Filter {
610            name,
611            input: Box::new(node),
612            args: extra_args,
613        };
614        rest = r3;
615    }
616    Ok((rest, node))
617}
618
619fn parse_unary_no_filters(input: &str) -> IResult<&str, Expr> {
620    let t = trim_start(input);
621    if let Some(rest) = parse_keyword(t, "not") {
622        let (rest, e) = parse_unary_no_filters(rest)?;
623        return Ok((
624            rest,
625            Expr::Unary {
626                op: UnaryOp::Not,
627                expr: Box::new(e),
628            },
629        ));
630    }
631    if t.starts_with('-')
632        && t.as_bytes()
633            .get(1)
634            .is_some_and(|b| b.is_ascii_digit() || *b == b'.')
635    {
636        let (rest, v) = parse_number(t)?;
637        return Ok((rest, Expr::Literal(v)));
638    }
639    if let Some(rest) = t.strip_prefix('-') {
640        let (rest, e) = parse_unary_no_filters(rest)?;
641        return Ok((
642            rest,
643            Expr::Unary {
644                op: UnaryOp::Neg,
645                expr: Box::new(e),
646            },
647        ));
648    }
649    if let Some(rest) = t.strip_prefix('+') {
650        let (rest, e) = parse_unary_no_filters(rest)?;
651        return Ok((
652            rest,
653            Expr::Unary {
654                op: UnaryOp::Pos,
655                expr: Box::new(e),
656            },
657        ));
658    }
659    parse_atom_with_postfix(input)
660}
661
662fn parse_unary(input: &str) -> IResult<&str, Expr> {
663    let (rest, e) = parse_unary_no_filters(input)?;
664    parse_filter_chain(rest, e)
665}
666
667fn parse_pow(input: &str) -> IResult<&str, Expr> {
668    let (mut rest, mut acc) = parse_unary(input)?;
669    loop {
670        let r = trim_start(rest);
671        if let Some(r2) = r.strip_prefix("**") {
672            rest = r2;
673            let (r2, rhs) = parse_unary(rest)?;
674            rest = r2;
675            acc = Expr::Binary {
676                op: BinOp::Pow,
677                left: Box::new(acc),
678                right: Box::new(rhs),
679            };
680            continue;
681        }
682        break;
683    }
684    Ok((rest, acc))
685}
686
687fn parse_mod(input: &str) -> IResult<&str, Expr> {
688    let (mut rest, mut acc) = parse_pow(input)?;
689    loop {
690        let r = trim_start(rest);
691        if let Some(r2) = r.strip_prefix('%') {
692            if r2.starts_with('%') {
693                break;
694            }
695            let (r3, rhs) = parse_pow(r2)?;
696            rest = r3;
697            acc = Expr::Binary {
698                op: BinOp::Mod,
699                left: Box::new(acc),
700                right: Box::new(rhs),
701            };
702            continue;
703        }
704        break;
705    }
706    Ok((rest, acc))
707}
708
709fn parse_floor_div(input: &str) -> IResult<&str, Expr> {
710    let (mut rest, mut acc) = parse_mod(input)?;
711    loop {
712        let r = trim_start(rest);
713        if let Some(r2) = r.strip_prefix("//") {
714            let (r3, rhs) = parse_mod(r2)?;
715            rest = r3;
716            acc = Expr::Binary {
717                op: BinOp::FloorDiv,
718                left: Box::new(acc),
719                right: Box::new(rhs),
720            };
721            continue;
722        }
723        break;
724    }
725    Ok((rest, acc))
726}
727
728fn parse_div(input: &str) -> IResult<&str, Expr> {
729    let (mut rest, mut acc) = parse_floor_div(input)?;
730    loop {
731        let r = trim_start(rest);
732        if let Some(r2) = r.strip_prefix('/') {
733            if r2.starts_with('/') {
734                break;
735            }
736            let (r3, rhs) = parse_floor_div(r2)?;
737            rest = r3;
738            acc = Expr::Binary {
739                op: BinOp::Div,
740                left: Box::new(acc),
741                right: Box::new(rhs),
742            };
743            continue;
744        }
745        break;
746    }
747    Ok((rest, acc))
748}
749
750fn parse_mul(input: &str) -> IResult<&str, Expr> {
751    let (mut rest, mut acc) = parse_div(input)?;
752    loop {
753        let r = trim_start(rest);
754        if let Some(r2) = r.strip_prefix('*') {
755            if r2.starts_with('*') {
756                break;
757            }
758            let (r3, rhs) = parse_div(r2)?;
759            rest = r3;
760            acc = Expr::Binary {
761                op: BinOp::Mul,
762                left: Box::new(acc),
763                right: Box::new(rhs),
764            };
765            continue;
766        }
767        break;
768    }
769    Ok((rest, acc))
770}
771
772fn parse_sub(input: &str) -> IResult<&str, Expr> {
773    let (mut rest, mut acc) = parse_mul(input)?;
774    loop {
775        let r = trim_start(rest);
776        if let Some(r2) = r.strip_prefix('-') {
777            let (r3, rhs) = parse_mul(r2)?;
778            rest = r3;
779            acc = Expr::Binary {
780                op: BinOp::Sub,
781                left: Box::new(acc),
782                right: Box::new(rhs),
783            };
784            continue;
785        }
786        break;
787    }
788    Ok((rest, acc))
789}
790
791fn parse_add(input: &str) -> IResult<&str, Expr> {
792    let (mut rest, mut acc) = parse_sub(input)?;
793    loop {
794        let r = trim_start(rest);
795        if let Some(r2) = r.strip_prefix('+') {
796            let (r3, rhs) = parse_sub(r2)?;
797            rest = r3;
798            acc = Expr::Binary {
799                op: BinOp::Add,
800                left: Box::new(acc),
801                right: Box::new(rhs),
802            };
803            continue;
804        }
805        break;
806    }
807    Ok((rest, acc))
808}
809
810fn parse_concat(input: &str) -> IResult<&str, Expr> {
811    let (mut rest, mut acc) = parse_add(input)?;
812    loop {
813        let r = trim_start(rest);
814        if let Some(r2) = r.strip_prefix('~') {
815            let (r3, rhs) = parse_add(r2)?;
816            rest = r3;
817            acc = Expr::Binary {
818                op: BinOp::Concat,
819                left: Box::new(acc),
820                right: Box::new(rhs),
821            };
822            continue;
823        }
824        break;
825    }
826    Ok((rest, acc))
827}
828
829fn parse_compare_op(rest: &str) -> Option<(CompareOp, usize)> {
830    if rest.starts_with("===") {
831        Some((CompareOp::StrictEq, 3))
832    } else if rest.starts_with("!==") {
833        Some((CompareOp::StrictNe, 3))
834    } else if rest.starts_with("==") {
835        Some((CompareOp::Eq, 2))
836    } else if rest.starts_with("!=") {
837        Some((CompareOp::Ne, 2))
838    } else if rest.starts_with("<=") {
839        Some((CompareOp::Le, 2))
840    } else if rest.starts_with(">=") {
841        Some((CompareOp::Ge, 2))
842    } else if rest.starts_with('<') {
843        Some((CompareOp::Lt, 1))
844    } else if rest.starts_with('>') {
845        Some((CompareOp::Gt, 1))
846    } else {
847        None
848    }
849}
850
851fn parse_compare(input: &str) -> IResult<&str, Expr> {
852    let (mut rest, head) = parse_concat(input)?;
853    let mut rest_vec: Vec<(CompareOp, Expr)> = Vec::new();
854    loop {
855        let r = trim_start(rest);
856        if let Some((op, len)) = parse_compare_op(r) {
857            let after = &r[len..];
858            let (r2, rhs) = parse_concat(after)?;
859            rest = r2;
860            rest_vec.push((op, rhs));
861            continue;
862        }
863        break;
864    }
865    if rest_vec.is_empty() {
866        Ok((rest, head))
867    } else {
868        Ok((
869            rest,
870            Expr::Compare {
871                head: Box::new(head),
872                rest: rest_vec,
873            },
874        ))
875    }
876}
877
878fn parse_is(input: &str) -> IResult<&str, Expr> {
879    let (mut rest, mut acc) = parse_compare(input)?;
880    loop {
881        let r = trim_start(rest);
882        if let Some(r2) = parse_keyword(r, "is") {
883            let mut after = trim_start(r2);
884            let mut negated = false;
885            if let Some(r3) = parse_keyword(after, "not") {
886                negated = true;
887                after = trim_start(r3);
888            }
889            let (r3, rhs) = parse_compare(after)?;
890            rest = r3;
891            let node = Expr::Binary {
892                op: BinOp::Is,
893                left: Box::new(acc),
894                right: Box::new(rhs),
895            };
896            acc = if negated {
897                Expr::Unary {
898                    op: UnaryOp::Not,
899                    expr: Box::new(node),
900                }
901            } else {
902                node
903            };
904            continue;
905        }
906        break;
907    }
908    Ok((rest, acc))
909}
910
911fn parse_in(input: &str) -> IResult<&str, Expr> {
912    let (mut rest, mut acc) = parse_is(input)?;
913    loop {
914        let r = trim_start(rest);
915        let (invert, after_not) = if let Some(r2) = parse_keyword(r, "not") {
916            let r3 = trim_start(r2);
917            if let Some(r4) = parse_keyword(r3, "in") {
918                (true, r4)
919            } else {
920                break;
921            }
922        } else if let Some(r2) = parse_keyword(r, "in") {
923            (false, r2)
924        } else {
925            break;
926        };
927        let (r2, rhs) = parse_is(after_not)?;
928        rest = r2;
929        let mut node = Expr::Binary {
930            op: BinOp::In,
931            left: Box::new(acc),
932            right: Box::new(rhs),
933        };
934        if invert {
935            node = Expr::Unary {
936                op: UnaryOp::Not,
937                expr: Box::new(node),
938            };
939        }
940        acc = node;
941    }
942    Ok((rest, acc))
943}
944
945fn parse_and(input: &str) -> IResult<&str, Expr> {
946    let (mut rest, mut acc) = parse_in(input)?;
947    loop {
948        let r = trim_start(rest);
949        if let Some(r2) = parse_keyword(r, "and") {
950            let (r3, rhs) = parse_in(r2)?;
951            rest = r3;
952            acc = Expr::Binary {
953                op: BinOp::And,
954                left: Box::new(acc),
955                right: Box::new(rhs),
956            };
957            continue;
958        }
959        break;
960    }
961    Ok((rest, acc))
962}
963
964fn parse_or(input: &str) -> IResult<&str, Expr> {
965    let (mut rest, mut acc) = parse_and(input)?;
966    loop {
967        let r = trim_start(rest);
968        if let Some(r2) = parse_keyword(r, "or") {
969            let (r3, rhs) = parse_and(r2)?;
970            rest = r3;
971            acc = Expr::Binary {
972                op: BinOp::Or,
973                left: Box::new(acc),
974                right: Box::new(rhs),
975            };
976            continue;
977        }
978        break;
979    }
980    Ok((rest, acc))
981}
982
983pub(crate) fn parse_macro_param_segment(seg: &str) -> Result<MacroParam> {
984    let seg = seg.trim();
985    if seg.is_empty() {
986        return Err(RunjucksError::new("empty macro parameter"));
987    }
988    if let Some((name, rhs)) = split_call_kw_seg(seg) {
989        Ok(MacroParam {
990            name: name.to_string(),
991            default: Some(parse_expression(rhs)?),
992        })
993    } else if simple_ident_str(seg) {
994        Ok(MacroParam {
995            name: seg.to_string(),
996            default: None,
997        })
998    } else {
999        Err(RunjucksError::new(format!(
1000            "invalid macro parameter `{seg}` (expected `name` or `name = default`)"
1001        )))
1002    }
1003}
1004
1005pub(crate) fn parse_inline_if(input: &str) -> IResult<&str, Expr> {
1006    let (rest, first) = parse_or(input)?;
1007    let r = trim_start(rest);
1008    if let Some(r2) = parse_keyword(r, "if") {
1009        let (r3, cond) = parse_or(r2)?;
1010        let r3 = trim_start(r3);
1011        let (rest, else_expr) = if let Some(r4) = parse_keyword(r3, "else") {
1012            let (r5, e) = parse_or(r4)?;
1013            (r5, Some(e))
1014        } else {
1015            (r3, None)
1016        };
1017        return Ok((
1018            rest,
1019            Expr::InlineIf {
1020                cond: Box::new(cond),
1021                then_expr: Box::new(first),
1022                else_expr: else_expr.map(Box::new),
1023            },
1024        ));
1025    }
1026    Ok((rest, first))
1027}
1028
1029/// Parses a full `{{ … }}` body (must consume all non-whitespace).
1030pub fn parse_expression(source: &str) -> Result<Expr> {
1031    let s = source.trim();
1032    if s.is_empty() {
1033        return Err(RunjucksError::new(
1034            "empty expression inside `{{ }}` is not allowed",
1035        ));
1036    }
1037    match all_consuming(parse_inline_if).parse(s) {
1038        Ok((_, expr)) => Ok(expr),
1039        Err(e) => Err(RunjucksError::new(format!("expression parse error: {e}"))),
1040    }
1041}
1042
1043#[cfg(test)]
1044mod tests {
1045    use super::*;
1046    use crate::ast::BinOp;
1047
1048    #[test]
1049    fn precedence_mul_before_add() {
1050        let e = parse_expression("2 + 3 * 4").unwrap();
1051        match e {
1052            Expr::Binary {
1053                op: BinOp::Add,
1054                left,
1055                right,
1056            } => {
1057                assert!(matches!(*left, Expr::Literal(_)));
1058                assert!(matches!(*right, Expr::Binary { op: BinOp::Mul, .. }));
1059            }
1060            _ => panic!("unexpected {:?}", e),
1061        }
1062    }
1063}