Search code examples
rustparser-combinatorsnom

Matching template filter expressions with nom


I'm working on a templating engine where some of the syntax could be like:

{{ somevar|filter }}

In place of somevar could be an arbitrary "expression", which is to say, either a variable name like somevar, or a nested filter expression (like {{ somevar|filter|anotherfilter }}). I'm trying to parse this using Rust's nom parser combinator library, but failing to get it to work so far.

Here's the parser I've come up with so far:

#[macro_use]
extern crate nom;

use std::str;

#[derive(Debug)]
pub enum Expr<'a> {
    Var(&'a [u8]),
    Filter(&'a str, Box<Expr<'a>>),
}

#[derive(Debug)]
pub enum Node<'a> {
    Lit(&'a [u8]),
    Expr(Expr<'a>),
}

named!(expr_var<Expr>, dbg_dmp!(map!(nom::alphanumeric, Expr::Var)));

named!(expr_filter<Expr>,
    dbg_dmp!(do_parse!(
         val: any_expr >>
         tag_s!("|") >>
         name: map_res!(nom::alphanumeric, str::from_utf8) >>
         (Expr::Filter(name, Box::new(val)))
    ))
);

named!(any_expr<Expr>, dbg_dmp!(ws!(
    alt_complete!(
        expr_filter |
        expr_var  
    ))));

named!(expr_node<Node>, dbg_dmp!(map!(
    delimited!(tag_s!("{{"), any_expr, tag_s!("}}")),
    Node::Expr)));

named!(parse_template< Vec<Node> >, many1!(expr_node));

With a playground. The current version panics through a stack overflow. I can fix this by reversing the expr_var | expr_filter order in any_expr, but then I'm back to basically the same error as before.


Solution

  • I fixed it by writing my own parser function:

    named!(expr_var<Expr>, map!(nom::alphanumeric, Expr::Var));
    
    fn expr_filtered(i: &[u8]) -> IResult<&[u8], Expr> {
        let (mut left, mut expr) = match expr_var(i) {
            IResult::Error(err) => { return IResult::Error(err); },
            IResult::Incomplete(needed) => { return IResult::Incomplete(needed); },
            IResult::Done(left, res) => (left, res),
        };
        while left[0] == b'|' {
            match nom::alphanumeric(&left[1..]) {
                IResult::Error(err) => {
                    return IResult::Error(err);
                },
                IResult::Incomplete(needed) => {
                    return IResult::Incomplete(needed);
                },
                IResult::Done(new_left, res) => {
                    left = new_left;
                    expr = Expr::Filter(str::from_utf8(res).unwrap(), Box::new(expr));
                },
            };
        }
        return IResult::Done(left, expr);
    }
    
    named!(expr_node<Node>, map!(
        delimited!(tag_s!("{{"), ws!(expr_filtered), tag_s!("}}")),
    Node::Expr));
    

    There is probably some nicer way to do the same thing with nom macros, but at least I got something working.