ructe/
templateexpression.rs

1use crate::expression::{
2    comma_expressions, expr_in_braces, expr_inside_parens, expression,
3    input_to_str, rust_name,
4};
5use crate::parseresult::PResult;
6use crate::spacelike::{comment_tail, spacelike};
7use nom::branch::alt;
8use nom::bytes::complete::is_not;
9use nom::bytes::complete::tag;
10use nom::character::complete::char;
11use nom::combinator::{map, map_res, opt, recognize, value};
12use nom::error::context;
13use nom::multi::{many0, many_till, separated_list0};
14use nom::sequence::{delimited, pair, preceded, terminated};
15use nom::Parser as _;
16use std::fmt::{self, Display, Write};
17
18#[derive(Debug, PartialEq, Eq)]
19pub enum TemplateExpression {
20    Comment,
21    Text {
22        text: String,
23    },
24    Expression {
25        expr: String,
26    },
27    ForLoop {
28        name: String,
29        expr: String,
30        body: Vec<TemplateExpression>,
31    },
32    IfBlock {
33        expr: String,
34        body: Vec<TemplateExpression>,
35        else_body: Option<Vec<TemplateExpression>>,
36    },
37    MatchBlock {
38        expr: String,
39        arms: Vec<(String, Vec<TemplateExpression>)>,
40    },
41    CallTemplate {
42        name: String,
43        args: Vec<TemplateArgument>,
44    },
45}
46
47#[derive(Debug, PartialEq, Eq)]
48pub enum TemplateArgument {
49    Rust(String),
50    Body(Vec<TemplateExpression>),
51}
52
53impl Display for TemplateArgument {
54    fn fmt(&self, out: &mut fmt::Formatter) -> Result<(), fmt::Error> {
55        match *self {
56            TemplateArgument::Rust(ref s) => out.write_str(s),
57            TemplateArgument::Body(ref v) if v.is_empty() => {
58                out.write_str("|_| Ok(())")
59            }
60            TemplateArgument::Body(ref v) => {
61                out.write_str("#[allow(clippy::used_underscore_binding)] |mut _ructe_out_| {\n")?;
62                for b in v {
63                    b.write_code(out)?;
64                }
65                out.write_str("Ok(())\n}\n")
66            }
67        }
68    }
69}
70
71impl TemplateExpression {
72    pub fn text(text: &str) -> Self {
73        TemplateExpression::Text {
74            text: text.to_string(),
75        }
76    }
77    pub fn write_code(&self, out: &mut impl Write) -> fmt::Result {
78        match *self {
79            TemplateExpression::Comment => Ok(()),
80            TemplateExpression::Text { ref text } if text.is_ascii() => {
81                writeln!(out, "_ructe_out_.write_all(b{text:?})?;")
82            }
83            TemplateExpression::Text { ref text } => {
84                writeln!(out, "_ructe_out_.write_all({text:?}.as_bytes())?;")
85            }
86            TemplateExpression::Expression { ref expr } => {
87                writeln!(out, "{expr}.to_html(_ructe_out_.by_ref())?;")
88            }
89            TemplateExpression::ForLoop {
90                ref name,
91                ref expr,
92                ref body,
93            } => {
94                writeln!(out, "for {name} in {expr} {{")?;
95                for b in body {
96                    b.write_code(out)?;
97                }
98                out.write_str("}\n")
99            }
100            TemplateExpression::IfBlock {
101                ref expr,
102                ref body,
103                ref else_body,
104            } => {
105                writeln!(out, "if {expr} {{")?;
106                for b in body {
107                    b.write_code(out)?;
108                }
109                out.write_str("}")?;
110                match else_body.as_deref() {
111                    Some([e @ TemplateExpression::IfBlock { .. }]) => {
112                        out.write_str(" else ")?;
113                        e.write_code(out)
114                    }
115                    Some(body) => {
116                        out.write_str(" else {\n")?;
117                        for b in body {
118                            b.write_code(out)?;
119                        }
120                        out.write_str("}\n")
121                    }
122                    None => out.write_char('\n'),
123                }
124            }
125            TemplateExpression::MatchBlock { ref expr, ref arms } => {
126                write!(out, "match {expr} {{")?;
127                for (expr, body) in arms {
128                    write!(out, "\n  {expr} => {{")?;
129                    for b in body {
130                        b.write_code(out)?;
131                    }
132                    write!(out, "}}")?;
133                }
134                writeln!(out, "\n}}")
135            }
136            TemplateExpression::CallTemplate { ref name, ref args } => {
137                write!(out, "{name}(_ructe_out_.by_ref()",)?;
138                for arg in args {
139                    write!(out, ", {arg}")?;
140                }
141                writeln!(out, ")?;")
142            }
143        }
144    }
145}
146
147pub fn template_expression(input: &[u8]) -> PResult<TemplateExpression> {
148    match opt(preceded(
149        char('@'),
150        alt((
151            tag("*"),
152            tag(":"),
153            tag("@"),
154            tag("{"),
155            tag("}"),
156            tag("("),
157            terminated(alt((tag("if"), tag("for"), tag("match"))), tag(" ")),
158            value(&b""[..], tag("")),
159        )),
160    ))
161    .parse(input)?
162    {
163        (i, Some(b":")) => map(
164            pair(
165                rust_name,
166                delimited(
167                    char('('),
168                    separated_list0(
169                        terminated(tag(","), spacelike),
170                        template_argument,
171                    ),
172                    char(')'),
173                ),
174            ),
175            |(name, args)| TemplateExpression::CallTemplate {
176                name: name.to_string(),
177                args,
178            },
179        )
180        .parse(i),
181        (i, Some(b"@")) => Ok((i, TemplateExpression::text("@"))),
182        (i, Some(b"{")) => Ok((i, TemplateExpression::text("{"))),
183        (i, Some(b"}")) => Ok((i, TemplateExpression::text("}"))),
184        (i, Some(b"*")) => {
185            map(comment_tail, |()| TemplateExpression::Comment).parse(i)
186        }
187        (i, Some(b"if")) => if2(i),
188        (i, Some(b"for")) => map(
189            (
190                for_variable,
191                delimited(
192                    terminated(
193                        context("Expected \"in\"", tag("in")),
194                        spacelike,
195                    ),
196                    context("Expected iterable expression", loop_expression),
197                    spacelike,
198                ),
199                context("Error in loop block:", template_block),
200            ),
201            |(name, expr, body)| TemplateExpression::ForLoop {
202                name,
203                expr,
204                body,
205            },
206        )
207        .parse(i),
208        (i, Some(b"match")) => context(
209            "Error in match expression:",
210            map(
211                (
212                    delimited(spacelike, expression, spacelike),
213                    preceded(
214                        char('{'),
215                        map(
216                            many_till(
217                                context(
218                                    "Error in match arm starting here:",
219                                    pair(
220                                        delimited(
221                                            spacelike,
222                                            map(expression, String::from),
223                                            spacelike,
224                                        ),
225                                        preceded(
226                                            terminated(tag("=>"), spacelike),
227                                            template_block,
228                                        ),
229                                    ),
230                                ),
231                                preceded(spacelike, char('}')),
232                            ),
233                            |(arms, _end)| arms,
234                        ),
235                    ),
236                ),
237                |(expr, arms)| TemplateExpression::MatchBlock {
238                    expr: expr.to_string(),
239                    arms,
240                },
241            ),
242        )
243        .parse(i),
244        (i, Some(b"(")) => {
245            map(terminated(expr_inside_parens, tag(")")), |expr| {
246                TemplateExpression::Expression {
247                    expr: format!("({expr})"),
248                }
249            })
250            .parse(i)
251        }
252        (i, Some(b"")) => {
253            map(expression, |expr| TemplateExpression::Expression {
254                expr: expr.to_string(),
255            })
256            .parse(i)
257        }
258        (_i, Some(_)) => unreachable!(),
259        (i, None) => map(map_res(is_not("@{}"), input_to_str), |text| {
260            TemplateExpression::Text {
261                text: text.to_string(),
262            }
263        })
264        .parse(i),
265    }
266}
267
268fn if2(input: &[u8]) -> PResult<TemplateExpression> {
269    context(
270        "Error in conditional expression:",
271        map(
272            (
273                delimited(spacelike, cond_expression, spacelike),
274                template_block,
275                opt(preceded(
276                    delimited(spacelike, tag("else"), spacelike),
277                    alt((
278                        preceded(tag("if"), map(if2, |e| vec![e])),
279                        template_block,
280                    )),
281                )),
282            ),
283            |(expr, body, else_body)| TemplateExpression::IfBlock {
284                expr,
285                body,
286                else_body,
287            },
288        ),
289    )
290    .parse(input)
291}
292
293fn for_variable(input: &[u8]) -> PResult<String> {
294    delimited(
295        spacelike,
296        context(
297            "Expected loop variable name or destructuring tuple",
298            alt((
299                map(
300                    map_res(
301                        recognize(preceded(rust_name, opt(expr_in_braces))),
302                        input_to_str,
303                    ),
304                    String::from,
305                ),
306                map(
307                    pair(
308                        opt(char('&')),
309                        delimited(char('('), comma_expressions, char(')')),
310                    ),
311                    |(pre, args)| {
312                        format!("{}({})", pre.map_or("", |_| "&"), args)
313                    },
314                ),
315            )),
316        ),
317        spacelike,
318    )
319    .parse(input)
320}
321
322fn template_block(input: &[u8]) -> PResult<Vec<TemplateExpression>> {
323    preceded(
324        char('{'),
325        map(
326            many_till(
327                context(
328                    "Error in expression starting here:",
329                    template_expression,
330                ),
331                char('}'),
332            ),
333            |(block, _end)| block,
334        ),
335    )
336    .parse(input)
337}
338
339fn template_argument(input: &[u8]) -> PResult<TemplateArgument> {
340    alt((
341        map(
342            delimited(
343                char('{'),
344                many0(template_expression),
345                terminated(char('}'), spacelike),
346            ),
347            TemplateArgument::Body,
348        ),
349        map(map(expression, String::from), TemplateArgument::Rust),
350    ))
351    .parse(input)
352}
353
354fn cond_expression(input: &[u8]) -> PResult<String> {
355    match opt(tag("let")).parse(input)? {
356        (i, Some(b"let")) => map(
357            pair(
358                preceded(
359                    spacelike,
360                    context(
361                        "Expected LHS expression in let binding",
362                        expression,
363                    ),
364                ),
365                preceded(
366                    delimited(spacelike, char('='), spacelike),
367                    context(
368                        "Expected RHS expression in let binding",
369                        expression,
370                    ),
371                ),
372            ),
373            |(lhs, rhs)| format!("let {lhs} = {rhs}"),
374        )
375        .parse(i),
376        (_i, Some(_)) => unreachable!(),
377        (i, None) => map(
378            context("Expected expression", logic_expression),
379            String::from,
380        )
381        .parse(i),
382    }
383}
384
385fn loop_expression(input: &[u8]) -> PResult<String> {
386    map(
387        map_res(
388            recognize(terminated(
389                expression,
390                opt(preceded(
391                    terminated(tag(".."), opt(char('='))),
392                    expression,
393                )),
394            )),
395            input_to_str,
396        ),
397        String::from,
398    )
399    .parse(input)
400}
401
402fn logic_expression(input: &[u8]) -> PResult<&str> {
403    map_res(
404        recognize((
405            opt(terminated(char('!'), spacelike)),
406            expression,
407            opt(pair(
408                rel_operator,
409                context("Expected expression", logic_expression),
410            )),
411        )),
412        input_to_str,
413    )
414    .parse(input)
415}
416
417fn rel_operator(input: &[u8]) -> PResult<&str> {
418    map_res(
419        delimited(
420            spacelike,
421            context(
422                "Expected relational operator",
423                alt((
424                    tag("!="),
425                    tag("&&"),
426                    tag("<="),
427                    tag("<"),
428                    tag("=="),
429                    tag(">="),
430                    tag(">"),
431                    tag("||"),
432                )),
433            ),
434            spacelike,
435        ),
436        input_to_str,
437    )
438    .parse(input)
439}
440
441#[cfg(test)]
442mod test {
443    use super::super::parseresult::show_errors;
444    use super::*;
445
446    #[test]
447    fn for_variable_simple() {
448        assert_eq!(
449            for_variable(b"foo").unwrap(),
450            (&b""[..], "foo".to_string())
451        )
452    }
453
454    #[test]
455    fn for_variable_tuple() {
456        assert_eq!(
457            for_variable(b"(foo, bar)").unwrap(),
458            (&b""[..], "(foo, bar)".to_string())
459        )
460    }
461
462    #[test]
463    fn for_variable_tuple_ref() {
464        assert_eq!(
465            for_variable(b"&(foo, bar)").unwrap(),
466            (&b""[..], "&(foo, bar)".to_string())
467        )
468    }
469
470    #[test]
471    fn for_variable_struct() {
472        assert_eq!(
473            for_variable(b"MyStruct{foo, bar}").unwrap(),
474            (&b""[..], "MyStruct{foo, bar}".to_string())
475        )
476    }
477
478    #[test]
479    fn call_simple() {
480        assert_eq!(
481            template_expression(b"@foo()"),
482            Ok((
483                &b""[..],
484                TemplateExpression::Expression {
485                    expr: "foo()".to_string(),
486                },
487            ))
488        )
489    }
490
491    /// Check that issue #53 stays fixed.
492    #[test]
493    fn call_empty_str() {
494        assert_eq!(
495            template_expression(b"@foo(\"\")"),
496            Ok((
497                &b""[..],
498                TemplateExpression::Expression {
499                    expr: "foo(\"\")".to_string(),
500                },
501            ))
502        )
503    }
504
505    #[test]
506    fn if_boolean_var() {
507        assert_eq!(
508            template_expression(b"@if cond { something }"),
509            Ok((
510                &b""[..],
511                TemplateExpression::IfBlock {
512                    expr: "cond".to_string(),
513                    body: vec![TemplateExpression::text(" something ")],
514                    else_body: None,
515                }
516            ))
517        )
518    }
519
520    #[test]
521    fn if_let() {
522        assert_eq!(
523            template_expression(b"@if let Some(x) = x { something }"),
524            Ok((
525                &b""[..],
526                TemplateExpression::IfBlock {
527                    expr: "let Some(x) = x".to_string(),
528                    body: vec![TemplateExpression::text(" something ")],
529                    else_body: None,
530                }
531            ))
532        )
533    }
534
535    #[test]
536    fn if_let_2() {
537        assert_eq!(
538            template_expression(b"@if let Some((x, y)) = x { something }"),
539            Ok((
540                &b""[..],
541                TemplateExpression::IfBlock {
542                    expr: "let Some((x, y)) = x".to_string(),
543                    body: vec![TemplateExpression::text(" something ")],
544                    else_body: None,
545                }
546            ))
547        )
548    }
549
550    #[test]
551    fn if_let_3() {
552        assert_eq!(
553            template_expression(
554                b"@if let Some(p) = Uri::borrow_from(&state) { something }"
555            ),
556            Ok((
557                &b""[..],
558                TemplateExpression::IfBlock {
559                    expr: "let Some(p) = Uri::borrow_from(&state)"
560                        .to_string(),
561                    body: vec![TemplateExpression::text(" something ")],
562                    else_body: None,
563                }
564            ))
565        )
566    }
567
568    #[test]
569    fn if_let_struct() {
570        assert_eq!(
571            template_expression(
572                b"@if let Struct{x, y} = variable { something }"
573            ),
574            Ok((
575                &b""[..],
576                TemplateExpression::IfBlock {
577                    expr: "let Struct{x, y} = variable".to_string(),
578                    body: vec![TemplateExpression::text(" something ")],
579                    else_body: None,
580                }
581            ))
582        )
583    }
584
585    #[test]
586    fn if_compare() {
587        assert_eq!(
588            template_expression(b"@if x == 17 { something }"),
589            Ok((
590                &b""[..],
591                TemplateExpression::IfBlock {
592                    expr: "x == 17".to_string(),
593                    body: vec![TemplateExpression::text(" something ")],
594                    else_body: None,
595                }
596            ))
597        )
598    }
599
600    /// Check that issue #53 stays fixed.
601    #[test]
602    fn if_compare_empty_string() {
603        // Note that x.is_empty() would be better in real code, but this and
604        // other uses of empty strings in conditionals should be ok.
605        assert_eq!(
606            template_expression(b"@if x == \"\" { something }"),
607            Ok((
608                &b""[..],
609                TemplateExpression::IfBlock {
610                    expr: "x == \"\"".to_string(),
611                    body: vec![TemplateExpression::text(" something ")],
612                    else_body: None,
613                }
614            ))
615        )
616    }
617
618    #[test]
619    fn if_complex_logig() {
620        assert_eq!(
621            template_expression(b"@if x == 17 || y && z() { something }"),
622            Ok((
623                &b""[..],
624                TemplateExpression::IfBlock {
625                    expr: "x == 17 || y && z()".to_string(),
626                    body: vec![TemplateExpression::text(" something ")],
627                    else_body: None,
628                }
629            ))
630        )
631    }
632    #[test]
633    fn if_missing_conditional() {
634        assert_eq!(
635            expression_error(b"@if { oops }"),
636            ":   1:@if { oops }\n\
637             :         ^ Error in conditional expression:\n\
638             :   1:@if { oops }\n\
639             :         ^ Expected expression\n\
640             :   1:@if { oops }\n\
641             :         ^ Expected rust expression\n"
642        )
643    }
644
645    #[test]
646    fn if_bad_let() {
647        assert_eq!(
648            expression_error(b"@if let foo { oops }"),
649            ":   1:@if let foo { oops }\n\
650             :         ^ Error in conditional expression:\n\
651             :   1:@if let foo { oops }\n\
652             :                 ^ Expected \'=\'\n"
653        )
654    }
655
656    #[test]
657    fn for_in_struct() {
658        assert_eq!(
659            template_expression(
660                b"@for Struct{x, y} in structs { something }"
661            ),
662            Ok((
663                &b""[..],
664                TemplateExpression::ForLoop {
665                    name: "Struct{x, y}".to_string(),
666                    expr: "structs".to_string(),
667                    body: vec![TemplateExpression::text(" something ")],
668                }
669            ))
670        )
671    }
672
673    #[test]
674    fn for_missing_in() {
675        // TODO The second part of this message isn't really helpful.
676        assert_eq!(
677            expression_error(b"@for what ever { hello }"),
678            ":   1:@for what ever { hello }\n\
679             :               ^ Expected \"in\"\n"
680        )
681    }
682
683    fn expression_error(input: &[u8]) -> String {
684        let mut buf = Vec::new();
685        if let Err(error) = template_expression(input) {
686            show_errors(&mut buf, input, &error, ":");
687        }
688        String::from_utf8(buf).unwrap()
689    }
690}