ructe 0.18.2

Rust Compiled Templates, efficient type-safe web page templates.
Documentation
use crate::expression::{input_to_str, rust_name};
use crate::parseresult::PResult;
use crate::spacelike::spacelike;
use crate::templateexpression::{template_expression, TemplateExpression};
use nom::branch::alt;
use nom::bytes::complete::is_not;
use nom::bytes::complete::tag;
use nom::character::complete::{char, multispace0};
use nom::combinator::{map, map_res, opt, recognize, value};
use nom::error::context;
use nom::multi::{many0, many_till, separated_list0, separated_list1};
use nom::sequence::{delimited, preceded, terminated};
use nom::Parser as _;
use std::fmt::Write;

#[derive(Debug, PartialEq, Eq)]
pub struct Template {
    preamble: Vec<String>,
    type_args: String,
    args: Vec<String>,
    body: Vec<TemplateExpression>,
}

impl Template {
    pub fn write_rust(
        &self,
        out: &mut impl Write,
        name: &str,
    ) -> std::fmt::Result {
        out.write_str(
            "use std::io::{self, Write};\n\
             #[allow(clippy::useless_attribute, unused)]\n\
             use super::{Html,ToHtml};\n",
        )?;
        for line in &self.preamble {
            writeln!(out, "{line};")?;
        }
        writeln!(
            out,
            "\n\
             #[allow(clippy::used_underscore_binding)]\n\
             pub fn {name}<{ta}{ta_sep}W>(\
             \n  #[allow(unused_mut)] mut _ructe_out_: W,",
            name = name,
            ta = self.type_args,
            ta_sep = if self.type_args.is_empty() { "" } else { ", " },
        )?;
        for arg in &self.args {
            writeln!(
                out,
                "  {},",
                arg.replace(
                    " Content",
                    " impl FnOnce(&mut W) -> io::Result<()>"
                )
            )?;
        }
        writeln!(
            out,
            ") -> io::Result<()>\n\
             where W: Write {{",
        )?;
        for b in &self.body {
            b.write_code(out)?;
        }
        writeln!(out, "Ok(())\n}}")?;
        Ok(())
    }
}

pub fn template(input: &[u8]) -> PResult<Template> {
    map(
        (
            spacelike,
            many0(map(
                delimited(
                    tag("@"),
                    map_res(is_not(";()"), input_to_str),
                    terminated(tag(";"), spacelike),
                ),
                String::from,
            )),
            context("expected '@('...')' template declaration.", tag("@")),
            opt(delimited(
                terminated(tag("<"), multispace0),
                context(
                    "expected type argument or '>'",
                    map_res(
                        recognize(separated_list1(
                            terminated(tag(","), multispace0),
                            context(
                                "expected lifetime declaration",
                                preceded(tag("'"), rust_name),
                            ),
                        )),
                        input_to_str,
                    ),
                ),
                tag(">"),
            )),
            delimited(
                context(
                    "expected '('...')' template arguments declaration.",
                    terminated(tag("("), multispace0),
                ),
                separated_list0(
                    terminated(tag(","), multispace0),
                    context(
                        "expected formal argument",
                        map(formal_argument, String::from),
                    ),
                ),
                context(
                    "expected ',' or ')'.",
                    delimited(multispace0, tag(")"), spacelike),
                ),
            ),
            many_till(
                context(
                    "Error in expression starting here:",
                    template_expression,
                ),
                end_of_file,
            ),
        ),
        |((), preamble, _, type_args, args, body)| Template {
            preamble,
            type_args: type_args.map(String::from).unwrap_or_default(),
            args,
            body: body.0,
        },
    )
    .parse(input)
}

fn end_of_file(input: &[u8]) -> PResult<()> {
    if input.is_empty() {
        Ok((input, ()))
    } else {
        use nom_language::error::{VerboseError, VerboseErrorKind};
        Err(nom::Err::Error(VerboseError {
            errors: vec![(input, VerboseErrorKind::Context("end of file"))],
        }))
    }
}

fn formal_argument(input: &[u8]) -> PResult<&str> {
    map_res(
        recognize((
            rust_name,
            spacelike,
            char(':'),
            spacelike,
            type_expression,
        )),
        input_to_str,
    )
    .parse(input)
}

fn type_expression(input: &[u8]) -> PResult<()> {
    value(
        (),
        (
            alt((tag("&"), tag(""))),
            opt(lifetime),
            delimited(
                spacelike,
                alt((tag("impl"), tag("dyn"), tag(""))),
                spacelike,
            ),
            context(
                "Expected rust type expression",
                alt((
                    value((), rust_name),
                    delimited(tag("["), value((), type_expression), tag("]")),
                    delimited(
                        tag("("),
                        value((), comma_type_expressions),
                        tag(")"),
                    ),
                )),
            ),
            opt(delimited(tag("<"), comma_type_expressions, tag(">"))),
        ),
    )
    .parse(input)
}

pub fn comma_type_expressions(input: &[u8]) -> PResult<()> {
    value(
        (),
        terminated(
            separated_list0(
                preceded(tag(","), multispace0),
                alt((type_expression, lifetime)),
            ),
            opt(preceded(tag(","), multispace0)),
        ),
    )
    .parse(input)
}

fn lifetime(input: &[u8]) -> PResult<()> {
    delimited(spacelike, value((), tag("'")), rust_name).parse(input)
}

#[cfg(test)]
mod test {
    use super::type_expression;

    #[test]
    fn tuple() {
        check_type_expr("(Foo, Bar)");
    }

    #[test]
    fn unspaced_tuple() {
        check_type_expr("(Foo,Bar)");
    }

    #[test]
    fn tuple_with_trailing() {
        check_type_expr("(Foo,Bar,)");
    }

    #[test]
    fn generic() {
        check_type_expr("HashMap<Foo, Bar>");
    }

    #[test]
    fn unspaced_generic() {
        check_type_expr("HashMap<Foo,Bar>");
    }

    #[test]
    fn generic_with_trailing() {
        check_type_expr("Vec<Foo,>");
    }

    #[test]
    fn generic_with_lifetime() {
        check_type_expr("SomeTypeWithRef<'a>");
    }

    #[test]
    fn generic_with_anonymous_lifetime() {
        check_type_expr("SomeTypeWithRef<'_>");
    }

    #[test]
    fn multiword_constant() {
        check_type_expr("ONE_TWO_THREE");
    }

    fn check_type_expr(expr: &str) {
        assert_eq!(type_expression(expr.as_bytes()), Ok((&b""[..], ())));
    }
}