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::object_parser::minimal_set_object; use super::org_source::OrgSource; use super::util::exit_matcher_parser; use super::util::maybe_consume_object_trailing_whitespace_if_not_exiting; use crate::context::parser_with_context; use crate::context::ContextElement; use crate::context::ExitClass; use crate::context::ExitMatcherNode; use crate::context::RefContext; use crate::error::CustomError; use crate::error::MyError; use crate::error::Res; use crate::parser::util::get_consumed; use crate::types::Object; use crate::types::RadioLink; use crate::types::RadioTarget; #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] pub(crate) fn radio_link<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, ) -> Res, RadioLink<'s>> { for radio_target in &context.get_global_settings().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: source.into(), children: rematched_target, }, )); } } Err(nom::Err::Error(CustomError::MyError(MyError( "NoRadioLink".into(), )))) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] pub(crate) fn rematch_target<'x, 'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, target: &'x Vec>, input: OrgSource<'s>, ) -> Res, 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::Italic(italic) => { let (new_remaining, new_match) = italic.rematch_object(context, remaining)?; remaining = new_remaining; new_matches.push(new_match); } Object::Underline(underline) => { let (new_remaining, new_match) = underline.rematch_object(context, remaining)?; remaining = new_remaining; new_matches.push(new_match); } Object::StrikeThrough(strikethrough) => { let (new_remaining, new_match) = strikethrough.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".into(), )))); } }; } Ok((remaining, new_matches)) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] pub(crate) fn radio_target<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, ) -> Res, RadioTarget<'s>> { let (remaining, _opening) = tag("<<<")(input)?; let parser_context = ContextElement::ExitMatcherNode(ExitMatcherNode { class: ExitClass::Gamma, exit_matcher: &radio_target_end, }); let parser_context = context.with_additional_node(&parser_context); 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) = maybe_consume_object_trailing_whitespace_if_not_exiting(context, remaining)?; let source = get_consumed(input, remaining); Ok(( remaining, RadioTarget { source: source.into(), children, }, )) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] fn radio_target_end<'b, 'g, 'r, 's>( _context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, ) -> Res, OrgSource<'s>> { alt((tag("<"), tag(">"), line_ending))(input) } pub(crate) trait RematchObject<'x> { fn rematch_object<'b, 'g, 'r, 's>( &'x self, context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, ) -> Res, Object<'s>>; } #[cfg(test)] mod tests { use super::*; use crate::context::Context; use crate::context::GlobalSettings; use crate::context::List; use crate::parser::element_parser::element; use crate::types::Bold; use crate::types::Element; use crate::types::GetStandardProperties; use crate::types::PlainText; #[test] fn plain_text_radio_target() { let input = OrgSource::new("foo bar baz"); let radio_target_match = vec![Object::PlainText(PlainText { source: "bar" })]; let global_settings = { let mut global_settings = GlobalSettings::default(); global_settings.radio_targets = vec![&radio_target_match]; global_settings }; let initial_context = ContextElement::document_context(); let initial_context = Context::new(&global_settings, List::new(&initial_context)); let paragraph_matcher = parser_with_context!(element(true))(&initial_context); let (remaining, first_paragraph) = paragraph_matcher(input).expect("Parse first paragraph"); let first_paragraph = match first_paragraph { Element::Paragraph(paragraph) => paragraph, _ => panic!("Should be a paragraph!"), }; assert_eq!(Into::<&str>::into(remaining), ""); assert_eq!( first_paragraph.get_standard_properties().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 = OrgSource::new("foo *bar* baz"); let radio_target_match = vec![Object::Bold(Bold { source: "*bar*", children: vec![Object::PlainText(PlainText { source: "bar" })], })]; let global_settings = { let mut global_settings = GlobalSettings::default(); global_settings.radio_targets = vec![&radio_target_match]; global_settings }; let initial_context = ContextElement::document_context(); let initial_context = Context::new(&global_settings, List::new(&initial_context)); let paragraph_matcher = parser_with_context!(element(true))(&initial_context); let (remaining, first_paragraph) = paragraph_matcher(input.into()).expect("Parse first paragraph"); let first_paragraph = match first_paragraph { Element::Paragraph(paragraph) => paragraph, _ => panic!("Should be a paragraph!"), }; assert_eq!(Into::<&str>::into(remaining), ""); assert_eq!( first_paragraph.get_standard_properties().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" })] })] }) ); } }