diff --git a/src/compare/sexp.rs b/src/compare/sexp.rs index 2adf545..e8cf1fa 100644 --- a/src/compare/sexp.rs +++ b/src/compare/sexp.rs @@ -5,10 +5,15 @@ use nom::bytes::complete::take_till1; use nom::character::complete::multispace0; use nom::character::complete::multispace1; use nom::character::complete::one_of; +use nom::combinator::map; use nom::combinator::not; +use nom::combinator::opt; use nom::combinator::peek; +use nom::combinator::verify; use nom::multi::separated_list1; use nom::sequence::delimited; +use nom::sequence::preceded; +use nom::sequence::tuple; use super::error::Res; @@ -16,6 +21,13 @@ use super::error::Res; pub enum Token<'s> { Atom(&'s str), List(Vec>), + TextWithProperties(TextWithProperties<'s>), +} + +#[derive(Debug)] +pub struct TextWithProperties<'s> { + text: &'s str, + properties: Vec>, } #[tracing::instrument(ret, level = "debug")] @@ -33,20 +45,20 @@ fn token<'s>(input: &'s str) -> Res<&'s str, Token<'s>> { #[tracing::instrument(ret, level = "debug")] fn list<'s>(input: &'s str) -> Res<&'s str, Token<'s>> { - let (remaining, opening_paren) = tag("(")(input)?; + let (remaining, _) = tag("(")(input)?; let (remaining, children) = delimited( multispace0, separated_list1(multispace1, token), multispace0, )(remaining)?; - let (remaining, closing_paren) = tag(")")(remaining)?; + let (remaining, _) = tag(")")(remaining)?; Ok((remaining, Token::List(children))) } #[tracing::instrument(ret, level = "debug")] fn atom<'s>(input: &'s str) -> Res<&'s str, Token<'s>> { not(peek(tag(")")))(input)?; - alt((quoted_atom, unquoted_atom))(input) + alt((text_with_properties, quoted_atom, unquoted_atom))(input) } #[tracing::instrument(ret, level = "debug")] @@ -74,6 +86,29 @@ fn quoted_atom<'s>(input: &'s str) -> Res<&'s str, Token<'s>> { Ok((remaining, Token::Atom(source))) } +fn text_with_properties<'s>(input: &'s str) -> Res<&'s str, Token<'s>> { + let (remaining, _) = tag("#(")(input)?; + let (remaining, (text, props)) = delimited( + multispace0, + tuple(( + map(quoted_atom, |atom| match atom { + Token::Atom(body) => body, + _ => unreachable!(), + }), + preceded(multispace1, opt(separated_list1(multispace1, token))), + )), + multispace0, + )(remaining)?; + let (remaining, _) = tag(")")(remaining)?; + Ok(( + remaining, + Token::TextWithProperties(TextWithProperties { + text, + properties: props.unwrap_or(Vec::new()), + }), + )) +} + /// Get a slice of the string that was consumed in a parser using the original input to the parser and the remaining input after the parser. fn get_consumed<'s>(input: &'s str, remaining: &'s str) -> &'s str { assert!(is_slice_of(input, remaining)); @@ -105,6 +140,7 @@ mod tests { assert!(match parsed { Token::Atom(_) => false, Token::List(_) => true, + Token::TextWithProperties(_) => false, }); } @@ -116,6 +152,18 @@ mod tests { assert!(match parsed { Token::Atom(_) => false, Token::List(_) => true, + Token::TextWithProperties(_) => false, }); + let children = match parsed { + Token::List(children) => children, + _ => panic!("Should be a list."), + }; + assert_eq!( + match children.first() { + Some(Token::Atom(body)) => *body, + _ => panic!("First child should be an atom."), + }, + r#""foo""# + ) } }