use nom::branch::alt; use nom::bytes::complete::tag; use nom::character::complete::line_ending; use nom::character::complete::space0; use nom::combinator::verify; use nom::multi::many_till; use super::Context; use super::Object; use crate::error::CustomError; use crate::error::MyError; use crate::error::Res; use crate::parser::exiting::ExitClass; use crate::parser::object_parser::minimal_set_object; use crate::parser::parser_context::ContextElement; use crate::parser::parser_context::ExitMatcherNode; use crate::parser::parser_with_context::parser_with_context; use crate::parser::util::exit_matcher_parser; use crate::parser::util::get_consumed; use crate::parser::RadioLink; use crate::parser::RadioTarget; #[tracing::instrument(ret, level = "debug")] pub fn radio_link<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, RadioLink<'s>> { let radio_targets = context .iter() .filter_map(|context_element| match context_element.get_data() { ContextElement::RadioTarget(targets) => Some(targets), _ => None, }) .flatten(); for radio_target in radio_targets { let rematched_target = rematch_target(context, radio_target, input); if let Ok((remaining, rematched_target)) = rematched_target { let (remaining, _) = space0(remaining)?; let source = get_consumed(input, remaining); return Ok(( remaining, RadioLink { source, children: rematched_target, }, )); } } Err(nom::Err::Error(CustomError::MyError(MyError( "NoRadioLink", )))) } #[tracing::instrument(ret, level = "debug")] pub fn rematch_target<'x, 'r, 's>( context: Context<'r, 's>, target: &'x Vec>, input: &'s str, ) -> Res<&'s str, Vec>> { let mut remaining = input; let mut new_matches = Vec::with_capacity(target.len()); for original_object in target { match original_object { // TODO: The rest of the minimal set of objects. Object::Bold(bold) => { let (new_remaining, new_match) = bold.rematch_object(context, remaining)?; remaining = new_remaining; new_matches.push(new_match); } Object::PlainText(plaintext) => { let (new_remaining, new_match) = plaintext.rematch_object(context, remaining)?; remaining = new_remaining; new_matches.push(new_match); } _ => { return Err(nom::Err::Error(CustomError::MyError(MyError( "OnlyMinimalSetObjectsAllowed", )))); } }; } Ok((remaining, new_matches)) } #[tracing::instrument(ret, level = "debug")] pub fn radio_target<'r, 's>( context: Context<'r, 's>, input: &'s str, ) -> Res<&'s str, RadioTarget<'s>> { let (remaining, _opening) = tag("<<<")(input)?; let parser_context = context.with_additional_node(ContextElement::ExitMatcherNode(ExitMatcherNode { class: ExitClass::Beta, exit_matcher: &radio_target_end, })); let (remaining, (children, _exit_contents)) = verify( many_till( parser_with_context!(minimal_set_object)(&parser_context), parser_with_context!(exit_matcher_parser)(&parser_context), ), |(children, _exit_contents)| !children.is_empty(), )(remaining)?; let (remaining, _closing) = tag(">>>")(remaining)?; let (remaining, _trailing_whitespace) = space0(remaining)?; let source = get_consumed(input, remaining); Ok((remaining, RadioTarget { source, children })) } #[tracing::instrument(ret, level = "debug")] fn radio_target_end<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, &'s str> { alt((tag("<"), tag(">"), line_ending))(input) } pub trait RematchObject<'x> { fn rematch_object<'r, 's>( &'x self, context: Context<'r, 's>, input: &'s str, ) -> Res<&'s str, Object<'s>>; } #[cfg(test)] mod tests { use super::*; use crate::parser::element_parser::element; use crate::parser::parser_context::ContextElement; use crate::parser::parser_context::ContextTree; use crate::parser::parser_with_context::parser_with_context; use crate::parser::source::Source; use crate::parser::Bold; use crate::parser::PlainText; #[test] fn plain_text_radio_target() { let input = "foo bar baz"; let radio_target_match = vec![Object::PlainText(PlainText { source: "bar" })]; let initial_context: ContextTree<'_, '_> = ContextTree::new(); let document_context = initial_context .with_additional_node(ContextElement::DocumentRoot(input)) .with_additional_node(ContextElement::RadioTarget(vec![&radio_target_match])); let paragraph_matcher = parser_with_context!(element(true))(&document_context); let (remaining, first_paragraph) = paragraph_matcher(input).expect("Parse first paragraph"); let first_paragraph = match first_paragraph { crate::parser::Element::Paragraph(paragraph) => paragraph, _ => panic!("Should be a paragraph!"), }; assert_eq!(remaining, ""); assert_eq!(first_paragraph.get_source(), "foo bar baz"); assert_eq!(first_paragraph.children.len(), 3); assert_eq!( first_paragraph .children .get(1) .expect("Len already asserted to be 3"), &Object::RadioLink(RadioLink { source: "bar ", children: vec![Object::PlainText(PlainText { source: "bar" })] }) ); } #[test] fn bold_radio_target() { let input = "foo *bar* baz"; let radio_target_match = vec![Object::Bold(Bold { source: "*bar*", children: vec![Object::PlainText(PlainText { source: "bar" })], })]; let initial_context: ContextTree<'_, '_> = ContextTree::new(); let document_context = initial_context .with_additional_node(ContextElement::DocumentRoot(input)) .with_additional_node(ContextElement::RadioTarget(vec![&radio_target_match])); let paragraph_matcher = parser_with_context!(element(true))(&document_context); let (remaining, first_paragraph) = paragraph_matcher(input).expect("Parse first paragraph"); let first_paragraph = match first_paragraph { crate::parser::Element::Paragraph(paragraph) => paragraph, _ => panic!("Should be a paragraph!"), }; assert_eq!(remaining, ""); assert_eq!(first_paragraph.get_source(), "foo *bar* baz"); assert_eq!(first_paragraph.children.len(), 3); assert_eq!( first_paragraph .children .get(1) .expect("Len already asserted to be 3"), &Object::RadioLink(RadioLink { source: "*bar* ", children: vec![Object::Bold(Bold { source: "*bar* ", children: vec![Object::PlainText(PlainText { source: "bar" })] })] }) ); } }